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
|
||||
## 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -45,12 +46,13 @@ func showHelp() {
|
||||
|
||||
-i, --incremental 증분 백업 사용
|
||||
기존 백업과 비교하여 변경된 파일만 백업
|
||||
(.backup_meta.json을 생성하여 이용함.)
|
||||
|
||||
-c, --compare 파일 비교 방식 선택 (기본값: time)
|
||||
- time: 파일 수정 시간으로 비교
|
||||
- hash: 파일 내용의 해시값으로 비교
|
||||
|
||||
-d, --dry-run 하지 않고 어떤 파일이 복사되는지 출력
|
||||
-d, --dry-run 실하지 않고 어떤 파일이 복사되는지 출력
|
||||
실제 파일 시스템을 변경하지 않음
|
||||
|
||||
-v, --verbose 복사 로그 자세히 출력
|
||||
@@ -64,7 +66,7 @@ func showHelp() {
|
||||
backup /home/user/docs /simple_backup/docs -i -v
|
||||
backup /data /simple_backup -d --force
|
||||
|
||||
자세한 정보: https://github.com/yourusername/backup`
|
||||
자세한 정보: https://git.lhk.o-r.kr/simple-utils/simple_backup`
|
||||
|
||||
fmt.Println(helpText)
|
||||
}
|
||||
@@ -220,8 +222,8 @@ func main() {
|
||||
if p.TotalFiles > 0 {
|
||||
percentComplete = float64(p.ProcessedFiles) / float64(p.TotalFiles) * 100
|
||||
}
|
||||
fmt.Printf("\r진행률: %.1f%% (%d/%d) - 현재: %s\n",
|
||||
percentComplete, p.ProcessedFiles, p.TotalFiles, p.CurrentFile)
|
||||
fmt.Printf("\r진행률: %.1f%% (%d/%d) - %s: %s\n",
|
||||
percentComplete, p.ProcessedFiles, p.TotalFiles, p.Status, p.CurrentFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +232,7 @@ func main() {
|
||||
mode = backup.CompareHash
|
||||
}
|
||||
|
||||
backupOpts := backup.BackupOptions{
|
||||
backupOpts := backup.Options{
|
||||
DryRun: opts.DryRun,
|
||||
Verbose: opts.Verbose,
|
||||
Force: opts.Force,
|
||||
@@ -246,15 +248,11 @@ func main() {
|
||||
}
|
||||
|
||||
if err := backup.RunBackup(ctx, opts.Src, backupPath, backupOpts); err != nil {
|
||||
if err == context.Canceled {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
fmt.Println("\n백업이 취소되었습니다")
|
||||
} else {
|
||||
fmt.Println("\n백업 중 오류 발생:", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if opts.Verbose {
|
||||
fmt.Println("\n백업이 성공적으로 완료되었습니다")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,14 @@ const (
|
||||
|
||||
type Progress struct {
|
||||
CurrentFile string
|
||||
Status string
|
||||
ProcessedFiles int
|
||||
TotalFiles int
|
||||
}
|
||||
|
||||
type ProgressCallback func(Progress)
|
||||
|
||||
type BackupOptions struct {
|
||||
type Options struct {
|
||||
DryRun bool
|
||||
Verbose bool
|
||||
Force bool
|
||||
@@ -42,24 +43,24 @@ type BackupOptions struct {
|
||||
dirCache *path.DirCache
|
||||
}
|
||||
|
||||
type BackupMeta struct {
|
||||
type Meta struct {
|
||||
LastBackup time.Time `json:"lastBackup"`
|
||||
Files map[string]string `json:"files"` // 파일 경로 -> 해시 또는 수정 시간
|
||||
}
|
||||
|
||||
func loadBackupMeta(dst string) (*BackupMeta, error) {
|
||||
metaPath := filepath.Join(dst, constants.MetaFileName)
|
||||
func loadBackupMeta(src string) (*Meta, error) {
|
||||
metaPath := filepath.Join(src, constants.MetaFileName)
|
||||
data, err := os.ReadFile(metaPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &BackupMeta{
|
||||
return &Meta{
|
||||
Files: make(map[string]string),
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("메타 파일을 읽을 수 없습니다: %w", err)
|
||||
}
|
||||
|
||||
var meta BackupMeta
|
||||
var meta Meta
|
||||
if err := json.Unmarshal(data, &meta); err != nil {
|
||||
return nil, fmt.Errorf("메타 파일을 파싱할 수 없습니다: %w", err)
|
||||
}
|
||||
@@ -70,8 +71,8 @@ func loadBackupMeta(dst string) (*BackupMeta, error) {
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
func saveBackupMeta(dst string, meta *BackupMeta) error {
|
||||
metaPath := filepath.Join(dst, constants.MetaFileName)
|
||||
func saveBackupMeta(src string, meta *Meta) error {
|
||||
metaPath := filepath.Join(src, constants.MetaFileName)
|
||||
data, err := json.MarshalIndent(meta, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("메타 데이터를 직렬화할 수 없습니다: %w", err)
|
||||
@@ -104,7 +105,8 @@ func calculateFileHash(path string) (string, error) {
|
||||
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 {
|
||||
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
|
||||
if opts.Incremental {
|
||||
meta, err = loadBackupMeta(dst)
|
||||
meta, err = loadBackupMeta(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -180,13 +182,6 @@ func RunBackup(ctx context.Context, src, dst string, opts BackupOptions) error {
|
||||
default:
|
||||
}
|
||||
|
||||
// 진행 상황 업데이트
|
||||
progress.CurrentFile = srcPath
|
||||
progress.ProcessedFiles = i + 1
|
||||
if opts.Progress != nil {
|
||||
opts.Progress(progress)
|
||||
}
|
||||
|
||||
// 상대 경로 계산
|
||||
relPath, err := filepath.Rel(src, srcPath)
|
||||
if err != nil {
|
||||
@@ -199,7 +194,11 @@ func RunBackup(ctx context.Context, src, dst string, opts BackupOptions) error {
|
||||
dstInfo, err := os.Stat(dstPath)
|
||||
fileExists := err == nil
|
||||
|
||||
if fileExists && !opts.Force {
|
||||
// 진행 상황 업데이트
|
||||
progress.CurrentFile = srcPath
|
||||
progress.ProcessedFiles = i + 1
|
||||
|
||||
if !opts.Force {
|
||||
if opts.Incremental {
|
||||
// 증분 백업 모드에서는 메타데이터를 확인
|
||||
srcInfo, err := os.Stat(srcPath)
|
||||
@@ -214,7 +213,8 @@ func RunBackup(ctx context.Context, src, dst string, opts BackupOptions) error {
|
||||
case CompareTime:
|
||||
newValue = srcInfo.ModTime().String()
|
||||
if exists && oldValue == newValue {
|
||||
opts.Logger.Printf("건너뜀 (시간 동일): %s\n", relPath)
|
||||
progress.Status = "건너뜀 (시간 동일)"
|
||||
opts.Progress(progress)
|
||||
continue
|
||||
}
|
||||
case CompareHash:
|
||||
@@ -223,22 +223,43 @@ func RunBackup(ctx context.Context, src, dst string, opts BackupOptions) error {
|
||||
return fmt.Errorf("파일 해시를 계산할 수 없습니다: %w", err)
|
||||
}
|
||||
if exists && oldValue == newValue {
|
||||
opts.Logger.Printf("건너뜀 (해시 동일): %s\n", relPath)
|
||||
progress.Status = "건너뜀 (해시 동일)"
|
||||
opts.Progress(progress)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
meta.Files[relPath] = newValue
|
||||
} else {
|
||||
// 일반 모드에서는 크기와 수정 시간만 비교
|
||||
srcInfo, err := os.Stat(srcPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("원본 파일 정보를 읽을 수 없습니다: %w", err)
|
||||
}
|
||||
if fileExists {
|
||||
// 일반 모드에서는 크기와 수정 시간만 비교
|
||||
srcInfo, err := os.Stat(srcPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("원본 파일 정보를 읽을 수 없습니다: %w", err)
|
||||
}
|
||||
|
||||
if srcInfo.Size() == dstInfo.Size() && srcInfo.ModTime().Equal(dstInfo.ModTime()) {
|
||||
opts.Logger.Printf("건너뜀 (동일): %s\n", relPath)
|
||||
continue
|
||||
switch opts.CompareMode {
|
||||
case CompareTime:
|
||||
if srcInfo.Size() == dstInfo.Size() && srcInfo.ModTime().Equal(dstInfo.ModTime()) {
|
||||
progress.Status = "건너뜀 (시간 동일)"
|
||||
opts.Progress(progress)
|
||||
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)
|
||||
}
|
||||
|
||||
opts.Logger.Printf("복사됨: %s\n", relPath)
|
||||
progress.Status = "복사됨"
|
||||
if opts.Progress != nil {
|
||||
backupF = true
|
||||
opts.Progress(progress)
|
||||
}
|
||||
}
|
||||
|
||||
// 증분 백업 메타데이터 저장
|
||||
if opts.Incremental && !opts.DryRun {
|
||||
meta.LastBackup = time.Now()
|
||||
if err := saveBackupMeta(dst, meta); err != nil {
|
||||
if err := saveBackupMeta(src, meta); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user