기능개선

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

파일 보기

@@ -1,68 +1,260 @@
package main package main
import ( import (
"context"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"os/signal"
"syscall"
"time" "time"
"git.lhk.o-r.kr/freerer2/simple_backup/internal/backup" "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" "git.lhk.o-r.kr/freerer2/simple_backup/internal/path"
) )
func main() { type Options struct {
if len(os.Args) < 3 { Src string
fmt.Println("사용법: backup <원본경로> <백업경로> [옵션]") Dst string
os.Exit(1) 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("원본 경로와 백업 경로를 모두 지정해야 합니다")
} }
src := os.Args[1]
dst := os.Args[2]
fs := flag.NewFlagSet("backup", flag.ExitOnError) fs := flag.NewFlagSet("backup", flag.ExitOnError)
groupBy := fs.String("group-by", "", "백업 폴더 구조 기준: year, mon, day, hour, min, sec") fs.Usage = showHelp
snapshot := fs.String("snapshot", "", "스냅샷 기준: y, ym, ymd")
dryRun := fs.Bool("dry-run", false, "복사하지 않고 어떤 파일이 복사되는지 출력") opts := Options{
verbose := fs.Bool("verbose", false, "복사 로그 자세히 출력") Src: os.Args[1],
force := fs.Bool("force", false, "속성 무시하고 무조건 덮어쓰기") Dst: os.Args[2],
_ = fs.Parse(os.Args[3:]) 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() now := time.Now()
backupPath := dst backupPath := opts.Dst
if opts.GroupBy != "" {
if *groupBy != "" { backupPath = path.GetGroupByFolder(opts.Dst, opts.GroupBy, now)
backupPath = path.GetGroupByFolder(dst, *groupBy, now)
} }
if *snapshot != "" { log := logger.New(opts.Verbose)
var snapshotFolder string
switch *snapshot { // 진행 상황 콜백 함수
case "y": progressCallback := func(p backup.Progress) {
snapshotFolder = fmt.Sprintf("%d", now.Year()) if opts.Verbose {
case "ym": percentComplete := 0.0
snapshotFolder = fmt.Sprintf("%d%02d", now.Year(), int(now.Month())) if p.TotalFiles > 0 {
case "ymd": percentComplete = float64(p.ProcessedFiles) / float64(p.TotalFiles) * 100
snapshotFolder = fmt.Sprintf("%d%02d%02d", now.Year(), int(now.Month()), now.Day()) }
default: fmt.Printf("\r진행률: %.1f%% (%d/%d) - 현재: %s\n",
fmt.Printf("오류: 지원하지 않는 snapshot 옵션입니다: %s\n", *snapshot) 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) os.Exit(1)
} }
backupPath = fmt.Sprintf("%s/snapshots/%s", dst, snapshotFolder)
}
fmt.Println("원본 경로:", src) if opts.Verbose {
fmt.Println("최종 백업 경로:", backupPath) fmt.Println("\n백업이 성공적으로 완료되었습니다")
if *dryRun {
fmt.Println("Dry-run 모드 활성화됨")
}
if *verbose {
fmt.Println("Verbose 모드 활성화됨")
}
if *force {
fmt.Println("Force 모드 활성화됨")
}
if err := backup.RunBackup(src, backupPath, *dryRun, *verbose, *force); err != nil {
fmt.Println("백업 중 오류 발생:", err)
os.Exit(1)
} }
} }

파일 보기

@@ -1,61 +1,263 @@
package backup package backup
import ( import (
"context"
"crypto/md5"
"encoding/json"
"fmt" "fmt"
"git.lhk.o-r.kr/freerer2/simple_backup/internal/copy" "io"
"os" "os"
"path/filepath" "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: 백업 실행 함수 type CompareMode int
func RunBackup(src, dst string, dryRun, verbose, force bool) error {
if !dryRun { const (
// 백업 폴더 생성 CompareTime CompareMode = iota
if err := os.MkdirAll(dst, 0755); err != nil { CompareHash
return fmt.Errorf("백업 폴더 생성 실패: %w", err) )
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 { if err != nil {
return err return err
} }
relPath, err := filepath.Rel(src, path) // 디렉토리는 건너뜀
if err != nil {
return err
}
targetPath := filepath.Join(dst, relPath)
if info.IsDir() { if info.IsDir() {
if verbose {
fmt.Println("디렉터리 생성:", targetPath)
}
if !dryRun {
return os.MkdirAll(targetPath, info.Mode())
}
return nil return nil
} }
if dryRun { // 메타파일은 건너뜀
fmt.Println("복사 예정 파일:", targetPath) if filepath.Base(path) == constants.MetaFileName {
return nil return nil
} }
// force 옵션이 없으면 수정 시간 비교 files = append(files, path)
if !force {
dstInfo, err := os.Stat(targetPath)
if err == nil && !info.ModTime().After(dstInfo.ModTime()) {
if verbose {
fmt.Println("복사 스킵 (업데이트 필요 없음):", path)
}
return nil return nil
}
}
if verbose {
fmt.Println("파일 복사:", path, "→", targetPath)
}
return copy.RunCopy(path, targetPath)
}) })
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 package copy
import ( import (
"fmt"
"io" "io"
"os" "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 { 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) src, err := os.Open(srcFile)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to open source file: %w", err)
} }
defer func() {
defer func(src *os.File) { if cerr := src.Close(); cerr != nil && err == nil {
err := src.Close() err = fmt.Errorf("failed to close source file: %w", cerr)
if err != nil {
} }
}(src) }()
// 대상 파일 생성
dst, err := os.Create(dstFile) dst, err := os.Create(dstFile)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to create destination file: %w", err)
} }
defer func(dst *os.File) { defer func() {
err := dst.Close() if cerr := dst.Close(); cerr != nil && err == nil {
if err != nil { err = fmt.Errorf("failed to close destination file: %w", cerr)
} }
}(dst) }()
_, err = io.Copy(dst, src) // 버퍼 풀에서 가져오기
if err != nil { buf := bufferPool.Get().([]byte)
return err 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 := os.Chmod(dstFile, srcInfo.Mode()); err != nil {
if err == nil { return fmt.Errorf("failed to copy file permissions: %w", err)
err := os.Chmod(dstFile, info.Mode())
if err != nil {
return err
} }
// 수정 시간과 접근 시간 복사
if err := os.Chtimes(dstFile, srcInfo.ModTime(), srcInfo.ModTime()); err != nil {
return fmt.Errorf("failed to copy file timestamps: %w", err)
} }
return nil 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 ( import (
"fmt" "fmt"
"os"
"sync"
"time" "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 { func GetGroupByFolder(basePath, groupBy string, t time.Time) string {
switch groupBy { switch groupBy {
case "year": case constants.GroupByYear:
return fmt.Sprintf("%s/%d", basePath, t.Year()) return fmt.Sprintf("%s/%d", basePath, t.Year())
case "mon": case constants.GroupByMonth:
return fmt.Sprintf("%s/%d%02d", basePath, t.Year(), t.Month()) 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()) 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()) 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()) 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()) return fmt.Sprintf("%s/%d%02d%02d_%02d%02d%02d", basePath, t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
case "": case "":
return basePath return basePath
@@ -27,3 +40,39 @@ func GetGroupByFolder(basePath, groupBy string, t time.Time) string {
return basePath 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
}