package files import ( "errors" "fmt" "io" "io/fs" "os" "path/filepath" "syscall" "ddc/internal/diskspace" ) var ErrDestinationExists = errors.New("an item with this name already exists in the destination") func MoveInto(source, destinationDir string, reservedBytes int64, progress func(int64)) error { name := filepath.Base(source) target := filepath.Join(destinationDir, name) if exists(target) { return fmt.Errorf("%w: %s", ErrDestinationExists, name) } size, err := TreeSize(source) if err != nil { return err } renameErr := os.Rename(source, target) if renameErr == nil { progress(size) return nil } if !errors.Is(renameErr, syscall.EXDEV) { return renameErr } if err := diskspace.EnsureFree(destinationDir, size, reservedBytes); err != nil { return err } if err := copyTree(source, target, progress); err != nil { os.RemoveAll(target) return err } return os.RemoveAll(source) } func copyTree(source, target string, progress func(int64)) error { return filepath.WalkDir(source, func(current string, entry fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } relative, err := filepath.Rel(source, current) if err != nil { return err } destination := filepath.Join(target, relative) switch { case entry.IsDir(): return os.MkdirAll(destination, 0o755) case entry.Type().IsRegular(): return copyFile(current, destination, progress) default: return nil } }) } func copyFile(source, destination string, progress func(int64)) error { input, err := os.Open(source) if err != nil { return err } defer input.Close() output, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) if err != nil { return err } _, copyErr := io.Copy(output, &progressReader{reader: input, progress: progress}) closeErr := output.Close() if copyErr != nil { return copyErr } return closeErr } type progressReader struct { reader io.Reader progress func(int64) } func (reader *progressReader) Read(buffer []byte) (int, error) { count, err := reader.reader.Read(buffer) if count > 0 { reader.progress(int64(count)) } return count, err }