package fsops import ( "errors" "fmt" "io" "log" "os" "path/filepath" "syscall" ) type Manager struct { DryRun bool } var renameFn = os.Rename func (m Manager) EnsureDir(path string) error { if path == "" { return fmt.Errorf("ensure dir: empty path") } if m.DryRun { log.Printf("[dry-run] ensure dir %s\n", path) return nil } if err := os.MkdirAll(path, 0o755); err != nil { return fmt.Errorf("ensure dir %s: %w", path, err) } return nil } func (m Manager) Move(src, dst string) error { if src == "" || dst == "" { return fmt.Errorf("move: empty source or destination") } dstDir := filepath.Dir(dst) if err := m.EnsureDir(dstDir); err != nil { return err } if m.DryRun { log.Printf("[dry-run] move %s -> %s\n", src, dst) return nil } if err := renameFn(src, dst); err != nil { if errors.Is(err, syscall.EXDEV) { if copyErr := copyFile(src, dst); copyErr != nil { return fmt.Errorf("copy %s -> %s: %w", src, dst, copyErr) } if rmErr := os.Remove(src); rmErr != nil { return fmt.Errorf("remove %s after copy: %w", src, rmErr) } return nil } return fmt.Errorf("move %s -> %s: %w", src, dst, err) } return nil } func (m Manager) Remove(path string) error { if path == "" { return fmt.Errorf("remove: empty path") } if m.DryRun { log.Printf("[dry-run] remove %s\n", path) return nil } if err := os.Remove(path); err != nil && !os.IsNotExist(err) { return fmt.Errorf("remove %s: %w", path, err) } return nil } func (m Manager) RemoveAll(path string) error { if path == "" { return fmt.Errorf("remove all: empty path") } if m.DryRun { log.Printf("[dry-run] remove tree %s\n", path) return nil } if err := os.RemoveAll(path); err != nil { return fmt.Errorf("remove all %s: %w", path, err) } return nil } func (m Manager) RemoveEmptyDirs(root string) error { if root == "" { return fmt.Errorf("remove empty dirs: empty root") } if _, err := os.Stat(root); errors.Is(err, os.ErrNotExist) { return nil } else if err != nil { return err } return filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { if errors.Is(err, os.ErrNotExist) { return nil } return err } if path == root { return nil } if !d.IsDir() { return nil } entries, readErr := os.ReadDir(path) if readErr != nil { if errors.Is(readErr, os.ErrNotExist) { return nil } return readErr } if len(entries) > 0 { return nil } if m.DryRun { log.Printf("[dry-run] remove empty dir %s\n", path) return nil } if rmErr := os.Remove(path); rmErr != nil && !os.IsNotExist(rmErr) { return fmt.Errorf("remove empty dir %s: %w", path, rmErr) } return nil }) } func copyFile(src, dst string) error { info, err := os.Stat(src) if err != nil { return err } if info.IsDir() { return fmt.Errorf("copy directory not supported: %s", src) } in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode()) if err != nil { return err } defer func() { _ = out.Close() }() if _, err := io.Copy(out, in); err != nil { return err } return out.Close() }