281 lines
6.5 KiB
Go
281 lines
6.5 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
|
|
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
|
|
}
|