package storage import ( "fmt" "os" "os/exec" "regexp" "strings" ) var forbiddenFlagChars = regexp.MustCompile("[;&|$`\"<>\r\n\x00]") type MoverConfig struct { Source string Dest string CleanMacOS bool RemoveSrc bool Inplace bool ExtraFlags []string DryRun bool } func BuildPreviewArgs(cfg MoverConfig) ([]string, error) { if cfg.Source == "" || cfg.Dest == "" { return nil, fmt.Errorf("source and dest are required") } args := []string{ "rsync", "-an", "--remove-source-files", "--out-format=%n", cfg.Source + "/", cfg.Dest + "/", } return args, nil } func BuildMoverArgs(cfg MoverConfig) ([]string, error) { if cfg.Source == "" || cfg.Dest == "" { return nil, fmt.Errorf("source and dest are required") } args := []string{"rsync", "-aAXv"} if !cfg.Inplace { args = append(args, "--inplace") } args = append(args, "--remove-source-files") if !cfg.DryRun { args = append(args, "--progress", "--info=progress2") } args = append(args, "--no-compress") args = append(args, cfg.ExtraFlags...) args = append(args, "--info=progress2") args = append(args, cfg.Source+"/", cfg.Dest+"/") return args, nil } func CleanMacOSJunk(root string) error { for _, pattern := range []string{".DS_Store", "._*"} { cmd := exec.Command("find", root, "-name", pattern, "-delete") if err := cmd.Run(); err != nil { return fmt.Errorf("clean junk %s: %w", pattern, err) } } return nil } func RemoveEmptyDirs(root string) error { cmd := exec.Command("find", root, "-mindepth", "1", "-type", "d", "-empty", "-delete") if err := cmd.Run(); err != nil { return fmt.Errorf("remove empty dirs: %w", err) } return nil } func ValidateMoverPaths(cfg MoverConfig) error { if cfg.Source == "" { return fmt.Errorf("source path is required") } if cfg.Dest == "" { return fmt.Errorf("destination path is required") } info, err := os.Stat(cfg.Source) if err != nil { return fmt.Errorf("source %s: %w", cfg.Source, err) } if !info.IsDir() { return fmt.Errorf("source %s: not a directory", cfg.Source) } info, err = os.Stat(cfg.Dest) if err != nil { return fmt.Errorf("destination %s: %w", cfg.Dest, err) } if !info.IsDir() { return fmt.Errorf("destination %s: not a directory", cfg.Dest) } return nil } func ParseExtraRsyncFlags(raw string) ([]string, error) { var out []string lines := strings.Split(raw, "\n") for i, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } if !strings.HasPrefix(line, "-") { return nil, fmt.Errorf("line %d: flag must start with '-' (%q)", i+1, line) } if forbiddenFlagChars.MatchString(line) { return nil, fmt.Errorf("line %d: flag contains forbidden characters (%q)", i+1, line) } out = append(out, line) } return out, nil }