files
2025-08-01 16:04:49 +09:00

311 lines
7.1 KiB
Go

package backup
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"time"
"git.lhk.o-r.kr/freerer2/simple_backup/internal/constants"
"git.lhk.o-r.kr/freerer2/simple_backup/internal/copy"
"git.lhk.o-r.kr/freerer2/simple_backup/internal/logger"
"git.lhk.o-r.kr/freerer2/simple_backup/internal/path"
)
type CompareMode int
const (
CompareTime CompareMode = iota
CompareHash
)
type Progress struct {
CurrentFile string
Status string
ProcessedFiles int
TotalFiles int
}
type ProgressCallback func(Progress)
type Options struct {
DryRun bool
Verbose bool
Force bool
Incremental bool
CompareMode CompareMode
Logger *logger.Logger
Progress ProgressCallback
dirCache *path.DirCache
}
type Meta struct {
LastBackup time.Time `json:"lastBackup"`
Files map[string]string `json:"files"` // 파일 경로 -> 해시 또는 수정 시간
}
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 &Meta{
Files: make(map[string]string),
}, nil
}
return nil, fmt.Errorf("메타 파일을 읽을 수 없습니다: %w", err)
}
var meta Meta
if err := json.Unmarshal(data, &meta); err != nil {
return nil, fmt.Errorf("메타 파일을 파싱할 수 없습니다: %w", err)
}
if meta.Files == nil {
meta.Files = make(map[string]string)
}
return &meta, nil
}
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)
}
if err := os.WriteFile(metaPath, data, constants.DefaultFileMode); err != nil {
return fmt.Errorf("메타 파일을 저장할 수 없습니다: %w", err)
}
return nil
}
func calculateFileHash(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer func(f *os.File) {
err := f.Close()
if err != nil {
}
}(f)
h := md5.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func RunBackup(ctx context.Context, src, dst string, opts Options) error {
var backupF = false
if opts.dirCache == nil {
opts.dirCache = path.NewDirCache()
}
// 대상 디렉토리 생성
if !opts.DryRun {
// 대상 디렉토리 생성
if err := opts.dirCache.EnsureDir(dst); err != nil {
return fmt.Errorf("대상 디렉토리를 생성할 수 없습니다: %w", err)
}
}
// 증분 백업을 위한 메타데이터 로드
var meta *Meta
var err error
if opts.Incremental {
meta, err = loadBackupMeta(src)
if err != nil {
return err
}
}
// 파일 목록 수집
var files []string
err = filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 디렉토리는 건너뜀
if info.IsDir() {
return nil
}
// 메타파일은 건너뜀
if filepath.Base(path) == constants.MetaFileName {
return nil
}
files = append(files, path)
return nil
})
if err != nil {
return fmt.Errorf("파일 목록을 수집할 수 없습니다: %w", err)
}
// 진행 상황 초기화
progress := Progress{
TotalFiles: len(files),
}
// 파일 복사 함수
copyFile := func(srcPath string, dstPath string) error {
if opts.DryRun {
return nil
}
// 대상 디렉토리 생성
dstDir := filepath.Dir(dstPath)
if err := opts.dirCache.EnsureDir(dstDir); err != nil {
return fmt.Errorf("대상 디렉토리를 생성할 수 없습니다: %w", err)
}
return copy.RunCopy(srcPath, dstPath)
}
// 각 파일 처리
for i, srcPath := range files {
select {
case <-ctx.Done():
return context.Canceled
default:
}
// 상대 경로 계산
relPath, err := filepath.Rel(src, srcPath)
if err != nil {
return fmt.Errorf("상대 경로를 계산할 수 없습니다: %w", err)
}
dstPath := filepath.Join(dst, relPath)
// 파일이 이미 존재하는지 확인
dstInfo, err := os.Stat(dstPath)
fileExists := err == nil
// 진행 상황 업데이트
progress.CurrentFile = srcPath
progress.ProcessedFiles = i + 1
if !opts.Force {
if opts.Incremental {
// 증분 백업 모드에서는 메타데이터를 확인
srcInfo, err := os.Stat(srcPath)
if err != nil {
return fmt.Errorf("원본 파일 정보를 읽을 수 없습니다: %w", err)
}
oldValue, exists := meta.Files[relPath]
var newValue string
switch opts.CompareMode {
case CompareTime:
newValue = srcInfo.ModTime().String()
if exists && oldValue == newValue {
progress.Status = "건너뜀 (시간 동일)"
opts.Progress(progress)
continue
}
case CompareHash:
newValue, err = calculateFileHash(srcPath)
if err != nil {
return fmt.Errorf("파일 해시를 계산할 수 없습니다: %w", err)
}
if exists && oldValue == newValue {
progress.Status = "건너뜀 (해시 동일)"
opts.Progress(progress)
continue
}
}
meta.Files[relPath] = newValue
} else {
if fileExists {
// 일반 모드에서는 크기와 수정 시간만 비교
srcInfo, err := os.Stat(srcPath)
if err != nil {
return fmt.Errorf("원본 파일 정보를 읽을 수 없습니다: %w", err)
}
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
}
}
}
}
}
// 파일 복사
if err := copyFile(srcPath, dstPath); err != nil {
return fmt.Errorf("파일을 복사할 수 없습니다: %w", err)
}
progress.Status = "복사됨"
if opts.Progress != nil {
backupF = true
opts.Progress(progress)
}
}
// 증분 백업 메타데이터 저장
if opts.Incremental && !opts.DryRun {
meta.LastBackup = time.Now()
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
}