Compare commits
12 커밋
389faa72be
...
main
| 작성자 | SHA1 | 날짜 | |
|---|---|---|---|
| 6c7f6ca46d | |||
| f4c92b18f7 | |||
| b63de8bc4e | |||
| d415ed4525 | |||
| 12b61d4c48 | |||
| 7ba60e8561 | |||
| 05f582df2a | |||
| bfe006338a | |||
| 5a0ca30cf7 | |||
| e08097a067 | |||
| 4c6828a311 | |||
| 1cd39aff5b |
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@@ -1,8 +0,0 @@
|
|||||||
# 디폴트 무시된 파일
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# 에디터 기반 HTTP 클라이언트 요청
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/simple_backup.iml" filepath="$PROJECT_DIR$/.idea/simple_backup.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
9
.idea/simple_backup.iml
generated
9
.idea/simple_backup.iml
generated
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="WEB_MODULE" version="4">
|
|
||||||
<component name="Go" enabled="true" />
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
35
README.md
35
README.md
@@ -1,2 +1,37 @@
|
|||||||
# simple_backup
|
# simple_backup
|
||||||
|
## Build
|
||||||
|
```sh
|
||||||
|
$ GOOS=linux GOARCH=amd64 go build -o backup git.lhk.o-r.kr/freerer2/simple_backup/cmd
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
```sh
|
||||||
|
사용법: backup <원본경로> <백업경로> [옵션]
|
||||||
|
|
||||||
|
옵션:
|
||||||
|
-g, --group-by 백업 폴더 구조 기준 (기본값: day)
|
||||||
|
가능한 값: year, mon, day, hour, min, sec
|
||||||
|
|
||||||
|
-i, --incremental 증분 백업 사용
|
||||||
|
기존 백업과 비교하여 변경된 파일만 백업
|
||||||
|
(.backup_meta.json을 생성하여 이용함.)
|
||||||
|
|
||||||
|
-c, --compare 파일 비교 방식 선택 (기본값: time)
|
||||||
|
- time: 파일 수정 시간으로 비교
|
||||||
|
- hash: 파일 내용의 해시값으로 비교
|
||||||
|
|
||||||
|
-d, --dry-run 실하지 않고 어떤 파일이 복사되는지 출력
|
||||||
|
실제 파일 시스템을 변경하지 않음
|
||||||
|
|
||||||
|
-v, --verbose 복사 로그 자세히 출력
|
||||||
|
진행 상황과 세부 정보를 표시
|
||||||
|
|
||||||
|
-f, --force 속성 무시하고 무조건 덮어쓰기
|
||||||
|
기존 파일 존재 시 강제로 덮어씀
|
||||||
|
|
||||||
|
예시:
|
||||||
|
backup /source /simple_backup --group-by day --compare hash
|
||||||
|
backup /home/user/docs /simple_backup/docs -i -v
|
||||||
|
backup /data /simple_backup -d --force
|
||||||
|
backup /data /simple_backup -i -g sec -c hash -v
|
||||||
|
```
|
||||||
7
build.sh
Normal file
7
build.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
for OS in aix android darwin dragonfly freebsd illumos ios js linux netbsd openbsd plan9 solaris wasip1 windows; do
|
||||||
|
for ARCH in ppc64 386 amd64 arm arm64 riscv64 wasm loong64 mips mips64 mips64le mipsle ppc64 ppc64le s390x; do
|
||||||
|
OUTPUT="backup_${OS}_${ARCH}"
|
||||||
|
[ $OS = "windows" ] && OUTPUT+='.exe'
|
||||||
|
GOOS=$OS GOARCH=$ARCH go build -o $OUTPUT git.lhk.o-r.kr/freerer2/simple_backup/cmd
|
||||||
|
done
|
||||||
|
done
|
||||||
18
cmd/main.go
18
cmd/main.go
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -45,12 +46,13 @@ func showHelp() {
|
|||||||
|
|
||||||
-i, --incremental 증분 백업 사용
|
-i, --incremental 증분 백업 사용
|
||||||
기존 백업과 비교하여 변경된 파일만 백업
|
기존 백업과 비교하여 변경된 파일만 백업
|
||||||
|
(.backup_meta.json을 생성하여 이용함.)
|
||||||
|
|
||||||
-c, --compare 파일 비교 방식 선택 (기본값: time)
|
-c, --compare 파일 비교 방식 선택 (기본값: time)
|
||||||
- time: 파일 수정 시간으로 비교
|
- time: 파일 수정 시간으로 비교
|
||||||
- hash: 파일 내용의 해시값으로 비교
|
- hash: 파일 내용의 해시값으로 비교
|
||||||
|
|
||||||
-d, --dry-run 하지 않고 어떤 파일이 복사되는지 출력
|
-d, --dry-run 실하지 않고 어떤 파일이 복사되는지 출력
|
||||||
실제 파일 시스템을 변경하지 않음
|
실제 파일 시스템을 변경하지 않음
|
||||||
|
|
||||||
-v, --verbose 복사 로그 자세히 출력
|
-v, --verbose 복사 로그 자세히 출력
|
||||||
@@ -64,7 +66,7 @@ func showHelp() {
|
|||||||
backup /home/user/docs /simple_backup/docs -i -v
|
backup /home/user/docs /simple_backup/docs -i -v
|
||||||
backup /data /simple_backup -d --force
|
backup /data /simple_backup -d --force
|
||||||
|
|
||||||
자세한 정보: https://github.com/yourusername/backup`
|
자세한 정보: https://git.lhk.o-r.kr/simple-utils/simple_backup`
|
||||||
|
|
||||||
fmt.Println(helpText)
|
fmt.Println(helpText)
|
||||||
}
|
}
|
||||||
@@ -220,8 +222,8 @@ func main() {
|
|||||||
if p.TotalFiles > 0 {
|
if p.TotalFiles > 0 {
|
||||||
percentComplete = float64(p.ProcessedFiles) / float64(p.TotalFiles) * 100
|
percentComplete = float64(p.ProcessedFiles) / float64(p.TotalFiles) * 100
|
||||||
}
|
}
|
||||||
fmt.Printf("\r진행률: %.1f%% (%d/%d) - 현재: %s\n",
|
fmt.Printf("\r진행률: %.1f%% (%d/%d) - %s: %s\n",
|
||||||
percentComplete, p.ProcessedFiles, p.TotalFiles, p.CurrentFile)
|
percentComplete, p.ProcessedFiles, p.TotalFiles, p.Status, p.CurrentFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +232,7 @@ func main() {
|
|||||||
mode = backup.CompareHash
|
mode = backup.CompareHash
|
||||||
}
|
}
|
||||||
|
|
||||||
backupOpts := backup.BackupOptions{
|
backupOpts := backup.Options{
|
||||||
DryRun: opts.DryRun,
|
DryRun: opts.DryRun,
|
||||||
Verbose: opts.Verbose,
|
Verbose: opts.Verbose,
|
||||||
Force: opts.Force,
|
Force: opts.Force,
|
||||||
@@ -246,15 +248,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := backup.RunBackup(ctx, opts.Src, backupPath, backupOpts); err != nil {
|
if err := backup.RunBackup(ctx, opts.Src, backupPath, backupOpts); err != nil {
|
||||||
if err == context.Canceled {
|
if errors.Is(err, context.Canceled) {
|
||||||
fmt.Println("\n백업이 취소되었습니다")
|
fmt.Println("\n백업이 취소되었습니다")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("\n백업 중 오류 발생:", err)
|
fmt.Println("\n백업 중 오류 발생:", err)
|
||||||
}
|
}
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Verbose {
|
|
||||||
fmt.Println("\n백업이 성공적으로 완료되었습니다")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,14 @@ const (
|
|||||||
|
|
||||||
type Progress struct {
|
type Progress struct {
|
||||||
CurrentFile string
|
CurrentFile string
|
||||||
|
Status string
|
||||||
ProcessedFiles int
|
ProcessedFiles int
|
||||||
TotalFiles int
|
TotalFiles int
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProgressCallback func(Progress)
|
type ProgressCallback func(Progress)
|
||||||
|
|
||||||
type BackupOptions struct {
|
type Options struct {
|
||||||
DryRun bool
|
DryRun bool
|
||||||
Verbose bool
|
Verbose bool
|
||||||
Force bool
|
Force bool
|
||||||
@@ -42,24 +43,24 @@ type BackupOptions struct {
|
|||||||
dirCache *path.DirCache
|
dirCache *path.DirCache
|
||||||
}
|
}
|
||||||
|
|
||||||
type BackupMeta struct {
|
type Meta struct {
|
||||||
LastBackup time.Time `json:"lastBackup"`
|
LastBackup time.Time `json:"lastBackup"`
|
||||||
Files map[string]string `json:"files"` // 파일 경로 -> 해시 또는 수정 시간
|
Files map[string]string `json:"files"` // 파일 경로 -> 해시 또는 수정 시간
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadBackupMeta(dst string) (*BackupMeta, error) {
|
func loadBackupMeta(src string) (*Meta, error) {
|
||||||
metaPath := filepath.Join(dst, constants.MetaFileName)
|
metaPath := filepath.Join(src, constants.MetaFileName)
|
||||||
data, err := os.ReadFile(metaPath)
|
data, err := os.ReadFile(metaPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return &BackupMeta{
|
return &Meta{
|
||||||
Files: make(map[string]string),
|
Files: make(map[string]string),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("메타 파일을 읽을 수 없습니다: %w", err)
|
return nil, fmt.Errorf("메타 파일을 읽을 수 없습니다: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var meta BackupMeta
|
var meta Meta
|
||||||
if err := json.Unmarshal(data, &meta); err != nil {
|
if err := json.Unmarshal(data, &meta); err != nil {
|
||||||
return nil, fmt.Errorf("메타 파일을 파싱할 수 없습니다: %w", err)
|
return nil, fmt.Errorf("메타 파일을 파싱할 수 없습니다: %w", err)
|
||||||
}
|
}
|
||||||
@@ -70,8 +71,8 @@ func loadBackupMeta(dst string) (*BackupMeta, error) {
|
|||||||
return &meta, nil
|
return &meta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveBackupMeta(dst string, meta *BackupMeta) error {
|
func saveBackupMeta(src string, meta *Meta) error {
|
||||||
metaPath := filepath.Join(dst, constants.MetaFileName)
|
metaPath := filepath.Join(src, constants.MetaFileName)
|
||||||
data, err := json.MarshalIndent(meta, "", " ")
|
data, err := json.MarshalIndent(meta, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("메타 데이터를 직렬화할 수 없습니다: %w", err)
|
return fmt.Errorf("메타 데이터를 직렬화할 수 없습니다: %w", err)
|
||||||
@@ -104,7 +105,8 @@ func calculateFileHash(path string) (string, error) {
|
|||||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunBackup(ctx context.Context, src, dst string, opts BackupOptions) error {
|
func RunBackup(ctx context.Context, src, dst string, opts Options) error {
|
||||||
|
var backupF = false
|
||||||
if opts.dirCache == nil {
|
if opts.dirCache == nil {
|
||||||
opts.dirCache = path.NewDirCache()
|
opts.dirCache = path.NewDirCache()
|
||||||
}
|
}
|
||||||
@@ -118,10 +120,10 @@ func RunBackup(ctx context.Context, src, dst string, opts BackupOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 증분 백업을 위한 메타데이터 로드
|
// 증분 백업을 위한 메타데이터 로드
|
||||||
var meta *BackupMeta
|
var meta *Meta
|
||||||
var err error
|
var err error
|
||||||
if opts.Incremental {
|
if opts.Incremental {
|
||||||
meta, err = loadBackupMeta(dst)
|
meta, err = loadBackupMeta(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -180,13 +182,6 @@ func RunBackup(ctx context.Context, src, dst string, opts BackupOptions) error {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// 진행 상황 업데이트
|
|
||||||
progress.CurrentFile = srcPath
|
|
||||||
progress.ProcessedFiles = i + 1
|
|
||||||
if opts.Progress != nil {
|
|
||||||
opts.Progress(progress)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 상대 경로 계산
|
// 상대 경로 계산
|
||||||
relPath, err := filepath.Rel(src, srcPath)
|
relPath, err := filepath.Rel(src, srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -199,7 +194,11 @@ func RunBackup(ctx context.Context, src, dst string, opts BackupOptions) error {
|
|||||||
dstInfo, err := os.Stat(dstPath)
|
dstInfo, err := os.Stat(dstPath)
|
||||||
fileExists := err == nil
|
fileExists := err == nil
|
||||||
|
|
||||||
if fileExists && !opts.Force {
|
// 진행 상황 업데이트
|
||||||
|
progress.CurrentFile = srcPath
|
||||||
|
progress.ProcessedFiles = i + 1
|
||||||
|
|
||||||
|
if !opts.Force {
|
||||||
if opts.Incremental {
|
if opts.Incremental {
|
||||||
// 증분 백업 모드에서는 메타데이터를 확인
|
// 증분 백업 모드에서는 메타데이터를 확인
|
||||||
srcInfo, err := os.Stat(srcPath)
|
srcInfo, err := os.Stat(srcPath)
|
||||||
@@ -214,7 +213,8 @@ func RunBackup(ctx context.Context, src, dst string, opts BackupOptions) error {
|
|||||||
case CompareTime:
|
case CompareTime:
|
||||||
newValue = srcInfo.ModTime().String()
|
newValue = srcInfo.ModTime().String()
|
||||||
if exists && oldValue == newValue {
|
if exists && oldValue == newValue {
|
||||||
opts.Logger.Printf("건너뜀 (시간 동일): %s\n", relPath)
|
progress.Status = "건너뜀 (시간 동일)"
|
||||||
|
opts.Progress(progress)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
case CompareHash:
|
case CompareHash:
|
||||||
@@ -223,23 +223,44 @@ func RunBackup(ctx context.Context, src, dst string, opts BackupOptions) error {
|
|||||||
return fmt.Errorf("파일 해시를 계산할 수 없습니다: %w", err)
|
return fmt.Errorf("파일 해시를 계산할 수 없습니다: %w", err)
|
||||||
}
|
}
|
||||||
if exists && oldValue == newValue {
|
if exists && oldValue == newValue {
|
||||||
opts.Logger.Printf("건너뜀 (해시 동일): %s\n", relPath)
|
progress.Status = "건너뜀 (해시 동일)"
|
||||||
|
opts.Progress(progress)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
meta.Files[relPath] = newValue
|
meta.Files[relPath] = newValue
|
||||||
} else {
|
} else {
|
||||||
|
if fileExists {
|
||||||
// 일반 모드에서는 크기와 수정 시간만 비교
|
// 일반 모드에서는 크기와 수정 시간만 비교
|
||||||
srcInfo, err := os.Stat(srcPath)
|
srcInfo, err := os.Stat(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("원본 파일 정보를 읽을 수 없습니다: %w", err)
|
return fmt.Errorf("원본 파일 정보를 읽을 수 없습니다: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch opts.CompareMode {
|
||||||
|
case CompareTime:
|
||||||
if srcInfo.Size() == dstInfo.Size() && srcInfo.ModTime().Equal(dstInfo.ModTime()) {
|
if srcInfo.Size() == dstInfo.Size() && srcInfo.ModTime().Equal(dstInfo.ModTime()) {
|
||||||
opts.Logger.Printf("건너뜀 (동일): %s\n", relPath)
|
progress.Status = "건너뜀 (시간 동일)"
|
||||||
|
opts.Progress(progress)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
case CompareHash:
|
||||||
|
srcHash, err := calculateFileHash(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("원본 파일 해시를 계산할 수 없습니다: %w", err)
|
||||||
|
}
|
||||||
|
dstHash, err := calculateFileHash(dstPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("대상 파일 해시를 계산할 수 없습니다: %w", err)
|
||||||
|
}
|
||||||
|
if srcHash == dstHash {
|
||||||
|
progress.Status = "건너뜀 (해시 동일)"
|
||||||
|
opts.Progress(progress)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,16 +269,42 @@ func RunBackup(ctx context.Context, src, dst string, opts BackupOptions) error {
|
|||||||
return fmt.Errorf("파일을 복사할 수 없습니다: %w", err)
|
return fmt.Errorf("파일을 복사할 수 없습니다: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.Logger.Printf("복사됨: %s\n", relPath)
|
progress.Status = "복사됨"
|
||||||
|
if opts.Progress != nil {
|
||||||
|
backupF = true
|
||||||
|
opts.Progress(progress)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 증분 백업 메타데이터 저장
|
// 증분 백업 메타데이터 저장
|
||||||
if opts.Incremental && !opts.DryRun {
|
if opts.Incremental && !opts.DryRun {
|
||||||
meta.LastBackup = time.Now()
|
meta.LastBackup = time.Now()
|
||||||
if err := saveBackupMeta(dst, meta); err != nil {
|
if err := saveBackupMeta(src, meta); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !opts.DryRun {
|
||||||
|
entries, err := os.ReadDir(dst)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("디렉토리 읽기 실패: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entries) == 0 {
|
||||||
|
err := os.Remove(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
opts.Logger.Printf("백업된 내역이 없습니다.\n")
|
||||||
|
} else {
|
||||||
|
fmt.Println("백업이 성공적으로 완료되었습니다.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if backupF {
|
||||||
|
fmt.Println("백업이 성공적으로 완료되었습니다.")
|
||||||
|
} else {
|
||||||
|
opts.Logger.Printf("백업된 내역이 없습니다.\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user