Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions documentation/docs/features/cross-seed/hardlink-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Reflink mode creates copy-on-write clones of the matched files. Unlike hardlinks
### When to Use Reflink Mode

- You want to cross-seed torrents that hardlink mode would skip due to "extra files share pieces with content"
- Your filesystem supports copy-on-write clones (BTRFS, XFS on Linux; APFS on macOS)
- Your filesystem supports copy-on-write clones (BTRFS, XFS on Linux; APFS on macOS; ReFS on Windows)
- You prefer the safety of copy-on-write over hardlinks

### Reflink Requirements
Expand All @@ -99,7 +99,12 @@ Reflink mode creates copy-on-write clones of the matched files. Unlike hardlinks
- The filesystem must support reflinks:
- **Linux**: BTRFS, XFS (with reflink=1), and similar CoW filesystems
- **macOS**: APFS
- **Windows/FreeBSD**: Not currently supported
- **Windows**: ReFS on the same volume as the source files and reflink base directory
- **FreeBSD**: Not currently supported

:::note
Windows reflink mode uses ReFS block cloning (requiring an ReFS filesystem). NTFS is not supported. If reflink creation fails, fallback still depends on the existing "Fallback to regular mode" setting.
:::

:::tip
On Linux, check the filesystem type with `df -T /path` (you want `xfs`/`btrfs`, not `fuseblk`/`fuse.mergerfs`/`overlayfs`).
Expand Down
8 changes: 4 additions & 4 deletions pkg/reflinktree/reflink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ func TestSupportsReflink(t *testing.T) {

supported, reason := SupportsReflink(tmpDir)

// On unsupported platforms, should return false
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
// On unsupported platforms, should return false.
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" && runtime.GOOS != "windows" {
if supported {
t.Errorf("SupportsReflink should return false on %s", runtime.GOOS)
}
Expand All @@ -28,8 +28,8 @@ func TestSupportsReflink(t *testing.T) {
return
}

// On supported platforms, the result depends on filesystem
// We just verify the function doesn't panic and returns sensible values
// On supported platforms, the result depends on filesystem.
// We just verify the function doesn't panic and returns sensible values.
t.Logf("SupportsReflink(%s): supported=%v, reason=%s", tmpDir, supported, reason)

if !supported {
Expand Down
4 changes: 2 additions & 2 deletions pkg/reflinktree/reflink_unsupported.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copyright (c) 2025-2026, s0up and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later

//go:build !linux && !darwin
//go:build !linux && !darwin && !windows

package reflinktree

// SupportsReflink returns false on unsupported platforms.
// Windows and FreeBSD do not have a standard reflink mechanism that we support.
// FreeBSD do not have a standard reflink mechanism that we support.
func SupportsReflink(_ string) (supported bool, reason string) {
return false, "reflink is not supported on this operating system"
}
Expand Down
299 changes: 299 additions & 0 deletions pkg/reflinktree/reflink_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
// Copyright (c) 2025-2026, s0up and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later

//go:build windows

package reflinktree

import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"unsafe"

"golang.org/x/sys/windows"
)

const (
fsctlDuplicateExtentsToFile = 0x00098344
maxCloneChunkSize = 1024 * 1024 * 1024
copyBufferSize = 1024 * 1024
refsFilesystemName = "REFS"
)

type duplicateExtentsData struct {
FileHandle windows.Handle
SourceFileOffset int64
TargetFileOffset int64
ByteCount int64
}

var (
kernel32DLL = windows.NewLazySystemDLL("kernel32.dll")
procGetDiskFreeSpaceW = kernel32DLL.NewProc("GetDiskFreeSpaceW")
volumeRootForPathFn = getVolumeRoot
filesystemNameForVolFn = getFilesystemName
clusterSizeForVolFn = getClusterSize
duplicateExtentFn = duplicateExtent
copyFileTailFn = copyFileTail
)

// SupportsReflink tests whether the given directory supports reflinks
// by attempting an actual clone operation with temporary files.
// Returns true if reflinks are supported, along with a reason string.
func SupportsReflink(dir string) (supported bool, reason string) {
if err := os.MkdirAll(dir, 0o755); err != nil {
return false, fmt.Sprintf("cannot access directory: %v", err)
}

srcFile, err := os.CreateTemp(dir, ".reflink_probe_src_*")
if err != nil {
return false, fmt.Sprintf("cannot create temp file: %v", err)
}
srcPath := srcFile.Name()
defer os.Remove(srcPath)

if _, err := srcFile.WriteString("reflink probe test data"); err != nil {
srcFile.Close()
return false, fmt.Sprintf("cannot write to temp file: %v", err)
}
if err := srcFile.Close(); err != nil {
return false, fmt.Sprintf("cannot close temp file: %v", err)
}

dstPath := filepath.Join(dir, ".reflink_probe_dst_"+filepath.Base(srcPath)[len(".reflink_probe_src_"):])
defer os.Remove(dstPath)

if err := cloneFile(srcPath, dstPath); err != nil {
return false, fmt.Sprintf("reflink not supported: %v", err)
}

return true, "reflink supported (ReFS block cloning)"
}

func cloneFile(src, dst string) (retErr error) {
srcInfo, err := os.Stat(src)
if err != nil {
return fmt.Errorf("stat source: %w", err)
}

dstParent := filepath.Dir(dst)
if dstParent == "" {
dstParent = "."
}

volumeRoot, err := ensureSameVolume(src, dstParent)
if err != nil {
return err
}

clusterSize, err := ensureRefsVolume(volumeRoot)
if err != nil {
return err
}

srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("open source: %w", err)
}
defer srcFile.Close()

dstFile, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_EXCL, srcInfo.Mode())
if err != nil {
return fmt.Errorf("create destination: %w", err)
}
defer func() {
_ = dstFile.Close()
if retErr != nil {
_ = os.Remove(dst)
}
}()

if err := dstFile.Truncate(srcInfo.Size()); err != nil {
return fmt.Errorf("resize destination: %w", err)
}

srcHandle := windows.Handle(srcFile.Fd())
dstHandle := windows.Handle(dstFile.Fd())
cloneableSize := srcInfo.Size() - (srcInfo.Size() % clusterSize)

for offset := int64(0); offset < cloneableSize; offset += maxCloneChunkSize {
chunkSize := min(maxCloneChunkSize, cloneableSize-offset)
if err := duplicateExtentFn(dstHandle, srcHandle, offset, offset, chunkSize); err != nil {
return fmt.Errorf("duplicate extents: %w", err)
}
}

if tailSize := srcInfo.Size() - cloneableSize; tailSize > 0 {
if err := copyFileTailFn(srcFile, dstFile, cloneableSize, tailSize); err != nil {
return fmt.Errorf("copy file tail: %w", err)
}
}

return nil
}

func ensureSameVolume(src, dst string) (string, error) {
srcRoot, err := volumeRootForPathFn(src)
if err != nil {
return "", fmt.Errorf("get source volume: %w", err)
}

dstRoot, err := volumeRootForPathFn(dst)
if err != nil {
return "", fmt.Errorf("get destination volume: %w", err)
}

if !strings.EqualFold(srcRoot, dstRoot) {
return "", errors.New("source and destination must be on the same volume")
}

return srcRoot, nil
}

func ensureRefsVolume(volumeRoot string) (int64, error) {
filesystemName, err := filesystemNameForVolFn(volumeRoot)
if err != nil {
return 0, err
}

if !strings.EqualFold(filesystemName, refsFilesystemName) {
return 0, fmt.Errorf("volume %s is %s, not ReFS", volumeRoot, filesystemName)
}

clusterSize, err := clusterSizeForVolFn(volumeRoot)
if err != nil {
return 0, err
}
if clusterSize <= 0 {
return 0, errors.New("invalid cluster size")
}

return clusterSize, nil
}

func getVolumeRoot(path string) (string, error) {
absPath, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("abs path: %w", err)
}

volumePath := make([]uint16, windows.MAX_PATH+1)
pathPtr, err := windows.UTF16PtrFromString(absPath)
if err != nil {
return "", fmt.Errorf("convert path: %w", err)
}

if err := windows.GetVolumePathName(pathPtr, &volumePath[0], uint32(len(volumePath))); err != nil {
return "", fmt.Errorf("get volume path name: %w", err)
}

volumeRoot := windows.UTF16ToString(volumePath)
if !strings.HasSuffix(volumeRoot, `\`) {
volumeRoot += `\`
}

return volumeRoot, nil
}

func getFilesystemName(volumeRoot string) (string, error) {
volumePathPtr, err := windows.UTF16PtrFromString(volumeRoot)
if err != nil {
return "", fmt.Errorf("convert volume path: %w", err)
}

filesystemName := make([]uint16, windows.MAX_PATH+1)
var volumeSerial uint32
var maxComponentLength uint32
var flags uint32
if err := windows.GetVolumeInformation(
volumePathPtr,
nil,
0,
&volumeSerial,
&maxComponentLength,
&flags,
&filesystemName[0],
uint32(len(filesystemName)),
); err != nil {
return "", fmt.Errorf("get volume information: %w", err)
}

name := windows.UTF16ToString(filesystemName)
if name == "" {
return "", errors.New("filesystem name is empty")
}

return name, nil
}

func getClusterSize(volumeRoot string) (int64, error) {
volumePathPtr, err := windows.UTF16PtrFromString(volumeRoot)
if err != nil {
return 0, fmt.Errorf("convert volume path: %w", err)
}

var sectorsPerCluster uint32
var bytesPerSector uint32
var freeClusters uint32
var totalClusters uint32
r1, _, callErr := procGetDiskFreeSpaceW.Call(
uintptr(unsafe.Pointer(volumePathPtr)),
uintptr(unsafe.Pointer(&sectorsPerCluster)),
uintptr(unsafe.Pointer(&bytesPerSector)),
uintptr(unsafe.Pointer(&freeClusters)),
uintptr(unsafe.Pointer(&totalClusters)),
)
if r1 == 0 {
if callErr != nil && !errors.Is(callErr, windows.ERROR_SUCCESS) {
return 0, fmt.Errorf("get cluster size: %w", callErr)
}
return 0, errors.New("get cluster size: unknown error")
}

return int64(sectorsPerCluster) * int64(bytesPerSector), nil
}

func duplicateExtent(targetHandle, sourceHandle windows.Handle, sourceOffset, targetOffset, byteCount int64) error {
data := duplicateExtentsData{
FileHandle: sourceHandle,
SourceFileOffset: sourceOffset,
TargetFileOffset: targetOffset,
ByteCount: byteCount,
}

var bytesReturned uint32
return windows.DeviceIoControl(
targetHandle,
fsctlDuplicateExtentsToFile,
(*byte)(unsafe.Pointer(&data)),
uint32(unsafe.Sizeof(data)),
nil,
0,
&bytesReturned,
nil,
)
}

func copyFileTail(srcFile, dstFile *os.File, offset, length int64) error {
if _, err := srcFile.Seek(offset, io.SeekStart); err != nil {
return fmt.Errorf("seek source: %w", err)
}
if _, err := dstFile.Seek(offset, io.SeekStart); err != nil {
return fmt.Errorf("seek destination: %w", err)
}

buffer := make([]byte, copyBufferSize)
copied, err := io.CopyBuffer(dstFile, io.LimitReader(srcFile, length), buffer)
if err != nil {
return err
}
if copied != length {
return io.ErrUnexpectedEOF
}

return nil
}
Loading