기능개선

This commit is contained in:
LHK
2025-08-01 10:48:56 +09:00
부모 f17514086d
커밋 2a7b3c5468
6개의 변경된 파일637개의 추가작업 그리고 112개의 파일을 삭제

파일 보기

@@ -1,61 +1,263 @@
package backup
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"git.lhk.o-r.kr/freerer2/simple_backup/internal/copy"
"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"
)
// RunBackup: 백업 실행 함수
func RunBackup(src, dst string, dryRun, verbose, force bool) error {
if !dryRun {
// 백업 폴더 생성
if err := os.MkdirAll(dst, 0755); err != nil {
return fmt.Errorf("백업 폴더 생성 실패: %w", err)
type CompareMode int
const (
CompareTime CompareMode = iota
CompareHash
)
type Progress struct {
CurrentFile string
ProcessedFiles int
TotalFiles int
}
type ProgressCallback func(Progress)
type BackupOptions struct {
DryRun bool
Verbose bool
Force bool
Incremental bool
CompareMode CompareMode
Logger *logger.Logger
Progress ProgressCallback
dirCache *path.DirCache
}
type BackupMeta struct {
LastBackup time.Time `json:"lastBackup"`
Files map[string]string `json:"files"` // 파일 경로 -> 해시 또는 수정 시간
}
func loadBackupMeta(dst string) (*BackupMeta, error) {
metaPath := filepath.Join(dst, constants.MetaFileName)
data, err := os.ReadFile(metaPath)
if err != nil {
if os.IsNotExist(err) {
return &BackupMeta{
Files: make(map[string]string),
}, nil
}
return nil, fmt.Errorf("메타 파일을 읽을 수 없습니다: %w", err)
}
var meta BackupMeta
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(dst string, meta *BackupMeta) error {
metaPath := filepath.Join(dst, 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 BackupOptions) 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)
}
}
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
// 증분 백업을 위한 메타데이터 로드
var meta *BackupMeta
var err error
if opts.Incremental {
meta, err = loadBackupMeta(dst)
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
}
relPath, err := filepath.Rel(src, path)
if err != nil {
return err
}
targetPath := filepath.Join(dst, relPath)
// 디렉토리는 건너뜀
if info.IsDir() {
if verbose {
fmt.Println("디렉터리 생성:", targetPath)
}
if !dryRun {
return os.MkdirAll(targetPath, info.Mode())
}
return nil
}
if dryRun {
fmt.Println("복사 예정 파일:", targetPath)
// 메타파일은 건너뜀
if filepath.Base(path) == constants.MetaFileName {
return nil
}
// force 옵션이 없으면 수정 시간 비교
if !force {
dstInfo, err := os.Stat(targetPath)
if err == nil && !info.ModTime().After(dstInfo.ModTime()) {
if verbose {
fmt.Println("복사 스킵 (업데이트 필요 없음):", path)
}
return nil
}
}
if verbose {
fmt.Println("파일 복사:", path, "→", targetPath)
}
return copy.RunCopy(path, targetPath)
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 fileExists && !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 {
// 일반 모드에서는 크기와 수정 시간만 비교
srcInfo, err := os.Stat(srcPath)
if err != nil {
return fmt.Errorf("원본 파일 정보를 읽을 수 없습니다: %w", err)
}
if srcInfo.Size() == dstInfo.Size() && srcInfo.ModTime().Equal(dstInfo.ModTime()) {
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(dst, meta); err != nil {
return err
}
}
return nil
}

파일 보기

@@ -0,0 +1,28 @@
package constants
// 그룹화 옵션
const (
GroupByYear = "year"
GroupByMonth = "mon"
GroupByDay = "day"
GroupByHour = "hour"
GroupByMin = "min"
GroupBySec = "sec"
)
// 비교 모드 옵션
const (
CompareTime = "time"
CompareHash = "hash"
)
// 버퍼 크기
const (
CopyBufferSize = 1024 * 1024 // 1MB
)
const (
DefaultGroupBy = "day"
DefaultCompare = "time"
MetaFileName = ".backup_meta.json"
DefaultFileMode = 0755
)

파일 보기

@@ -1,48 +1,70 @@
package copy
import (
"fmt"
"io"
"os"
"sync"
"git.lhk.o-r.kr/freerer2/simple_backup/internal/constants"
)
// CopyFile 복사 함수: srcFile → dstFile
var bufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, constants.CopyBufferSize)
},
}
func RunCopy(srcFile, dstFile string) error {
// 소스 파일 정보 확인
srcInfo, err := os.Stat(srcFile)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("source file does not exist: %s", srcFile)
}
return fmt.Errorf("failed to get source file info: %w", err)
}
// 소스 파일 열기
src, err := os.Open(srcFile)
if err != nil {
return err
return fmt.Errorf("failed to open source file: %w", err)
}
defer func(src *os.File) {
err := src.Close()
if err != nil {
defer func() {
if cerr := src.Close(); cerr != nil && err == nil {
err = fmt.Errorf("failed to close source file: %w", cerr)
}
}(src)
}()
// 대상 파일 생성
dst, err := os.Create(dstFile)
if err != nil {
return err
return fmt.Errorf("failed to create destination file: %w", err)
}
defer func(dst *os.File) {
err := dst.Close()
if err != nil {
defer func() {
if cerr := dst.Close(); cerr != nil && err == nil {
err = fmt.Errorf("failed to close destination file: %w", cerr)
}
}(dst)
}()
_, err = io.Copy(dst, src)
if err != nil {
return err
// 버퍼 풀에서 가져오기
buf := bufferPool.Get().([]byte)
defer bufferPool.Put(buf)
// 파일 내용 복사
if _, err = io.CopyBuffer(dst, src, buf); err != nil {
return fmt.Errorf("failed to copy file contents: %w", err)
}
// 원본 파일 권한 복사
info, err := os.Stat(srcFile)
if err == nil {
err := os.Chmod(dstFile, info.Mode())
if err != nil {
return err
}
// 파일 권한과 시간 정보 복사
if err := os.Chmod(dstFile, srcInfo.Mode()); err != nil {
return fmt.Errorf("failed to copy file permissions: %w", err)
}
// 수정 시간과 접근 시간 복사
if err := os.Chtimes(dstFile, srcInfo.ModTime(), srcInfo.ModTime()); err != nil {
return fmt.Errorf("failed to copy file timestamps: %w", err)
}
return nil

32
internal/logger/logger.go Normal file
파일 보기

@@ -0,0 +1,32 @@
package logger
import (
"bufio"
"fmt"
"os"
"sync"
)
type Logger struct {
verbose bool
writer *bufio.Writer
mu sync.Mutex
}
func New(verbose bool) *Logger {
return &Logger{
verbose: verbose,
writer: bufio.NewWriter(os.Stdout),
}
}
func (l *Logger) Printf(format string, args ...interface{}) {
if !l.verbose {
return
}
l.mu.Lock()
_, _ = fmt.Fprintf(l.writer, format, args...)
_ = l.writer.Flush()
l.mu.Unlock()
}

파일 보기

@@ -2,23 +2,36 @@ package path
import (
"fmt"
"os"
"sync"
"time"
"git.lhk.o-r.kr/freerer2/simple_backup/internal/constants"
)
// GetGroupByFolder: group-by 옵션에 따라 백업 경로 생성
// GetGroupByFolder generates a backup path based on the groupBy option and time.
// If groupBy is invalid or empty, it returns the basePath unchanged.
//
// Parameters:
// - basePath: the base directory path for backup
// - groupBy: grouping option (year, mon, day, hour, min, sec)
// - t: time to use for path generation
//
// Returns:
// - string: generated path based on groupBy option
func GetGroupByFolder(basePath, groupBy string, t time.Time) string {
switch groupBy {
case "year":
case constants.GroupByYear:
return fmt.Sprintf("%s/%d", basePath, t.Year())
case "mon":
case constants.GroupByMonth:
return fmt.Sprintf("%s/%d%02d", basePath, t.Year(), t.Month())
case "day":
case constants.GroupByDay:
return fmt.Sprintf("%s/%d%02d%02d", basePath, t.Year(), t.Month(), t.Day())
case "hour":
case constants.GroupByHour:
return fmt.Sprintf("%s/%d%02d%02d_%02d", basePath, t.Year(), t.Month(), t.Day(), t.Hour())
case "min":
case constants.GroupByMin:
return fmt.Sprintf("%s/%d%02d%02d_%02d%02d", basePath, t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute())
case "sec":
case constants.GroupBySec:
return fmt.Sprintf("%s/%d%02d%02d_%02d%02d%02d", basePath, t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
case "":
return basePath
@@ -27,3 +40,39 @@ func GetGroupByFolder(basePath, groupBy string, t time.Time) string {
return basePath
}
}
// 디렉토리 캐시
type DirCache struct {
sync.RWMutex
exists map[string]bool
}
func NewDirCache() *DirCache {
return &DirCache{
exists: make(map[string]bool),
}
}
func (c *DirCache) EnsureDir(path string) error {
c.RLock()
if c.exists[path] {
c.RUnlock()
return nil
}
c.RUnlock()
c.Lock()
defer c.Unlock()
// 다시 확인 (다른 고루틴이 생성했을 수 있음)
if c.exists[path] {
return nil
}
if err := os.MkdirAll(path, constants.DefaultFileMode); err != nil {
return err
}
c.exists[path] = true
return nil
}