diff --git a/cmd/create.go b/cmd/create.go index c7fe13f..453ed3e 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -24,8 +24,9 @@ import ( ) var ( - source string - target string + source string + target string + excludePatterns []string ) var createCmd = &cobra.Command{ @@ -33,7 +34,7 @@ var createCmd = &cobra.Command{ Short: "Create a new backup", Long: "Create a new backup of a file, directory, or disk", Run: func(cmd *cobra.Command, args []string) { - model, err := initialModel(source, target) + model, err := initialModel(source, target, excludePatterns) if err != nil { fmt.Println("Error:", err) os.Exit(1) @@ -48,25 +49,26 @@ var createCmd = &cobra.Command{ } type model struct { - progress progress.Model - source string - target string - totalBytes int64 - processed int64 - done bool - err error - startTime time.Time - progressCh chan int64 - isDirectory bool - httpTargetInfo *ServerTargetInfo - isP2PTarget bool // Флаг для старого P2P-режима + progress progress.Model + source string + target string + totalBytes int64 + processed int64 + done bool + err error + startTime time.Time + progressCh chan int64 + isDirectory bool + httpTargetInfo *ServerTargetInfo + isP2PTarget bool // Флаг для старого P2P-режима + excludePatterns []string } type progressMsg int64 type doneMsg struct{} type errorMsg struct{ err error } -func getPathSize(path string) (int64, error) { +func getPathSize(path string, excludePatterns []string) (int64, error) { fileInfo, err := os.Stat(path) if err != nil { return 0, err @@ -97,10 +99,27 @@ func getPathSize(path string) (int64, error) { if fileInfo.IsDir() { var total int64 - err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { + err := filepath.Walk(path, func(current string, info os.FileInfo, err error) error { if err != nil { return err } + + relPath, err := filepath.Rel(path, current) + if err != nil { + return err + } + + if relPath == "." { + return nil + } + + if shouldExcludePath(relPath, excludePatterns) { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + if !info.IsDir() { total += info.Size() } @@ -112,8 +131,8 @@ func getPathSize(path string) (int64, error) { return fileInfo.Size(), nil } -func initialModel(src, dst string) (*model, error) { - totalBytes, err := getPathSize(src) +func initialModel(src, dst string, excludePatterns []string) (*model, error) { + totalBytes, err := getPathSize(src, excludePatterns) if err != nil { return nil, err } @@ -159,15 +178,16 @@ func initialModel(src, dst string) (*model, error) { } return &model{ - progress: p, - source: src, - target: targetPath, - totalBytes: totalBytes, - startTime: time.Now(), - progressCh: make(chan int64, 100), - isDirectory: isDirectory, - httpTargetInfo: targetInfo, - isP2PTarget: isP2P, + progress: p, + source: src, + target: targetPath, + totalBytes: totalBytes, + startTime: time.Now(), + progressCh: make(chan int64, 100), + isDirectory: isDirectory, + httpTargetInfo: targetInfo, + isP2PTarget: isP2P, + excludePatterns: excludePatterns, }, nil } @@ -278,6 +298,38 @@ func (m *model) backupFileOrDisk(w io.Writer) error { return err } +// Вспомогательная функция для проверки исключений +func shouldExcludePath(path string, patterns []string) bool { + cleanPath := filepath.Clean(path) + + for _, pattern := range patterns { + // Проверка полного совпадения + if cleanPath == pattern { + return true + } + + // Проверка префикса (исключение вложенных путей) + if strings.HasPrefix(cleanPath, pattern+string(filepath.Separator)) { + return true + } + + // Проверка glob-паттернов + if match, _ := filepath.Match(pattern, cleanPath); match { + return true + } + + // Проверка на соответствие базовому имени + if match, _ := filepath.Match(pattern, filepath.Base(cleanPath)); match { + return true + } + } + return false +} + +func (m *model) shouldExclude(path string) bool { + return shouldExcludePath(path, m.excludePatterns) +} + func (m *model) backupDirectory(w io.Writer) error { progressWriter := &progressTracker{Writer: w, progressCh: m.progressCh} gzipWriter := gzip.NewWriter(progressWriter) @@ -291,12 +343,21 @@ func (m *model) backupDirectory(w io.Writer) error { return err } - header, err := tar.FileInfoHeader(fi, file) + // Получаем относительный путь + relPath, err := filepath.Rel(m.source, file) if err != nil { return err } - relPath, err := filepath.Rel(m.source, file) + if relPath != "." { + if m.shouldExclude(relPath) { + if fi.IsDir() { + return filepath.SkipDir + } + return nil // Пропускаем файл + } + } + header, err := tar.FileInfoHeader(fi, file) if err != nil { return err } @@ -416,8 +477,27 @@ func (m *model) View() string { func init() { rootCmd.AddCommand(createCmd) - createCmd.Flags().StringVarP(&source, "source", "s", "", "Source file, directory or disk (e.g., /dev/sda) (required)") - createCmd.Flags().StringVarP(&target, "target", "t", "", "Target backup file or network address (e.g., /path/to/backup.tar.gz, 127.0.0.1:8080/backup.gz, or 127.0.0.1:8080) (required)") + createCmd.Flags().StringVarP( + &source, + "source", + "s", + "", + "Source file, directory or disk (e.g., /dev/sda) (required)", + ) + createCmd.Flags().StringVarP( + &target, + "target", + "t", + "", + "Target backup file or network address (e.g., /path/to/backup.tar.gz, 127.0.0.1:8080/backup.gz, or 127.0.0.1:8080) (required)", + ) + createCmd.Flags().StringSliceVarP( + &excludePatterns, + "exclude", + "e", + []string{}, + "Patterns to exclude (can be specified multiple times)", + ) createCmd.MarkFlagRequired("source") createCmd.MarkFlagRequired("target") }