diff --git a/application/config.go b/application/config.go index 944f619..cd629c0 100644 --- a/application/config.go +++ b/application/config.go @@ -33,7 +33,7 @@ func (c *Config) Validate() error { return fmt.Errorf("target dir is required") } - if !slices.Contains([]Mode{ModeHardlink, ModeSymlink, ModeMove, ModeSymlink}, c.Mode) { + if !slices.Contains([]Mode{ModeHardlink, ModeSymlink, ModeMove, ModeCopy}, c.Mode) { return fmt.Errorf("invalid mode %s", c.Mode) } diff --git a/internal/organizer/modes/copy.go b/internal/organizer/modes/copy.go index c15d7bb..8bd2ee5 100644 --- a/internal/organizer/modes/copy.go +++ b/internal/organizer/modes/copy.go @@ -5,12 +5,18 @@ import ( "io" "log" "os" + "runtime" + "sync/atomic" + + "golang.org/x/sys/unix" ) +var cowDisabled = atomic.Bool{} + type Copy struct{} func (c Copy) PlaceIt(sourcePath, targetPath string, mode os.FileMode) error { - targetFile, err := os.OpenFile(targetPath, os.O_TRUNC|os.O_RDWR|os.O_CREATE, mode) + targetFile, err := os.OpenFile(targetPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, mode) if err != nil { return fmt.Errorf("open target file: %w", err) } @@ -28,6 +34,17 @@ func (c Copy) PlaceIt(sourcePath, targetPath string, mode os.FileMode) error { _ = sourceFile.Close() }() + // Try to do a COW. + if runtime.GOOS == "linux" && !cowDisabled.Load() { + if err := unix.IoctlFileClone(int(targetFile.Fd()+1), int(sourceFile.Fd())); err == nil { + return nil + } else { + log.Println(fmt.Errorf("COW attempt for %s failed: %w", targetPath, err)) + log.Println("Disabling COW until restart") + cowDisabled.Store(true) + } + } + copySize, err := io.Copy(targetFile, sourceFile) if err != nil { return fmt.Errorf("copy source file: %w", err)