cowclone#
github.com/go-fsctl/cowclone
Pure-Go copy-on-write file cloning: share a file's blocks with a new
copy in O(metadata), falling back to a full byte copy when the host
filesystem can't — with no cgo and no shelling out to cp --reflink or
clonefile.
Unlike the rest of the family, which each drive one Linux subsystem's
ioctls, cowclone is cross-platform: it speaks the two portable reflink
primitives directly and degrades gracefully everywhere else.
- darwin — APFS
clonefile(2)(golang.org/x/sys/unix.Clonefile). - linux — the
FICLONEioctl (unix.IoctlFileClone): reflink on btrfs, XFS (reflink=1), and OpenZFS ≥ 2.2 block cloning. - everywhere else, across filesystems, or on a non-CoW filesystem — a streaming byte copy with identical observable semantics.
On a copy-on-write filesystem the clone is near-instant and shares
storage with the source until one side is written; the fallback produces
the same result — a full, independent dst — at the cost of copying
every byte. Either way the caller gets an independent dst.
Install#
Clone a file#
import "github.com/go-fsctl/cowclone"
// Make dst a copy-on-write clone of src (replacing dst if it exists).
// Tries a real reflink first; transparently falls back to a byte copy when
// the filesystem can't share blocks or src/dst are on different filesystems.
err := cowclone.Clone("base.img", "instance.img")
Clone(src, dst) is the whole surface:
- It removes any pre-existing
dst(a CoW clone fails if the target already exists), erroring only if that removal genuinely fails. - It attempts the platform reflink primitive. On success
dstis a block-sharing clone andClonereturnsnil. - If the primitive reports the filesystem can't share blocks
(
ENOTSUP/EOPNOTSUPP), or thatsrcanddstlive on different filesystems (EXDEV), or that the ioctl is unimplemented (ENOSYS), it falls back to a streaming byte copy. - Any other error (
ENOENT,EACCES,ENOSPC, …) is surfaced, wrapped, and not silently retried as a copy.
The result is observable-equivalent across all paths: after Clone,
writing to one file never affects the other.
Build tags#
| File | Builds on | Clone primitive |
|---|---|---|
clone_darwin.go |
darwin |
APFS clonefile(2) |
clone_linux.go |
linux |
FICLONE ioctl (reflink) |
clone_other.go |
everything else | always byte-copy fallback |
Testing without a reflink-capable mount#
Following the go-fsctl house style, the OS primitives are reached
through indirection seams (seams_linux.go, seams_darwin.go) — a
var over unix.Clonefile / unix.IoctlFileClone and the file opens —
so tests fault-inject each success and errno branch deterministically.
On a plain ext4 or tmpfs CI filesystem FICLONE only ever returns
EOPNOTSUPP, so neither the reflink success path nor the other errnos
would otherwise be reachable without root or a dedicated btrfs/xfs mount.
Every branch is covered on every platform, and the suite needs neither
root nor a special filesystem — so it runs identically on the native
(linux + macOS) and QEMU-emulated CI lanes.
Platforms#
Built and tested on the six 64-bit Go architectures — amd64, arm64,
riscv64, loong64, ppc64le, s390x (big-endian) — plus native
macOS (darwin/arm64) for the real APFS clonefile path, and a
cross-build check on darwin/amd64, windows and freebsd (the
fallback stub). 100% statement coverage throughout.