package assets import ( "flag" "fmt" "io" "os" "path/filepath" "sort" "strings" ) // allFiles returns every regular file under root (recursive). func allFiles(root string) ([]string, error) { var out []string err := filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { out = append(out, p) } return nil }) sort.Strings(out) return out, err } // runCheckDupes reports files whose lower-cased basename collides across the // given dirs. Read-only. Exit exitFail if any collision is found. func runCheckDupes(args []string, stdout, stderr io.Writer) int { fs := flag.NewFlagSet("check-dupes", flag.ContinueOnError) fs.SetOutput(stderr) if err := fs.Parse(args); err != nil { return exitUsage } dirs := fs.Args() if len(dirs) == 0 { fmt.Fprintln(stderr, "assets check-dupes: usage: check-dupes ...") return exitUsage } first := map[string]string{} fail := false for _, dir := range dirs { if fi, err := os.Stat(dir); err != nil || !fi.IsDir() { fmt.Fprintf(stderr, "assets check-dupes: no such dir: %s\n", dir) return exitUsage } files, err := allFiles(dir) if err != nil { fmt.Fprintln(stderr, "assets check-dupes:", err) return exitTool } for _, f := range files { key := strings.ToLower(filepath.Base(f)) if prev, ok := first[key]; ok { fmt.Fprintf(stderr, "runtime-name collision: %s collides with %s\n", f, prev) fail = true } else { first[key] = f } } } if fail { fmt.Fprintln(stderr, "check-dupes: found runtime-name collision(s)") return exitFail } fmt.Fprintln(stdout, "check-dupes: OK") return exitOK } // runCleanDupes deletes files from whose lower-cased basename collides // with any file in . Mutates only . Refuses to run if the two // trees overlap. func runCleanDupes(args []string, stdout, stderr io.Writer) int { fs := flag.NewFlagSet("clean-dupes", flag.ContinueOnError) fs.SetOutput(stderr) dryRun := fs.Bool("dry-run", false, "report deletions without removing") if err := fs.Parse(args); err != nil { return exitUsage } pos := fs.Args() if len(pos) != 2 { fmt.Fprintln(stderr, "assets clean-dupes: usage: clean-dupes [--dry-run] ") return exitUsage } primary, clean := pos[0], pos[1] for _, d := range []string{primary, clean} { if fi, err := os.Stat(d); err != nil || !fi.IsDir() { fmt.Fprintf(stderr, "assets clean-dupes: no such dir: %s\n", d) return exitUsage } } primaryReal, err1 := filepath.EvalSymlinks(primary) cleanReal, err2 := filepath.EvalSymlinks(clean) if err1 != nil || err2 != nil { fmt.Fprintln(stderr, "assets clean-dupes: cannot resolve dirs") return exitTool } if overlaps(primaryReal, cleanReal) { fmt.Fprintln(stderr, "assets clean-dupes: primary and clean dirs must not overlap") return exitUsage } primaryFiles, err := allFiles(primary) if err != nil { fmt.Fprintln(stderr, "assets clean-dupes:", err) return exitTool } names := map[string]bool{} for _, f := range primaryFiles { names[strings.ToLower(filepath.Base(f))] = true } cleanFiles, err := allFiles(clean) if err != nil { fmt.Fprintln(stderr, "assets clean-dupes:", err) return exitTool } removed := 0 for _, f := range cleanFiles { if !names[strings.ToLower(filepath.Base(f))] { continue } if *dryRun { fmt.Fprintf(stderr, "would delete clean-tree collision: %s\n", f) } else { if err := os.Remove(f); err != nil { fmt.Fprintln(stderr, "assets clean-dupes:", err) return exitTool } fmt.Fprintf(stderr, "deleted clean-tree collision: %s\n", f) } removed++ } verb := "removed" if *dryRun { verb = "would remove" } fmt.Fprintf(stdout, "clean-dupes: %s %d file(s)\n", verb, removed) return exitOK } // overlaps reports whether either directory contains the other (or they are // equal), using cleaned absolute-ish paths with a trailing separator. func overlaps(a, b string) bool { as := a + string(filepath.Separator) bs := b + string(filepath.Separator) return strings.HasPrefix(as, bs) || strings.HasPrefix(bs, as) }