feat: add exclude pattern support for directory backups

- Added --exclude flag to allow excluding files/directories from backups
- Implemented pattern-based exclusion logic for directory backups -
Modified size calculation to account for excluded items - Updated
progress tracking to reflect excluded content - Added helper function
for path exclusion checks - Ensured consistent exclusion handling across
size calculation and backup

Key changes: 1. Added excludePatterns variable and flag binding 2.
Updated getPathSize to skip excluded paths 3. Implemented
shouldExcludePath with multiple matching strategies: - Full path
matching - Prefix matching for directories - Glob pattern matching -
Base name matching 4. Integrated exclusion checks in backupDirectory 5.
Propagated excludePatterns through initialModel 6. Maintained progress
accuracy by adjusting total size calculation

The new functionality allows users to specify exclusion patterns:
backup-tool create -s /data -t backup.tar.gz -e "*.tmp" -e "cache/"
This commit is contained in:
wt
2025-08-12 14:07:05 +07:00
parent 6b1a302c18
commit 4639779116
+112 -32
View File
@@ -24,8 +24,9 @@ import (
) )
var ( var (
source string source string
target string target string
excludePatterns []string
) )
var createCmd = &cobra.Command{ var createCmd = &cobra.Command{
@@ -33,7 +34,7 @@ var createCmd = &cobra.Command{
Short: "Create a new backup", Short: "Create a new backup",
Long: "Create a new backup of a file, directory, or disk", Long: "Create a new backup of a file, directory, or disk",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
model, err := initialModel(source, target) model, err := initialModel(source, target, excludePatterns)
if err != nil { if err != nil {
fmt.Println("Error:", err) fmt.Println("Error:", err)
os.Exit(1) os.Exit(1)
@@ -48,25 +49,26 @@ var createCmd = &cobra.Command{
} }
type model struct { type model struct {
progress progress.Model progress progress.Model
source string source string
target string target string
totalBytes int64 totalBytes int64
processed int64 processed int64
done bool done bool
err error err error
startTime time.Time startTime time.Time
progressCh chan int64 progressCh chan int64
isDirectory bool isDirectory bool
httpTargetInfo *ServerTargetInfo httpTargetInfo *ServerTargetInfo
isP2PTarget bool // Флаг для старого P2P-режима isP2PTarget bool // Флаг для старого P2P-режима
excludePatterns []string
} }
type progressMsg int64 type progressMsg int64
type doneMsg struct{} type doneMsg struct{}
type errorMsg struct{ err error } type errorMsg struct{ err error }
func getPathSize(path string) (int64, error) { func getPathSize(path string, excludePatterns []string) (int64, error) {
fileInfo, err := os.Stat(path) fileInfo, err := os.Stat(path)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -97,10 +99,27 @@ func getPathSize(path string) (int64, error) {
if fileInfo.IsDir() { if fileInfo.IsDir() {
var total int64 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 { if err != nil {
return err 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() { if !info.IsDir() {
total += info.Size() total += info.Size()
} }
@@ -112,8 +131,8 @@ func getPathSize(path string) (int64, error) {
return fileInfo.Size(), nil return fileInfo.Size(), nil
} }
func initialModel(src, dst string) (*model, error) { func initialModel(src, dst string, excludePatterns []string) (*model, error) {
totalBytes, err := getPathSize(src) totalBytes, err := getPathSize(src, excludePatterns)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -159,15 +178,16 @@ func initialModel(src, dst string) (*model, error) {
} }
return &model{ return &model{
progress: p, progress: p,
source: src, source: src,
target: targetPath, target: targetPath,
totalBytes: totalBytes, totalBytes: totalBytes,
startTime: time.Now(), startTime: time.Now(),
progressCh: make(chan int64, 100), progressCh: make(chan int64, 100),
isDirectory: isDirectory, isDirectory: isDirectory,
httpTargetInfo: targetInfo, httpTargetInfo: targetInfo,
isP2PTarget: isP2P, isP2PTarget: isP2P,
excludePatterns: excludePatterns,
}, nil }, nil
} }
@@ -278,6 +298,38 @@ func (m *model) backupFileOrDisk(w io.Writer) error {
return err 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 { func (m *model) backupDirectory(w io.Writer) error {
progressWriter := &progressTracker{Writer: w, progressCh: m.progressCh} progressWriter := &progressTracker{Writer: w, progressCh: m.progressCh}
gzipWriter := gzip.NewWriter(progressWriter) gzipWriter := gzip.NewWriter(progressWriter)
@@ -291,12 +343,21 @@ func (m *model) backupDirectory(w io.Writer) error {
return err return err
} }
header, err := tar.FileInfoHeader(fi, file) // Получаем относительный путь
relPath, err := filepath.Rel(m.source, file)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@@ -416,8 +477,27 @@ func (m *model) View() string {
func init() { func init() {
rootCmd.AddCommand(createCmd) rootCmd.AddCommand(createCmd)
createCmd.Flags().StringVarP(&source, "source", "s", "", "Source file, directory or disk (e.g., /dev/sda) (required)") createCmd.Flags().StringVarP(
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)") &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("source")
createCmd.MarkFlagRequired("target") createCmd.MarkFlagRequired("target")
} }