package main import ( "context" "flag" "fmt" "os" "os/signal" "syscall" "time" "git.lhk.o-r.kr/freerer2/simple_backup/internal/backup" "git.lhk.o-r.kr/freerer2/simple_backup/internal/constants" "git.lhk.o-r.kr/freerer2/simple_backup/internal/logger" "git.lhk.o-r.kr/freerer2/simple_backup/internal/path" ) type Options struct { Src string Dst string GroupBy string CompareMode string Incremental bool DryRun bool Verbose bool Force bool } type ValidationError struct { Option string Value string Msg string } func (e *ValidationError) Error() string { return fmt.Sprintf("%s 옵션 오류: %s (%s)", e.Option, e.Msg, e.Value) } func showHelp() { helpText := `사용법: backup <원본경로> <백업경로> [옵션] 옵션: -g, --group-by 백업 폴더 구조 기준 (기본값: day) 가능한 값: year, mon, day, hour, min, sec -i, --incremental 증분 백업 사용 기존 백업과 비교하여 변경된 파일만 백업 -c, --compare 파일 비교 방식 선택 (기본값: time) - time: 파일 수정 시간으로 비교 - hash: 파일 내용의 해시값으로 비교 -d, --dry-run 하지 않고 어떤 파일이 복사되는지 출력 실제 파일 시스템을 변경하지 않음 -v, --verbose 복사 로그 자세히 출력 진행 상황과 세부 정보를 표시 -f, --force 속성 무시하고 무조건 덮어쓰기 기존 파일 존재 시 강제로 덮어씀 예시: backup /source /backup --group-by day --compare hash backup /home/user/docs /backup/docs -i -v backup /data /backup -d --force 자세한 정보: https://github.com/yourusername/backup` fmt.Println(helpText) } func validateOptions(opts Options) error { // 필수 경로 검증 if opts.Src == "" { return &ValidationError{Option: "src", Value: opts.Src, Msg: "원본 경로가 지정되지 않았습니다"} } if opts.Dst == "" { return &ValidationError{Option: "dst", Value: opts.Dst, Msg: "백업 경로가 지정되지 않았습니다"} } // 원본 경로 존재 확인 if _, err := os.Stat(opts.Src); os.IsNotExist(err) { return &ValidationError{Option: "src", Value: opts.Src, Msg: "원본 경로가 존재하지 않습니다"} } // group-by 옵션 검증 validGroupBy := map[string]bool{ "": true, constants.GroupByYear: true, constants.GroupByMonth: true, constants.GroupByDay: true, constants.GroupByHour: true, constants.GroupByMin: true, constants.GroupBySec: true, } if !validGroupBy[opts.GroupBy] { return &ValidationError{ Option: "group-by", Value: opts.GroupBy, Msg: "지원하지 않는 그룹화 옵션입니다. 가능한 값: year, mon, day, hour, min, sec", } } // compare 옵션 검증 validCompare := map[string]bool{ constants.CompareTime: true, constants.CompareHash: true, } if !validCompare[opts.CompareMode] { return &ValidationError{ Option: "compare", Value: opts.CompareMode, Msg: "지원하지 않는 비교 모드입니다. 가능한 값: time, hash", } } // 증분 백업 + force 모드 조합 경고 if opts.Incremental && opts.Force { fmt.Println("경고: force 모드와 증분 백업을 함께 사용하면 증분 백업의 이점이 사라질 수 있습니다") } return nil } func parseFlags() (Options, error) { if len(os.Args) < 2 || (len(os.Args) == 2 && (os.Args[1] == "-h" || os.Args[1] == "--help")) { showHelp() os.Exit(0) } if len(os.Args) < 3 { return Options{}, fmt.Errorf("원본 경로와 백업 경로를 모두 지정해야 합니다") } fs := flag.NewFlagSet("backup", flag.ExitOnError) fs.Usage = showHelp opts := Options{ Src: os.Args[1], Dst: os.Args[2], GroupBy: constants.DefaultGroupBy, CompareMode: constants.DefaultCompare, } // 플래그 정의 fs.StringVar(&opts.GroupBy, "group-by", constants.DefaultGroupBy, "") fs.StringVar(&opts.GroupBy, "g", constants.DefaultGroupBy, "") fs.StringVar(&opts.CompareMode, "compare", constants.DefaultCompare, "") fs.StringVar(&opts.CompareMode, "c", constants.DefaultCompare, "") fs.BoolVar(&opts.Incremental, "incremental", false, "") fs.BoolVar(&opts.Incremental, "i", false, "") fs.BoolVar(&opts.DryRun, "dry-run", false, "") fs.BoolVar(&opts.DryRun, "d", false, "") fs.BoolVar(&opts.Verbose, "verbose", false, "") fs.BoolVar(&opts.Verbose, "v", false, "") fs.BoolVar(&opts.Force, "force", false, "") fs.BoolVar(&opts.Force, "f", false, "") if err := fs.Parse(os.Args[3:]); err != nil { return opts, err } return opts, nil } func printOptions(opts Options, backupPath string) { fmt.Println("\n실행 설정:") fmt.Println("----------------------------------------") fmt.Printf("원본 경로: %s\n", opts.Src) fmt.Printf("최종 백업 경로: %s\n", backupPath) fmt.Printf("그룹화 방식: %s\n", opts.GroupBy) fmt.Printf("비교 모드: %s\n", opts.CompareMode) fmt.Printf("증분 백업: %v\n", opts.Incremental) fmt.Printf("드라이런 모드: %v\n", opts.DryRun) fmt.Printf("상세 로그: %v\n", opts.Verbose) fmt.Printf("강제 덮어쓰기: %v\n", opts.Force) fmt.Println("----------------------------------------\n") } func main() { opts, err := parseFlags() if err != nil { fmt.Println("오류:", err) showHelp() os.Exit(1) } if err := validateOptions(opts); err != nil { fmt.Println("오류:", err) fmt.Println("\n사용법을 확인하려면 --help를 사용하세요") os.Exit(1) } // 컨텍스트 설정 ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() // 시그널 처리 sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) go func() { <-sigCh fmt.Println("\n백업 취소 요청됨...") cancel() }() now := time.Now() backupPath := opts.Dst if opts.GroupBy != "" { backupPath = path.GetGroupByFolder(opts.Dst, opts.GroupBy, now) } log := logger.New(opts.Verbose) // 진행 상황 콜백 함수 progressCallback := func(p backup.Progress) { if opts.Verbose { percentComplete := 0.0 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) } } mode := backup.CompareTime if opts.CompareMode == constants.CompareHash { mode = backup.CompareHash } backupOpts := backup.BackupOptions{ DryRun: opts.DryRun, Verbose: opts.Verbose, Force: opts.Force, Incremental: opts.Incremental, CompareMode: mode, Logger: log, Progress: progressCallback, } // main 함수에서 해당 부분을 다음과 같이 교체: if opts.Verbose { printOptions(opts, backupPath) } if err := backup.RunBackup(ctx, opts.Src, backupPath, backupOpts); err != nil { if err == context.Canceled { fmt.Println("\n백업이 취소되었습니다") } else { fmt.Println("\n백업 중 오류 발생:", err) } os.Exit(1) } if opts.Verbose { fmt.Println("\n백업이 성공적으로 완료되었습니다") } }