files
simple_backup/cmd/main.go
2025-08-01 13:57:02 +09:00

261 lines
7.3 KiB
Go

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 /simple_backup --group-by day --compare hash
backup /home/user/docs /simple_backup/docs -i -v
backup /data /simple_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백업이 성공적으로 완료되었습니다")
}
}