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 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 { 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: } // 진행 상황 업데이트 progress.CurrentFile = srcPath progress.ProcessedFiles = i + 1 if opts.Progress != nil { opts.Progress(progress) } // 상대 경로 계산 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 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 { opts.Logger.Printf("건너뜀 (시간 동일): %s\n", relPath) continue } case CompareHash: newValue, err = calculateFileHash(srcPath) if err != nil { return fmt.Errorf("파일 해시를 계산할 수 없습니다: %w", err) } if exists && oldValue == newValue { opts.Logger.Printf("건너뜀 (해시 동일): %s\n", relPath) 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()) { opts.Logger.Printf("건너뜀 (시간 동일): %s\n", relPath) 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 { opts.Logger.Printf("건너뜀 (해시 동일): %s\n", relPath) continue } } } } } // 파일 복사 if err := copyFile(srcPath, dstPath); err != nil { return fmt.Errorf("파일을 복사할 수 없습니다: %w", err) } opts.Logger.Printf("복사됨: %s\n", relPath) } // 증분 백업 메타데이터 저장 if opts.Incremental && !opts.DryRun { meta.LastBackup = time.Now() if err := saveBackupMeta(src, meta); err != nil { return err } } return nil }