Skip to content

Commit 212ece9

Browse files
committed
feat: Add Darwin implementation of ReflinkCopy FileCopyMethod
1 parent 7923873 commit 212ece9

File tree

3 files changed

+92
-1
lines changed

3 files changed

+92
-1
lines changed

.github/workflows/filecopymethod-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
environment:
2424
- runner: macos-latest
2525
filesystem: APFS
26-
copymethod: GetBytes
26+
copymethod: ReflinkCopy
2727
- runner: ubuntu-latest
2828
filesystem: btrfs
2929
copymethod: GetBytes

all_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ func setupFileCopyMethod(m *testing.M) {
2828
defaultCopyMethod = CopyBytes
2929
supportsWrapReaderOption = true
3030
supportsFSOption = true
31+
case "ReflinkCopy":
32+
defaultCopyMethod = ReflinkCopy
33+
supportsWrapReaderOption = false
34+
supportsFSOption = false
3135
}
3236
}
3337

copy_methods_darwin.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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, skipFile bool) {
42+
if opt.FS != nil {
43+
return fmt.Errorf("%w: cannot create reflink from Go's fs.FS interface", ErrUnsupportedCopyMethod), false
44+
}
45+
46+
if opt.WrapReader != nil {
47+
return fmt.Errorf("%w: cannot create reflink when WrapReader option is used", ErrUnsupportedCopyMethod), false
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, false
58+
}
59+
60+
err = unix.Clonefile(src, dest, clonefileFlags) // retry
61+
}
62+
63+
// Return error if clone is not possible.
64+
if err != nil {
65+
if os.IsNotExist(err) {
66+
return nil, true // but not if source file doesn't exist
67+
}
68+
69+
return &os.PathError{
70+
Op: "create reflink",
71+
Path: src,
72+
Err: err,
73+
}, false
74+
}
75+
76+
// Copy-on-write preserves the modtime by default.
77+
// If PreserveTimes is not true, update the time to now.
78+
if !opt.PreserveTimes {
79+
now := time.Now()
80+
if err := os.Chtimes(dest, now, now); err != nil {
81+
return err, false
82+
}
83+
}
84+
85+
return nil, false
86+
},
87+
}

0 commit comments

Comments
 (0)