177 lines
3.2 KiB
Go
177 lines
3.2 KiB
Go
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()
|
|
}
|