From c295ebde639a7ecc77fde921cac1c61190ee0c28 Mon Sep 17 00:00:00 2001 From: "Ethan P." Date: Sat, 24 Aug 2024 16:06:28 -0700 Subject: [PATCH] feat: Add Darwin implementation of ReflinkCopy FileCopyMethod --- .github/workflows/go.yml | 2 +- all_test.go | 3 ++ copy_methods_darwin.go | 87 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 copy_methods_darwin.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b050a7b..916b8e9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -20,7 +20,7 @@ jobs: - runner: windows-latest - runner: macos-latest filesystem: APFS - copymethod: GetBytes + copymethod: ReflinkCopy - runner: ubuntu-latest filesystem: btrfs copymethod: GetBytes diff --git a/all_test.go b/all_test.go index 6604bf7..2ce4d23 100644 --- a/all_test.go +++ b/all_test.go @@ -24,6 +24,9 @@ func setupFileCopyMethod(m *testing.M) { case "CopyBytes": defaultCopyMethodName = "CopyBytes" defaultCopyMethod = CopyBytes + case "ReflinkCopy": + defaultCopyMethodName = "ReflinkCopy" + defaultCopyMethod = ReflinkCopy } } diff --git a/copy_methods_darwin.go b/copy_methods_darwin.go new file mode 100644 index 0000000..578a39d --- /dev/null +++ b/copy_methods_darwin.go @@ -0,0 +1,87 @@ +//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, skipFile bool) { + if opt.FS != nil { + return fmt.Errorf("%w: cannot create reflink from Go's fs.FS interface", ErrUnsupportedCopyMethod), false + } + + if opt.WrapReader != nil { + return fmt.Errorf("%w: cannot create reflink when WrapReader option is used", ErrUnsupportedCopyMethod), false + } + + // 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, false + } + + err = unix.Clonefile(src, dest, clonefileFlags) // retry + } + + // Return error if clone is not possible. + if err != nil { + if os.IsNotExist(err) { + return nil, true // but not if source file doesn't exist + } + + return &os.PathError{ + Op: "create reflink", + Path: src, + Err: err, + }, false + } + + // 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, false + } + } + + return nil, false + }, +}