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 (
|
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")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user