subsyt/internal/fsops/fsops.go
Viktor Varland a639ac073e
Some checks are pending
build / build (push) Waiting to run
fix: attempt to fix cross device move
2025-10-02 18:03:23 +02:00

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()
}