Skip to content

Commit f225cea

Browse files
committed
feat: Add Darwin implementation of ReflinkCopy FileCopyMethod
1 parent 8b435ac commit f225cea

File tree

3 files changed

+83
-1
lines changed

3 files changed

+83
-1
lines changed

.github/workflows/go.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- runner: windows-latest
2121
- runner: macos-latest
2222
filesystem: APFS
23-
copymethod: GetBytes
23+
copymethod: ReflinkCopy
2424
- runner: ubuntu-latest
2525
filesystem: btrfs
2626
copymethod: GetBytes

all_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ func setupFileCopyMethod(m *testing.M) {
2424
case "CopyBytes":
2525
defaultCopyMethodName = "CopyBytes"
2626
defaultCopyMethod = CopyBytes
27+
case "ReflinkCopy":
28+
defaultCopyMethodName = "ReflinkCopy"
29+
defaultCopyMethod = ReflinkCopy
2730
}
2831
}
2932

copy_methods_darwin.go

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//go:build darwin
2+
3+
package copy
4+
5+
import (
6+
"errors"
7+
"fmt"
8+
"os"
9+
"time"
10+
11+
"golang.org/x/sys/unix"
12+
)
13+
14+
// ReflinkCopy tries to copy the file by creating a reflink from the source
15+
// file to the destination file. This asks the filesystem to share the
16+
// contents between the files using a copy-on-write method.
17+
//
18+
// Reflinks are the fastest way to copy large files, but have a few limitations:
19+
//
20+
// - Requires using a supported filesystem (btrfs, xfs, apfs)
21+
// - Source and destination must be on the same filesystem.
22+
//
23+
// See: https://btrfs.readthedocs.io/en/latest/Reflink.html
24+
//
25+
// -------------------- PLATFORM SPECIFIC INFORMATION --------------------
26+
//
27+
// Darwin implementation uses the `clonefile` syscall:
28+
// https://www.manpagez.com/man/2/clonefile/
29+
//
30+
// Support:
31+
// - MacOS 10.14 or newer
32+
// - APFS filesystem
33+
//
34+
// Considerations:
35+
// - Ownership is not preserved.
36+
// - Setuid and Setgid are not preserved.
37+
// - Times are copied by default.
38+
// - Flag CLONE_NOFOLLOW is not used, we use lcopy instead of fcopy for
39+
// symbolic links.
40+
var ReflinkCopy = FileCopyMethod{
41+
fcopy: func(src, dest string, info os.FileInfo, opt Options) (err error) {
42+
if opt.FS != nil {
43+
return fmt.Errorf("%w: cannot create reflink from Go's fs.FS interface", ErrUnsupportedCopyMethod)
44+
}
45+
46+
if opt.WrapReader != nil {
47+
return fmt.Errorf("%w: cannot create reflink when WrapReader option is used", ErrUnsupportedCopyMethod)
48+
}
49+
50+
// Do copy.
51+
const clonefileFlags = 0
52+
err = unix.Clonefile(src, dest, clonefileFlags)
53+
54+
// If the error is the file already exists, delete it and try again.
55+
if errors.Is(err, os.ErrExist) {
56+
if err = os.Remove(dest); err != nil {
57+
return err
58+
}
59+
60+
err = unix.Clonefile(src, dest, clonefileFlags) // retry
61+
}
62+
63+
// Return an if copy-on-write is not possible.
64+
if err != nil {
65+
return fmt.Errorf("cannot create reflink: %w", err)
66+
}
67+
68+
// Copy-on-write preserves the modtime by default.
69+
// If PreserveTimes is not true, update the time to now.
70+
if !opt.PreserveTimes {
71+
now := time.Now()
72+
if err := os.Chtimes(dest, now, now); err != nil {
73+
return err
74+
}
75+
}
76+
77+
return
78+
},
79+
}

0 commit comments

Comments
 (0)