diff --git a/cmd/acp-rewrite/main.go b/cmd/acp-rewrite/main.go index 1dce3be..bcfe2d1 100644 --- a/cmd/acp-rewrite/main.go +++ b/cmd/acp-rewrite/main.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/json" + "errors" "flag" "fmt" "io/fs" @@ -12,6 +13,7 @@ import ( "path" "path/filepath" "sort" + "strings" "time" "github.com/samuelncui/acp" @@ -32,6 +34,7 @@ type rewriteState struct { Root string `json:"root"` Pending []rewriteEntry `json:"pending,omitempty"` Busy []rewriteEntry `json:"busy,omitempty"` + Missing []rewriteEntry `json:"missing,omitempty"` TmpFiles []string `json:"tmp_files,omitempty"` } @@ -41,6 +44,12 @@ func main() { statePath := flag.String("state", ".acp-rewrite-state.json", "state storage path") reportPath := flag.String("report", "", "json report storage path") reportIndent := flag.Bool("report-indent", false, "json report with indent") + ignorePaths := make([]string, 0, 4) + + flag.Func("ignore", "ignore path, file or dir", func(s string) error { + ignorePaths = append(ignorePaths, s) + return nil + }) flag.Parse() if flag.NArg() == 0 { @@ -63,6 +72,10 @@ func main() { logrus.Fatalf("get abs report path fail, %s", err) } } + ignoreAbs, err := normalizeIgnorePaths(rootAbs, ignorePaths) + if err != nil { + logrus.Fatalf("normalize ignore path fail, %s", err) + } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() @@ -87,8 +100,11 @@ func main() { } if len(state.Pending) == 0 && len(state.Busy) == 0 { - entries, err := scanEntries(rootAbs, stateAbs, reportAbs) + entries, err := scanEntries(ctx, rootAbs, stateAbs, reportAbs, ignoreAbs) if err != nil { + if errors.Is(err, context.Canceled) { + return + } logrus.Fatalf("scan path fail, %s", err) } state.Pending = entries @@ -128,6 +144,22 @@ func main() { return } + if _, err := os.Stat(entry.Path); err != nil { + if os.IsNotExist(err) { + state.Missing = append(state.Missing, entry) + if bar != nil { + _ = bar.Add(1) + } + continue + } + logrus.Warnf("stat fail, path= '%s', err= %s", entry.Path, err) + state.Pending = append(state.Pending, entry) + if bar != nil { + _ = bar.Add(1) + } + continue + } + busy, err := isFileBusy(entry.Path) if err != nil { logrus.Warnf("check busy fail, path= '%s', err= %s", entry.Path, err) @@ -170,6 +202,9 @@ func main() { } if err != nil { + if ctx.Err() != nil { + _ = os.Remove(tmpPath) + } logrus.Warnf("rewrite fail, path= '%s', err= %s", entry.Path, err) state.Pending = append(state.Pending, entry) if bar != nil { @@ -204,14 +239,23 @@ func main() { printDuplicates(reportJobs) } -func scanEntries(root, statePath, reportPath string) ([]rewriteEntry, error) { +func scanEntries(ctx context.Context, root, statePath, reportPath string, ignorePaths []string) ([]rewriteEntry, error) { groups := make(map[fileIdentity][]string) entries := make([]rewriteEntry, 0, 128) err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error { + if ctx.Err() != nil { + return ctx.Err() + } if err != nil { return err } + if shouldIgnorePath(p, ignorePaths) { + if d.IsDir() { + return fs.SkipDir + } + return nil + } if p == statePath || (reportPath != "" && p == reportPath) { return nil } @@ -256,6 +300,45 @@ func scanEntries(root, statePath, reportPath string) ([]rewriteEntry, error) { return entries, nil } +func normalizeIgnorePaths(root string, ignores []string) ([]string, error) { + if len(ignores) == 0 { + return nil, nil + } + normalized := make([]string, 0, len(ignores)) + for _, p := range ignores { + if p == "" { + continue + } + if !filepath.IsAbs(p) { + p = filepath.Join(root, p) + } + abs, err := filepath.Abs(p) + if err != nil { + return nil, err + } + normalized = append(normalized, filepath.Clean(abs)) + } + return normalized, nil +} + +func shouldIgnorePath(p string, ignores []string) bool { + if len(ignores) == 0 { + return false + } + for _, ig := range ignores { + if ig == "" { + continue + } + if p == ig { + return true + } + if strings.HasPrefix(p, ig+string(os.PathSeparator)) { + return true + } + } + return false +} + func newTmpPath(path string, rnd *rand.Rand) (string, error) { suffix := func() string { const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" diff --git a/cmd/acp-rewrite/main_test.go b/cmd/acp-rewrite/main_test.go index a76a6ab..1d35285 100644 --- a/cmd/acp-rewrite/main_test.go +++ b/cmd/acp-rewrite/main_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "path/filepath" "runtime" @@ -38,7 +39,7 @@ func TestScanEntriesHardlinks(t *testing.T) { t.Fatalf("write c: %v", err) } - entries, err := scanEntries(root, statePath, reportPath) + entries, err := scanEntries(context.Background(), root, statePath, reportPath, nil) if err != nil { t.Fatalf("scan entries: %v", err) } @@ -69,6 +70,52 @@ func TestScanEntriesHardlinks(t *testing.T) { } } +func TestScanEntriesCanceled(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + root := t.TempDir() + entries, err := scanEntries(ctx, root, "", "", nil) + if err == nil || err != context.Canceled { + t.Fatalf("expected canceled, got %v", err) + } + if len(entries) != 0 { + t.Fatalf("entries count = %d", len(entries)) + } +} + +func TestScanEntriesIgnorePaths(t *testing.T) { + root := t.TempDir() + keep := filepath.Join(root, "keep.txt") + skipDir := filepath.Join(root, "skip") + skipFile := filepath.Join(root, "skip.txt") + + if err := os.MkdirAll(skipDir, 0o755); err != nil { + t.Fatalf("mkdir skip: %v", err) + } + if err := os.WriteFile(filepath.Join(skipDir, "a.txt"), []byte("a"), 0o644); err != nil { + t.Fatalf("write skip a: %v", err) + } + if err := os.WriteFile(skipFile, []byte("skip"), 0o644); err != nil { + t.Fatalf("write skip file: %v", err) + } + if err := os.WriteFile(keep, []byte("keep"), 0o644); err != nil { + t.Fatalf("write keep: %v", err) + } + + ignorePaths, err := normalizeIgnorePaths(root, []string{"skip", "skip.txt"}) + if err != nil { + t.Fatalf("normalize ignore: %v", err) + } + entries, err := scanEntries(context.Background(), root, "", "", ignorePaths) + if err != nil { + t.Fatalf("scan entries: %v", err) + } + if len(entries) != 1 || entries[0].Path != keep { + t.Fatalf("entries = %+v", entries) + } +} + func TestRelinkOnePreserveLinkAttrs(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("hardlink attrs unsupported on windows")