Repository: rclone/rclone (language: go)

Issue:
Add extended attributes
I'm trying very hard to restore my macOS system from b2 after copying it with Rclone.

I'm now realizing that Rclone didn't save extended attributes and that means that all my macOS Applications won't open at all since they're missing those attributes.

I know it's been brought up a few times before but if support for xattrs could be added at some point that would be very useful

otherwise.. thankfully I still have the original files with the attributes on them. I was hoping to easily substitute rclone by rsync in my script.. but it isn't working out because rsync doesn't support filter-from and their include/exclude/include-from/exclude-from works differently from rclone's. Rclone is much more intuitive and I'm finding it impossible to use the same filters =/

However rsync has a --files-from which says "rsync all those files in a file and nothing else" This would work perfectly if I could get Rclone to produce a list of files that match Rclone's filters.. is there a way to do that? I realize Rclone's dry-run comes close but then a bunch of text would need to be grepped out so rsync would accept it as a list of filepaths

any help would be.. really appreciated!


Discussion / hints:
@ncw would it be possible and maybe not too difficult to keep extended attributes when doing local to local rclone at least?

btw.. I managed to create a list of files that match the rclone filter in a format that is usable by rsync's files-from

it's ugly.. because my regex is quite poor.. but it does work. Basically I did a --dry-run from source -> empty folder with rclone and then cleaned up the extra noise with sed:

```
logFile=${1}

sed -E -i.bak 's#^[0-9]{1,4}/[0-9]{1,2}/[0-9]{1,2} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2} [a-zA-Z ]{1,6}: ##' $logFile
sed -E -i.bak '/^Local file system at/d' $logFile
sed -E -i.bak 's/: Not copying as --dry-run//g' $logFile

sed -E -i.bak '/^Transferred:/d' $logFile
sed -E -i.bak '/^Errors:/d' $logFile
sed -E -i.bak '/^Checks:/d' $logFile
sed -E -i.bak '/^Transferred:/d' $logFile
sed -E -i.bak '/^Elapsed time:/d' $logFile
```

> I'm trying very hard to restore my macOS system from b2 after copying it with Rclone.
> 
> I'm now realizing that Rclone didn't save extended attributes and that means that all my macOS Applications won't open at all since they're missing those attributes.

:-(  I'm sorry about that.  I'm not an OS X user so it dropped off my radar.  How much stuff does OS X store in the attributes? In the bad old days, mac OS used to store the whole program binary in the resource fork - is that the case now?

Could you make a new ticket specifically about xattrs because I don't think there is one and if you could describe what OS X stores in them, and how OS X programs copy them about to systems which don't understand xattrs (eg FAT formatted disks, or Google Drive) that would be really useful.

> However rsync has a --files-from which says "rsync all those files in a file and nothing else" This would work perfectly if I could get Rclone to produce a list of files that match Rclone's filters.. is there a way to do that? 

You are going to kick yourself, but `rclone ls` is what you want!

> would it be possible and maybe not too difficult to keep extended attributes when doing local to local rclone at least?

Can you write that in the xattrs issue too please - it is a good idea!
hah.. yea.. thankfully I didn't waste too much time writing those sed filters.. and I still need to use a sed filter on rclone ls to remove the size at the beginning.. since rsync's --files-from expects only paths

thanks for letting me know. would be nice to add an option to return only file paths and not size.. datestamps would be a useful optional output of ls too

for the xattrs to be honest I barely understand it myself.. I mean I don't even understand what's really going on here.. I just know after using rsync with -E flag all my apps work correctly again. But that means my b2 backup is not as useful as I thought it was

if I come across the info you need I'll be sure to pass it along to you
If `rsync -E` fixed it, then what you are after is just the normal attributes I think?  Namely executable?
Because my local `xfs` file system [supports metadata checksums only](https://git.kernel.org/pub/scm/fs/xfs/xfsprogs-dev.git/tree/doc/CHANGES?h=v3.2.3#n10), I too am using [extended attributes](https://en.wikipedia.org/wiki/Extended_file_attributes) to store a file's `md5sum` as extended attributes:

```
$ getfattr -d README 
# file: README
user.checksum.md5="7599d8ff89f9a84e9a4259eca5eeef7c"
````
This works in [Linux](https://manpages.debian.org/unstable/attr/setfattr.1.en.html "setfattr(1)") but also (with different tools) for [FreeBSD](https://github.com/edenzik/pxattr "A fork of pxattr: a portable extended attributes C++ programming interface"), [macOS](https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man1/xattr.1.html "xattr(1)") and [Solaris](https://docs.oracle.com/cd/E36784_01/html/E36870/runat-1.html "runat(1)") systems. Ideally those attributes should be stored in the backups too. `rsync` has `-X` (or `-E` on macOS) to accomplish this, but of course the backup destination must support those attributes as well. [Backblaze B2](https://www.backblaze.com/b2/docs/b2_get_file_info.html) appears to have support for something called `fileInfo` - maybe this can used to store user-defined extended attributes? I realize that B2 stores `contentSha1` anyway, so my need to store `user.checksum.md5` may be moot, but it was just meant as an example; @vesper8 mentioned some attributes that macOS needs to start applications but it really could be anything.
S3 (which is what I use rclone with - the protocol if not the service) supports ["User Metadata"](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html) which maps really well from xattr.

For example, the FUSE filesystem [YAS3FS](https://github.com/danilop/yas3fs) stores xattr as S3 user metadata
I've been researching this a bit; if I can find time, I might have a stab at implementing something. For now, I'm documenting my findings here...

@ncw macOS bundles a variant of rsync 2.6.9 that includes a patch that uses the `-E` option to copy extended attributes. When this functionality was later officially added to rsync, the options were changed so that `-E` became "preserve executability" and `-X` became "preserve extended attributes".

The convention on Mac OS X when copying files with extended attributes to filesystems that do not support them is to create a file that has the name of the original file prefixed by a "._" that contains the original file's extended attributes in [AppleDouble](https://web.archive.org/web/20180311140826if_/http://kaiser-edv.de/documents/AppleSingle_AppleDouble.pdf) file format.

It is possible to combine rclone with rsync to achieve what @vesper8 is looking for. You need `--noappledouble=false` to enable MacFUSE ('FUSE for macOS' now?) to support AppleDouble files and `--vfs-cache-mode=writes` since writing extended attributes to the AppleDouble file results in seek operations.

For example, I used:

```
% rclone mount b2remote:metadaddy-private ~/b2mount --noappledouble=false --vfs-cache-mode=writes
```

Then, in another terminal tab:

```
# The -l@ combination of options makes ls display extended attribute keys and sizes
% ls -l@ ~/synctest
total 2528
-rw-r--r--@ 1 ppatterson  staff  288272 Jun  2 16:23 image1.png
	com.apple.lastuseddate#PS	    16 
	com.apple.metadata:kMDItemIsScreenCapture	    42 
	com.apple.metadata:kMDItemScreenCaptureGlobalRect	    76 
	com.apple.metadata:kMDItemScreenCaptureType	    49 
-rw-r--r--@ 1 ppatterson  staff  328771 Jun  2 16:23 image2.png
	com.apple.lastuseddate#PS	    16 
	com.apple.metadata:kMDItemIsScreenCapture	    42 
	com.apple.metadata:kMDItemScreenCaptureGlobalRect	    76 
	com.apple.metadata:kMDItemScreenCaptureType	    49 
-rw-r--r--@ 1 ppatterson  staff  305882 Jun  2 12:45 image3.png
	com.apple.lastuseddate#PS	    16 
	com.apple.metadata:kMDItemIsScreenCapture	    42 
	com.apple.metadata:kMDItemScreenCaptureGlobalRect	    76 
	com.apple.metadata:kMDItemScreenCaptureType	    49 
-rw-r--r--@ 1 ppatterson  staff  364312 Jun  2 12:45 image4.png
	com.apple.lastuseddate#PS	    16 
	com.apple.metadata:kMDItemIsScreenCapture	    42 
	com.apple.metadata:kMDItemScreenCaptureGlobalRect	    76 
	com.apple.metadata:kMDItemScreenCaptureType	    49 
% rsync -r -E synctest ~/b2mount
```

This results in eight files being written to B2:

```
% rclone ls b2remote:metadaddy-private/synctest
     4096 ._image1.png
     4096 ._image2.png
     4096 ._image3.png
     4096 ._image4.png
   288272 image1.png
   328771 image2.png
   305882 image3.png
   364312 image4.png
```

Let's grab one and take a closer look:

```
% rclone copyto b2remote:metadaddy-private/synctest/._image1.png ._image1.png
% ls -la
total 8
drwxr-xr-x    3 ppatterson  staff     96 Nov 26 22:53 .
drwx------@ 743 ppatterson  staff  23776 Nov 26 22:53 ..
-rw-r--r--    1 ppatterson  staff   4096 Nov 26 20:12 ._image1.png
% file ._image1.png 
._image1.png: AppleDouble encoded Macintosh file
% hexdump -C ._image1.png 
00000000  00 05 16 07 00 02 00 00  4d 61 63 20 4f 53 20 58  |........Mac OS X|
00000010  20 20 20 20 20 20 20 20  00 02 00 00 00 09 00 00  |        ........|
00000020  00 32 00 00 0e b0 00 00  00 02 00 00 0e e2 00 00  |.2..............|
00000030  01 1e 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000050  00 00 00 00 41 54 54 52  00 00 00 05 00 00 0e e2  |....ATTR........|
00000060  00 00 01 50 00 00 00 b7  00 00 00 00 00 00 00 00  |...P............|
00000070  00 00 00 00 00 00 00 04  00 00 01 50 00 00 00 10  |...........P....|
00000080  00 00 1a 63 6f 6d 2e 61  70 70 6c 65 2e 6c 61 73  |...com.apple.las|
00000090  74 75 73 65 64 64 61 74  65 23 50 53 00 00 00 00  |tuseddate#PS....|
000000a0  00 00 01 60 00 00 00 2a  00 00 2a 63 6f 6d 2e 61  |...`...*..*com.a|
000000b0  70 70 6c 65 2e 6d 65 74  61 64 61 74 61 3a 6b 4d  |pple.metadata:kM|
000000c0  44 49 74 65 6d 49 73 53  63 72 65 65 6e 43 61 70  |DItemIsScreenCap|
000000d0  74 75 72 65 00 00 00 00  00 00 01 8a 00 00 00 4c  |ture...........L|
000000e0  00 00 32 63 6f 6d 2e 61  70 70 6c 65 2e 6d 65 74  |..2com.apple.met|
000000f0  61 64 61 74 61 3a 6b 4d  44 49 74 65 6d 53 63 72  |adata:kMDItemScr|
00000100  65 65 6e 43 61 70 74 75  72 65 47 6c 6f 62 61 6c  |eenCaptureGlobal|
00000110  52 65 63 74 00 00 00 00  00 00 01 d6 00 00 00 31  |Rect...........1|
00000120  00 00 2c 63 6f 6d 2e 61  70 70 6c 65 2e 6d 65 74  |..,com.apple.met|
00000130  61 64 61 74 61 3a 6b 4d  44 49 74 65 6d 53 63 72  |adata:kMDItemScr|
00000140  65 65 6e 43 61 70 74 75  72 65 54 79 70 65 00 00  |eenCaptureType..|
00000150  fa 79 7a 64 00 00 00 00  94 fc cd 1e 00 00 00 00  |.yzd............|
00000160  62 70 6c 69 73 74 30 30  09 08 00 00 00 00 00 00  |bplist00........|
00000170  01 01 00 00 00 00 00 00  00 01 00 00 00 00 00 00  |................|
00000180  00 00 00 00 00 00 00 00  00 09 62 70 6c 69 73 74  |..........bplist|
00000190  30 30 a4 01 01 02 03 23  00 00 00 00 00 00 00 00  |00.....#........|
000001a0  23 40 9e 00 00 00 00 00  00 23 40 90 e0 00 00 00  |#@.......#@.....|
000001b0  00 00 08 0d 16 1f 00 00  00 00 00 00 01 01 00 00  |................|
000001c0  00 00 00 00 00 04 00 00  00 00 00 00 00 00 00 00  |................|
000001d0  00 00 00 00 00 28 62 70  6c 69 73 74 30 30 57 64  |.....(bplist00Wd|
000001e0  69 73 70 6c 61 79 08 00  00 00 00 00 00 01 01 00  |isplay..........|
000001f0  00 00 00 00 00 00 01 00  00 00 00 00 00 00 00 00  |................|
00000200  00 00 00 00 00 00 10 00  00 00 00 00 00 00 00 00  |................|
00000210  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000ee0  00 00 00 00 01 00 00 00  01 00 00 00 00 00 00 00  |................|
00000ef0  00 1e 54 68 69 73 20 72  65 73 6f 75 72 63 65 20  |..This resource |
00000f00  66 6f 72 6b 20 69 6e 74  65 6e 74 69 6f 6e 61 6c  |fork intentional|
00000f10  6c 79 20 6c 65 66 74 20  62 6c 61 6e 6b 20 20 20  |ly left blank   |
00000f20  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000fe0  00 00 00 00 01 00 00 00  01 00 00 00 00 00 00 00  |................|
00000ff0  00 1e 00 00 00 00 00 00  00 00 00 1c 00 1e ff ff  |................|
00001000
```

The next step is to figure out how the extended attributes that rsync reads from APFS on my local disk get transformed into the `._` prefixed AppleDouble-format file that lands in B2. Looking at the rsync code, it just seems to call [setxattr()](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/setxattr.2.html). 

Looking at MacFuse, on macOS 10.5 and above, it seems to just hand the AppleDouble file (unless `--noappledouble` is `true`) off to rclone, so it seems like macOS itself is writing the AppleDouble file. On macOS 10.4 ('Tiger'), it looks like MacFuse actually writes the AppleDouble file.

So, putting it all together, it seems like rclone needs an option that enables it to read and write extended attributes via system calls when accessing macOS file systems that support them, and as AppleDouble files when accessing anything else.

It looks like the [copyfile()](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/copyfile.3.html) library function's `COPYFILE_PACK` and `COPYFILE_UNPACK` flags should do most of the work for us. The [apfs](https://github.com/go-darwin/apfs) package provides a golang binding for copyfile().
> So, putting it all together, it seems like rclone needs an option that enables it to read and write extended attributes via system calls when accessing macOS file systems that support them, and as AppleDouble files when accessing anything else.

Rclone does now have support for this with the `--metadata` flag. On the local backend this will read and write the xattr and this will be saved and restored in any backend that supports metadata (like s3).

However... rclone only saves and restores xattrs starting with the "user." tag at the moment

https://github.com/rclone/rclone/blob/85f142a206f83d1d3b03e271ada6509ef6b8c39e/backend/local/xattr.go#L16

So if you wanted to experiment you could try setting that to `""` and see if rclone can preserve xattr when copying local to local with `-M`.



Fix patch:
diff --git a/backend/local/clone_darwin.go b/backend/local/clone_darwin.go
new file mode 100644
index 0000000000000..d3410ed60e9c9
--- /dev/null
+++ b/backend/local/clone_darwin.go
@@ -0,0 +1,82 @@
+//go:build darwin && cgo
+
+// Package local provides a filesystem interface
+package local
+
+import (
+	"context"
+	"fmt"
+	"runtime"
+
+	"github.com/go-darwin/apfs"
+	"github.com/rclone/rclone/fs"
+)
+
+// Copy src to this remote using server-side copy operations.
+//
+// # This is stored with the remote path given
+//
+// # It returns the destination Object and a possible error
+//
+// Will only be called if src.Fs().Name() == f.Name()
+//
+// If it isn't possible then return fs.ErrorCantCopy
+func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
+	if runtime.GOOS != "darwin" || f.opt.TranslateSymlinks || f.opt.NoClone {
+		return nil, fs.ErrorCantCopy
+	}
+	srcObj, ok := src.(*Object)
+	if !ok {
+		fs.Debugf(src, "Can't clone - not same remote type")
+		return nil, fs.ErrorCantCopy
+	}
+
+	// Fetch metadata if --metadata is in use
+	meta, err := fs.GetMetadataOptions(ctx, f, src, fs.MetadataAsOpenOptions(ctx))
+	if err != nil {
+		return nil, fmt.Errorf("copy: failed to read metadata: %w", err)
+	}
+
+	// Create destination
+	dstObj := f.newObject(remote)
+	err = dstObj.mkdirAll()
+	if err != nil {
+		return nil, err
+	}
+
+	err = Clone(srcObj.path, f.localPath(remote))
+	if err != nil {
+		return nil, err
+	}
+	fs.Debugf(remote, "server-side cloned!")
+
+	// Set metadata if --metadata is in use
+	if meta != nil {
+		err = dstObj.writeMetadata(meta)
+		if err != nil {
+			return nil, fmt.Errorf("copy: failed to set metadata: %w", err)
+		}
+	}
+
+	return f.NewObject(ctx, remote)
+}
+
+// Clone uses APFS cloning if possible, otherwise falls back to copying (with full metadata preservation)
+// note that this is closely related to unix.Clonefile(src, dst, unix.CLONE_NOFOLLOW) but not 100% identical
+// https://opensource.apple.com/source/copyfile/copyfile-173.40.2/copyfile.c.auto.html
+func Clone(src, dst string) error {
+	state := apfs.CopyFileStateAlloc()
+	defer func() {
+		if err := apfs.CopyFileStateFree(state); err != nil {
+			fs.Errorf(dst, "free state error: %v", err)
+		}
+	}()
+	cloned, err := apfs.CopyFile(src, dst, state, apfs.COPYFILE_CLONE)
+	fs.Debugf(dst, "isCloned: %v, error: %v", cloned, err)
+	return err
+}
+
+// Check the interfaces are satisfied
+var (
+	_ fs.Copier = &Fs{}
+)
diff --git a/backend/local/local.go b/backend/local/local.go
index e292e689ac5d4..1a96d8036583b 100644
--- a/backend/local/local.go
+++ b/backend/local/local.go
@@ -32,9 +32,11 @@ import (
 )
 
 // Constants
-const devUnset = 0xdeadbeefcafebabe                                       // a device id meaning it is unset
-const linkSuffix = ".rclonelink"                                          // The suffix added to a translated symbolic link
-const useReadDir = (runtime.GOOS == "windows" || runtime.GOOS == "plan9") // these OSes read FileInfos directly
+const (
+	devUnset   = 0xdeadbeefcafebabe                                     // a device id meaning it is unset
+	linkSuffix = ".rclonelink"                                          // The suffix added to a translated symbolic link
+	useReadDir = (runtime.GOOS == "windows" || runtime.GOOS == "plan9") // these OSes read FileInfos directly
+)
 
 // timeType allows the user to choose what exactly ModTime() returns
 type timeType = fs.Enum[timeTypeChoices]
@@ -78,41 +80,46 @@ supported by all file systems) under the "user.*" prefix.
 Metadata is supported on files and directories.
 `,
 		},
-		Options: []fs.Option{{
-			Name:     "nounc",
-			Help:     "Disable UNC (long path names) conversion on Windows.",
-			Default:  false,
-			Advanced: runtime.GOOS != "windows",
-			Examples: []fs.OptionExample{{
-				Value: "true",
-				Help:  "Disables long file names.",
-			}},
-		}, {
-			Name:     "copy_links",
-			Help:     "Follow symlinks and copy the pointed to item.",
-			Default:  false,
-			NoPrefix: true,
-			ShortOpt: "L",
-			Advanced: true,
-		}, {
-			Name:     "links",
-			Help:     "Translate symlinks to/from regular files with a '" + linkSuffix + "' extension.",
-			Default:  false,
-			NoPrefix: true,
-			ShortOpt: "l",
-			Advanced: true,
-		}, {
-			Name: "skip_links",
-			Help: `Don't warn about skipped symlinks.
+		Options: []fs.Option{
+			{
+				Name:     "nounc",
+				Help:     "Disable UNC (long path names) conversion on Windows.",
+				Default:  false,
+				Advanced: runtime.GOOS != "windows",
+				Examples: []fs.OptionExample{{
+					Value: "true",
+					Help:  "Disables long file names.",
+				}},
+			},
+			{
+				Name:     "copy_links",
+				Help:     "Follow symlinks and copy the pointed to item.",
+				Default:  false,
+				NoPrefix: true,
+				ShortOpt: "L",
+				Advanced: true,
+			},
+			{
+				Name:     "links",
+				Help:     "Translate symlinks to/from regular files with a '" + linkSuffix + "' extension.",
+				Default:  false,
+				NoPrefix: true,
+				ShortOpt: "l",
+				Advanced: true,
+			},
+			{
+				Name: "skip_links",
+				Help: `Don't warn about skipped symlinks.
 
 This flag disables warning messages on skipped symlinks or junction
 points, as you explicitly acknowledge that they should be skipped.`,
-			Default:  false,
-			NoPrefix: true,
-			Advanced: true,
-		}, {
-			Name: "zero_size_links",
-			Help: `Assume the Stat size of links is zero (and read them instead) (deprecated).
+				Default:  false,
+				NoPrefix: true,
+				Advanced: true,
+			},
+			{
+				Name: "zero_size_links",
+				Help: `Assume the Stat size of links is zero (and read them instead) (deprecated).
 
 Rclone used to use the Stat size of links as the link size, but this fails in quite a few places:
 
@@ -122,11 +129,12 @@ Rclone used to use the Stat size of links as the link size, but this fails in qu
 
 So rclone now always reads the link.
 `,
-			Default:  false,
-			Advanced: true,
-		}, {
-			Name: "unicode_normalization",
-			Help: `Apply unicode NFC normalization to paths and filenames.
+				Default:  false,
+				Advanced: true,
+			},
+			{
+				Name: "unicode_normalization",
+				Help: `Apply unicode NFC normalization to paths and filenames.
 
 This flag can be used to normalize file names into unicode NFC form
 that are read from the local filesystem.
@@ -140,11 +148,12 @@ some OSes.
 
 Note that rclone compares filenames with unicode normalization in the sync
 routine so this flag shouldn't normally be used.`,
-			Default:  false,
-			Advanced: true,
-		}, {
-			Name: "no_check_updated",
-			Help: `Don't check to see if the files change during upload.
+				Default:  false,
+				Advanced: true,
+			},
+			{
+				Name: "no_check_updated",
+				Help: `Don't check to see if the files change during upload.
 
 Normally rclone checks the size and modification time of files as they
 are being uploaded and aborts with a message which starts "can't copy -
@@ -175,68 +184,96 @@ directory listing (where the initial stat value comes from on Windows)
 and when stat is called on them directly. Other copy tools always use
 the direct stat value and setting this flag will disable that.
 `,
-			Default:  false,
-			Advanced: true,
-		}, {
-			Name:     "one_file_system",
-			Help:     "Don't cross filesystem boundaries (unix/macOS only).",
-			Default:  false,
-			NoPrefix: true,
-			ShortOpt: "x",
-			Advanced: true,
-		}, {
-			Name: "case_sensitive",
-			Help: `Force the filesystem to report itself as case sensitive.
+				Default:  false,
+				Advanced: true,
+			},
+			{
+				Name:     "one_file_system",
+				Help:     "Don't cross filesystem boundaries (unix/macOS only).",
+				Default:  false,
+				NoPrefix: true,
+				ShortOpt: "x",
+				Advanced: true,
+			},
+			{
+				Name: "case_sensitive",
+				Help: `Force the filesystem to report itself as case sensitive.
 
 Normally the local backend declares itself as case insensitive on
 Windows/macOS and case sensitive for everything else.  Use this flag
 to override the default choice.`,
-			Default:  false,
-			Advanced: true,
-		}, {
-			Name: "case_insensitive",
-			Help: `Force the filesystem to report itself as case insensitive.
+				Default:  false,
+				Advanced: true,
+			},
+			{
+				Name: "case_insensitive",
+				Help: `Force the filesystem to report itself as case insensitive.
 
 Normally the local backend declares itself as case insensitive on
 Windows/macOS and case sensitive for everything else.  Use this flag
 to override the default choice.`,
-			Default:  false,
-			Advanced: true,
-		}, {
-			Name: "no_preallocate",
-			Help: `Disable preallocation of disk space for transferred files.
+				Default:  false,
+				Advanced: true,
+			},
+			{
+				Name: "no_clone",
+				Help: `Disable reflink cloning for server-side copies.
+
+Normally, for local-to-local transfers, rclone will "clone" the file when
+possible, and fall back to "copying" only when cloning is not supported.
+
+Cloning creates a shallow copy (or "reflink") which initially shares blocks with
+the original file. Unlike a "hardlink", the two files are independent and
+neither will affect the other if subsequently modified.
+
+Cloning is usually preferable to copying, as it is much faster and is
+deduplicated by default (i.e. having two identical files does not consume more
+storage than having just one.)  However, for use cases where data redundancy is
+preferable, --local-no-clone can be used to disable cloning and force "deep" copies.
+
+Currently, cloning is only supported when using APFS on macOS (support for other
+platforms may be added in the future.)`,
+				Default:  false,
+				Advanced: true,
+			},
+			{
+				Name: "no_preallocate",
+				Help: `Disable preallocation of disk space for transferred files.
 
 Preallocation of disk space helps prevent filesystem fragmentation.
 However, some virtual filesystem layers (such as Google Drive File
 Stream) may incorrectly set the actual file size equal to the
 preallocated space, causing checksum and file size checks to fail.
 Use this flag to disable preallocation.`,
-			Default:  false,
-			Advanced: true,
-		}, {
-			Name: "no_sparse",
-			Help: `Disable sparse files for multi-thread downloads.
+				Default:  false,
+				Advanced: true,
+			},
+			{
+				Name: "no_sparse",
+				Help: `Disable sparse files for multi-thread downloads.
 
 On Windows platforms rclone will make sparse files when doing
 multi-thread downloads. This avoids long pauses on large files where
 the OS zeros the file. However sparse files may be undesirable as they
 cause disk fragmentation and can be slow to work with.`,
-			Default:  false,
-			Advanced: true,
-		}, {
-			Name: "no_set_modtime",
-			Help: `Disable setting modtime.
+				Default:  false,
+				Advanced: true,
+			},
+			{
+				Name: "no_set_modtime",
+				Help: `Disable setting modtime.
 
 Normally rclone updates modification time of files after they are done
 uploading. This can cause permissions issues on Linux platforms when 
 the user rclone is running as does not own the file uploaded, such as
 when copying to a CIFS mount owned by another user. If this option is 
 enabled, rclone will no longer update the modtime after copying a file.`,
-			Default:  false,
-			Advanced: true,
-		}, {
-			Name: "time_type",
-			Help: `Set what kind of time is returned.
+				Default:  false,
+				Advanced: true,
+			},
+			{
+				Name: "time_type",
+				Help: `Set what kind of time is returned.
 
 Normally rclone does all operations on the mtime or Modification time.
 
@@ -255,27 +292,29 @@ will silently replace it with the modification time which all OSes support.
 Note that setting the time will still set the modified time so this is
 only useful for reading.
 `,
-			Default:  mTime,
-			Advanced: true,
-			Examples: []fs.OptionExample{{
-				Value: mTime.String(),
-				Help:  "The last modification time.",
-			}, {
-				Value: aTime.String(),
-				Help:  "The last access time.",
-			}, {
-				Value: bTime.String(),
-				Help:  "The creation time.",
-			}, {
-				Value: cTime.String(),
-				Help:  "The last status change time.",
-			}},
-		}, {
-			Name:     config.ConfigEncoding,
-			Help:     config.ConfigEncodingHelp,
-			Advanced: true,
-			Default:  encoder.OS,
-		}},
+				Default:  mTime,
+				Advanced: true,
+				Examples: []fs.OptionExample{{
+					Value: mTime.String(),
+					Help:  "The last modification time.",
+				}, {
+					Value: aTime.String(),
+					Help:  "The last access time.",
+				}, {
+					Value: bTime.String(),
+					Help:  "The creation time.",
+				}, {
+					Value: cTime.String(),
+					Help:  "The last status change time.",
+				}},
+			},
+			{
+				Name:     config.ConfigEncoding,
+				Help:     config.ConfigEncodingHelp,
+				Advanced: true,
+				Default:  encoder.OS,
+			},
+		},
 	}
 	fs.Register(fsi)
 }
@@ -296,6 +335,7 @@ type Options struct {
 	NoSetModTime      bool                 `config:"no_set_modtime"`
 	TimeType          timeType             `config:"time_type"`
 	Enc               encoder.MultiEncoder `config:"encoding"`
+	NoClone           bool                 `config:"no_clone"`
 }
 
 // Fs represents a local filesystem rooted at root
@@ -384,6 +424,10 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
 	if opt.FollowSymlinks {
 		f.lstat = os.Stat
 	}
+	if opt.NoClone {
+		// Disable server-side copy when --local-no-clone is set
+		f.features.Copy = nil
+	}
 
 	// Check to see if this points to a file
 	fi, err := f.lstat(f.root)
diff --git a/go.mod b/go.mod
index 7c1623df01590..c5287446eb5a1 100644
--- a/go.mod
+++ b/go.mod
@@ -28,6 +28,7 @@ require (
 	github.com/gabriel-vasile/mimetype v1.4.4
 	github.com/gdamore/tcell/v2 v2.7.4
 	github.com/go-chi/chi/v5 v5.1.0
+	github.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348
 	github.com/go-git/go-billy/v5 v5.5.0
 	github.com/google/uuid v1.6.0
 	github.com/hanwen/go-fuse/v2 v2.5.1
diff --git a/go.sum b/go.sum
index f0aaa8f62ad3f..c5a5a5efb9748 100644
--- a/go.sum
+++ b/go.sum
@@ -189,6 +189,8 @@ github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
 github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
 github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
 github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348 h1:JnrjqG5iR07/8k7NqrLNilRsl3s1EPRQEGvbPyOce68=
+github.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348/go.mod h1:Czxo/d1g948LtrALAZdL04TL/HnkopquAjxYUuI02bo=
 github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
 github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=


Test patch:
diff --git a/backend/union/union_internal_test.go b/backend/union/union_internal_test.go
index b6f78ec8d4900..ccc130e8f6cda 100644
--- a/backend/union/union_internal_test.go
+++ b/backend/union/union_internal_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"fmt"
+	"runtime"
 	"testing"
 	"time"
 
@@ -95,6 +96,12 @@ func TestMoveCopy(t *testing.T) {
 	fLocal := unionFs.upstreams[0].Fs
 	fMemory := unionFs.upstreams[1].Fs
 
+	if runtime.GOOS == "darwin" {
+		// need to disable as this test specifically tests a local that can't Copy
+		f.Features().Disable("Copy")
+		fLocal.Features().Disable("Copy")
+	}
+
 	t.Run("Features", func(t *testing.T) {
 		assert.NotNil(t, f.Features().Move)
 		assert.Nil(t, f.Features().Copy)
diff --git a/fs/operations/copy_test.go b/fs/operations/copy_test.go
index 73aa5e05a6443..1e8f3ae5e7ba2 100644
--- a/fs/operations/copy_test.go
+++ b/fs/operations/copy_test.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"os"
 	"path"
+	"runtime"
 	"sort"
 	"strings"
 	"testing"
@@ -449,6 +450,14 @@ func TestCopyFileMaxTransfer(t *testing.T) {
 	ci.MaxTransfer = sizeCutoff
 	ci.CutoffMode = fs.CutoffModeHard
 
+	if runtime.GOOS == "darwin" {
+		// disable server-side copies as they don't count towards transfer size stats
+		r.Flocal.Features().Disable("Copy")
+		if r.Fremote.Features().IsLocal {
+			r.Fremote.Features().Disable("Copy")
+		}
+	}
+
 	// file1: Show a small file gets transferred OK
 	accounting.Stats(ctx).ResetCounters()
 	err = operations.CopyFile(ctx, r.Fremote, r.Flocal, file1.Path, file1.Path)
diff --git a/fs/sync/sync_test.go b/fs/sync/sync_test.go
index 2d221e492b26e..f1c07921e7788 100644
--- a/fs/sync/sync_test.go
+++ b/fs/sync/sync_test.go
@@ -1382,6 +1382,12 @@ func testSyncWithMaxDuration(t *testing.T, cutoffMode fs.CutoffMode) {
 	r.CheckLocalItems(t, file1, file2)
 	r.CheckRemoteItems(t)
 
+	if runtime.GOOS == "darwin" {
+		r.Flocal.Features().Disable("Copy") // macOS cloning is too fast for this test!
+		if r.Fremote.Features().IsLocal {
+			r.Fremote.Features().Disable("Copy") // macOS cloning is too fast for this test!
+		}
+	}
 	accounting.GlobalStats().ResetCounters()
 	// ctx = predictDstFromLogger(ctx) // not currently supported (but tests do pass for CutoffModeSoft)
 	startTime := time.Now()
@@ -2569,6 +2575,14 @@ func TestMaxTransfer(t *testing.T) {
 		r.CheckLocalItems(t, file1, file2, file3)
 		r.CheckRemoteItems(t)
 
+		if runtime.GOOS == "darwin" {
+			// disable server-side copies as they don't count towards transfer size stats
+			r.Flocal.Features().Disable("Copy")
+			if r.Fremote.Features().IsLocal {
+				r.Fremote.Features().Disable("Copy")
+			}
+		}
+
 		accounting.GlobalStats().ResetCounters()
 
 		// ctx = predictDstFromLogger(ctx) // not currently supported