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:
+112
-32
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user