Compare commits

12 커밋

작성자 SHA1 메시지 날짜
6c7f6ca46d Update README.md 2025-08-02 15:28:59 +00:00
LHK
f4c92b18f7 빌드 스크립트 추가 2025-08-01 16:26:49 +09:00
LHK
b63de8bc4e 빌드 스크립트 추가 2025-08-01 16:14:16 +09:00
LHK
d415ed4525 자잘한 내용 수정 2025-08-01 16:06:15 +09:00
LHK
12b61d4c48 자잘한 내용 수 2025-08-01 16:04:49 +09:00
LHK
7ba60e8561 백업 내용 없을 경우 폴더 삭제 2025-08-01 15:24:44 +09:00
LHK
05f582df2a 로그 기능 수정 2025-08-01 15:09:33 +09:00
LHK
bfe006338a 1. 증분 백업 버그 수정
2. 일반 백업 해쉬 비교 추가
2025-08-01 14:54:47 +09:00
LHK
5a0ca30cf7 1. 증분 백업 버그 수정
2. 일반 백업 해쉬 비교 추가
2025-08-01 14:35:09 +09:00
LHK
e08097a067 소스정리 2025-08-01 13:58:47 +09:00
LHK
4c6828a311 소스정 2025-08-01 13:57:57 +09:00
LHK
1cd39aff5b 소스정 2025-08-01 13:57:31 +09:00
8개의 변경된 파일128개의 추가작업 그리고 72개의 파일을 삭제

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
파일 보기

@@ -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>

파일 보기

@@ -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
파일 보기

@@ -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>

파일 보기

@@ -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
파일 보기

@@ -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

파일 보기

@@ -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
}