262 lines
7.4 KiB
Go
262 lines
7.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"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 증분 백업 사용
|
|
기존 백업과 비교하여 변경된 파일만 백업
|
|
(.backup_meta.json을 생성하여 이용함.)
|
|
|
|
-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://git.lhk.o-r.kr/simple-utils/simple_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: %s\n",
|
|
percentComplete, p.ProcessedFiles, p.TotalFiles, p.Status, p.CurrentFile)
|
|
}
|
|
}
|
|
|
|
mode := backup.CompareTime
|
|
if opts.CompareMode == constants.CompareHash {
|
|
mode = backup.CompareHash
|
|
}
|
|
|
|
backupOpts := backup.Options{
|
|
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 errors.Is(err, context.Canceled) {
|
|
fmt.Println("\n백업이 취소되었습니다")
|
|
} else {
|
|
fmt.Println("\n백업 중 오류 발생:", err)
|
|
}
|
|
if err := os.RemoveAll(backupPath); err != nil {
|
|
fmt.Printf("백업 폴더 삭제 실패: %v\n", err)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
}
|