Skip to content

Commit

Permalink
feat: Add Darwin implementation of ReflinkCopy FileCopyMethod
Browse files Browse the repository at this point in the history
  • Loading branch information
eth-p committed Aug 25, 2024
1 parent 8b435ac commit f225cea
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- runner: windows-latest
- runner: macos-latest
filesystem: APFS
copymethod: GetBytes
copymethod: ReflinkCopy
- runner: ubuntu-latest
filesystem: btrfs
copymethod: GetBytes
Expand Down
3 changes: 3 additions & 0 deletions all_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ func setupFileCopyMethod(m *testing.M) {
case "CopyBytes":
defaultCopyMethodName = "CopyBytes"
defaultCopyMethod = CopyBytes
case "ReflinkCopy":
defaultCopyMethodName = "ReflinkCopy"
defaultCopyMethod = ReflinkCopy
}
}

Expand Down
79 changes: 79 additions & 0 deletions copy_methods_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//go:build darwin

package copy

import (
"errors"
"fmt"
"os"
"time"

"golang.org/x/sys/unix"
)

// ReflinkCopy tries to copy the file by creating a reflink from the source
// file to the destination file. This asks the filesystem to share the
// contents between the files using a copy-on-write method.
//
// Reflinks are the fastest way to copy large files, but have a few limitations:
//
// - Requires using a supported filesystem (btrfs, xfs, apfs)
// - Source and destination must be on the same filesystem.
//
// See: https://btrfs.readthedocs.io/en/latest/Reflink.html
//
// -------------------- PLATFORM SPECIFIC INFORMATION --------------------
//
// Darwin implementation uses the `clonefile` syscall:
// https://www.manpagez.com/man/2/clonefile/
//
// Support:
// - MacOS 10.14 or newer
// - APFS filesystem
//
// Considerations:
// - Ownership is not preserved.
// - Setuid and Setgid are not preserved.
// - Times are copied by default.
// - Flag CLONE_NOFOLLOW is not used, we use lcopy instead of fcopy for
// symbolic links.
var ReflinkCopy = FileCopyMethod{
fcopy: func(src, dest string, info os.FileInfo, opt Options) (err error) {
if opt.FS != nil {
return fmt.Errorf("%w: cannot create reflink from Go's fs.FS interface", ErrUnsupportedCopyMethod)
}

if opt.WrapReader != nil {
return fmt.Errorf("%w: cannot create reflink when WrapReader option is used", ErrUnsupportedCopyMethod)
}

// Do copy.
const clonefileFlags = 0
err = unix.Clonefile(src, dest, clonefileFlags)

// If the error is the file already exists, delete it and try again.
if errors.Is(err, os.ErrExist) {
if err = os.Remove(dest); err != nil {
return err
}

err = unix.Clonefile(src, dest, clonefileFlags) // retry
}

// Return an if copy-on-write is not possible.
if err != nil {
return fmt.Errorf("cannot create reflink: %w", err)
}

// Copy-on-write preserves the modtime by default.
// If PreserveTimes is not true, update the time to now.
if !opt.PreserveTimes {
now := time.Now()
if err := os.Chtimes(dest, now, now); err != nil {
return err
}
}

return
},
}

0 comments on commit f225cea

Please sign in to comment.