Skip to main content
Spacedrive treats the native filesystem as the substrate for everything above it. Detection, capacity reporting, copy-on-write, visibility filtering, and same-storage checks all have filesystem-specific behavior because the abstractions leak: df lies about ZFS pool sizes, APFS volumes share containers, Btrfs subvolumes look independent but aren’t, and Windows mount points rename themselves. This page documents what Spacedrive knows about each filesystem, how it detects them, and where the abstraction boundaries are.

Support Matrix

FilesystemPlatformCoW / ClonesPool-awareVisibility filterCapacity correction
APFSmacOS, iOSyes (clonefile)yes (containers)yes (system volumes)no
BtrfsLinuxyes (reflink)yes (subvolumes)yes (via Linux rules)no
ZFSLinuxyes (reflink on recent ZoL)yes (pools)yes (system pools, apps)yes (pool root)
ReFSWindowsyes (block clone)nonono
NTFSWindowsnononono
ext2/3/4Linuxnonoyes (via Linux rules)no
XFSLinuxnonoyes (via Linux rules)no
FAT32, exFATallnononono
HFS+macOSnonoyes (system volumes)no
“CoW / Clones” means std::fs::copy and the FastCopyStrategy produce metadata-only copies when source and destination are on the same filesystem. Everything else falls back to LocalStreamCopyStrategy which streams bytes with progress reporting.

Detection

Volume detection runs at startup and on mount/unmount events. Each platform uses a different primary source:

macOS (core/src/volume/platform/macos.rs)

Primary: diskutil apfs list — gives APFS container topology, volume roles (Data, System, VM, Preboot, Recovery, Update), and mount points. Containers group volumes that share physical storage and space (ApfsContainer). Fallback: df -h -T for non-APFS volumes (HFS+, external FAT32, etc.). Classification:
  • /, /System/Volumes/Data, /System/Volumes/Preboot etc. are system roles — hidden from user-visible view but still fingerprinted.
  • /Volumes/* that aren’t system roles are External.

Linux (core/src/volume/platform/linux.rs)

Primary: df -h -T — one line per mounted filesystem with device, type, size, available, mount point. Secondary: /sys/block/<device>/queue/rotational to distinguish SSD from HDD. /proc/mounts is also parseable via parse_proc_mounts() as an alternative source. ZFS datasets get a second pass via zfs list -H -o name,mountpoint,used,available,type -t filesystem to enrich each volume with dataset/pool information (see Capacity Reporting below).

Windows (core/src/volume/platform/windows.rs)

Uses Win32 APIs via windows-sys:
  • GetLogicalDrives to enumerate drive letters.
  • GetVolumeInformationW for filesystem type and volume label.
  • GetDiskFreeSpaceExW for capacity.
  • GetVolumeNameForVolumeMountPointW for the stable \\?\Volume{GUID}\ path — used as a hardware identifier that survives drive letter changes.

iOS (core/src/volume/platform/ios.rs)

Uses the macOS APFS code path but restricted to app-accessible volumes (sandboxed; detection is mostly informational).

FilesystemHandler trait

core/src/volume/fs/mod.rs defines a trait each filesystem implements:
#[async_trait]
pub trait FilesystemHandler: Send + Sync {
    /// Add filesystem-specific fields to a Volume (dataset info, container, subvolume, etc.)
    async fn enhance_volume(&self, volume: &mut Volume) -> VolumeResult<()>;

    /// Can these two paths use fast same-storage operations (clone/reflink)?
    async fn same_physical_storage(&self, path1: &Path, path2: &Path) -> bool;

    /// Copy strategy to use for this filesystem
    fn get_copy_strategy(&self) -> Box<dyn CopyStrategy>;

    /// Filesystem-specific contains-path check (accounts for datasets/subvolumes/etc.)
    fn contains_path(&self, volume: &Volume, path: &Path) -> bool;
}
get_filesystem_handler(FileSystem) returns the right implementation, falling back to GenericFilesystemHandler for anything unrecognized.

Per-filesystem details

APFS (core/src/volume/fs/apfs.rs)

  • Containers: APFS groups volumes into containers that share physical space. ApfsContainer is populated from diskutil apfs list and attached to each volume. same_physical_storage returns true when two paths are on volumes in the same container — that’s when clonefile(2) produces instant clones.
  • Firmlinks: macOS silently maps paths like /Users onto /System/Volumes/Data/Users. generate_macos_path_mappings() materializes these mappings so contains_path resolves correctly.
  • Role-based visibility: volumes with roles System, VM, Preboot, Recovery, Update are marked is_user_visible = false. Only Data and unroled external volumes appear in the default UI.

Btrfs (core/src/volume/fs/btrfs.rs)

  • Subvolumes: btrfs subvolume show <path> populates SubvolumeInfo. Subvolumes on the same Btrfs filesystem share storage.
  • Reflinks: same_physical_storage checks whether two paths share the top-level Btrfs filesystem via btrfs filesystem show. If yes, reflinks work between them.

ZFS (core/src/volume/fs/zfs.rs)

ZFS is the most-developed filesystem integration because TrueNAS Scale is a common Spacedrive server target.
  • Datasets and pools: zfs list output is parsed once per detection pass via fetch_zfs_list_output() (not per-volume — important for servers with many datasets). Each volume gets matched to its dataset via find_dataset_for_path, and the dataset’s pool is extracted from the name (pool/a/b → pool pool).
  • Pool root capacity correction: see Capacity Reporting.
  • System pool filter: is_system_zfs_pool matches boot-pool, rpool, zroot. Datasets on these pools are marked VolumeType::System, is_user_visible = false, and never auto-tracked.
  • App-managed dataset filter: is_app_managed_dataset matches names containing /ix-applications/, /.ix-apps/, /docker/, or /containerd/. These are hidden from user view. TrueNAS Scale apps create dozens of nested datasets per app — without this filter the volume list becomes unusable.
  • Clone support: supports_clones returns true for any read-write dataset. ZoL 2.2+ supports reflinks; older versions fall back to streaming copy.

ReFS (core/src/volume/fs/refs.rs)

  • Block cloning: checks for ReFS integrity stream support via DeviceIoControl / FSCTL_DUPLICATE_EXTENTS_TO_FILE. Sets supports_block_cloning on the volume.
  • Version gating: ReFS 3.x supports block cloning; 2.x doesn’t. The handler feature-detects rather than version-checks.

NTFS (core/src/volume/fs/ntfs.rs)

No CoW primitive on NTFS, so get_copy_strategy returns LocalStreamCopyStrategy. The handler mainly exists to provide NTFS-aware same_physical_storage (compares Volume GUIDs, not drive letters).

Generic (core/src/volume/fs/generic.rs)

Fallback for ext2/3/4, XFS, FAT32, exFAT, HFS+, and anything unrecognized. same_physical_storage compares mount point roots. Copy strategy is always LocalStreamCopyStrategy.

Visibility rules

Spacedrive tracks far more volumes than it shows. Hidden volumes still get stable fingerprints so locations on them survive remounts, but they don’t clutter the default UI and aren’t eligible for auto-tracking. Two flags drive this:
  • is_user_visible: bool — shown in the default volume list.
  • auto_track_eligible: bool — picked up by volumes.scan. Always implies is_user_visible.

Linux rules (core/src/volume/utils.rs)

is_virtual_filesystem(fs_type) drops anything backed by kernel memory: tmpfs, proc, sysfs, devtmpfs, cgroup, cgroup2, squashfs, efivarfs, overlay, fuse, and ~20 more. These are hidden even before classification. is_system_mount_point(path) matches Linux OS paths:
  • Exact: /, /usr, /var, /etc, /opt, /srv, /root, /boot, /home, /run, /dev, /proc, /sys, /tmp, /audit, /data, /conf, /mnt, /lost+found.
  • Prefixes: /boot/, /sys/, /proc/, /dev/, /run/, /var/log, /var/db/, /var/lib/systemd, /var/local/, /var/cache/.
The exact-match list includes TrueNAS Scale’s split-root datasets (it mounts /usr, /var, /etc as separate ZFS datasets for atomic OS updates). is_nested_app_mount(path) matches container/app mounts:
  • Anything under ix-applications/ or .ix-apps/ (TrueNAS apps — one app creates dozens of datasets).
  • docker/overlay2/, containerd/, kubelet/, snap/.
  • .snapshots/, .zfs/snapshot/ (ZFS snapshot browsing mounts).
should_hide_by_mount_path(path) is the combined check. It’s applied at:
  1. Detection — so newly-discovered volumes get is_user_visible = false persistently.
  2. Volume list query (core/src/ops/volumes/list/query.rs) — retroactively for tracked volumes whose DB rows predate these filters.
  3. Stats calculation (core/src/library/mod.rs) — so total_capacity and available_capacity exclude hidden volumes even if the DB flag is stale.

ZFS-specific rules

Applied during ZFS enhancement after should_hide_by_mount_path:
  • Datasets on is_system_zfs_pool pools (boot-pool, rpool, zroot) → hidden + VolumeType::System.
  • Datasets matching is_app_managed_dataset → hidden.

macOS rules

APFS role-based: System, VM, Preboot, Recovery, Update roles are hidden. Also /System/Volumes/* except /System/Volumes/Data is hidden by path.

Capacity reporting

The df-for-ZFS problem

df -T reports Size = used + available per mounted dataset. For a ZFS leaf dataset this is fine. For a ZFS pool root it’s misleading:
$ df -T /mnt/pool
Filesystem  Type  Size   Used  Available  Mount
pool        zfs   15.0T  199M  14.9T      /mnt/pool
The pool root’s own used is tiny (199 MB) because all the real data lives in descendant datasets. df doesn’t know that. On a 60 TB pool that’s 75% full, df says the pool root is “15 TB” — essentially just the free space. ZFS’s native used property on the pool root does include descendants:
$ zfs get used,available pool
pool  used       47.0T
pool  available  14.9T
47 T + 14.9 T ≈ 62 T = the real pool capacity after raidz2 parity.

Correction

enhance_volume_with_cached_output in zfs.rs detects pool-root volumes (dataset.name == dataset.pool_name) and overwrites total_capacity with used + available from zfs list. Leaf datasets keep their df-derived values — they’re accurate for single-dataset views.

Library statistics

calculate_volume_capacity (and _static) in core/src/library/mod.rs aggregates per-volume capacity with three passes:
  1. Filter by volume_type (Primary, UserData, External, Secondary).
  2. Filter by visibility (is_user_visible = true and !should_hide_by_mount_path(mount)).
  3. Deduplicate by fingerprint.
  4. Sort by mount-path length (shortest first).
  5. For each volume: skip if it’s a subpath of an already-counted volume on the same device; otherwise add its capacity to the running totals.
Subpath dedup handles the common ZFS case: when /mnt/pool is tracked along with /mnt/pool/footage and /mnt/pool/cctv, only /mnt/pool gets counted (once).

Pool-aware dedup limitation

Subpath dedup breaks if the user tracks only leaf datasets without the pool root. Each leaf reports the full available as its own — summing them over-counts by the pool’s free space per extra leaf. On TrueNAS this doesn’t bite because df always detects the pool root. For other setups, proper fix requires either persisting pool_name on the volume record or a second dedup pass keyed on (device_id, file_system=ZFS, available_capacity). Neither is implemented yet.

Copy strategies

core/src/ops/files/copy/strategy.rs defines three strategies:
  • LocalMoveStrategyfs::rename() for same-volume moves. Metadata-only.
  • FastCopyStrategystd::fs::copy() which invokes platform CoW primitives (clonefile on APFS, ficlone/FICLONERANGE on Btrfs/ZFS, block cloning on ReFS) when source and destination are on the same filesystem. Falls back to streaming if CoW fails.
  • LocalStreamCopyStrategy — chunked buffered copy with progress events. Used for cross-volume copies and for filesystems without CoW.
FilesystemHandler::get_copy_strategy picks FastCopyStrategy for APFS, Btrfs, ZFS, ReFS. Everything else gets LocalStreamCopyStrategy. Note that std::fs::copy itself picks the right syscall — the FastCopyStrategy/LocalStreamCopyStrategy split is about whether to try fast copy at all and how to report progress, not about which syscall to call. See File Copy Operations for the higher-level copy/move API.

Known limitations

  • Leaf-only ZFS dataset tracking — see Pool-aware dedup limitation.
  • Windows detection is shallow — we get capacity and FS type, but not the storage-pool topology that Storage Spaces / ReFS mirroring exposes. Same-pool detection across ReFS volumes isn’t implemented.
  • Btrfs subvolume visibility — we detect subvolumes but don’t hide nested subvolumes created by Docker or snapper. Equivalent to ZFS is_app_managed_dataset would need a similar name-based filter.
  • Network filesystems (NFS, SMB) — treated as MountType::Network but no protocol-aware capacity or CoW handling. Available comes from whatever the server reports via statvfs.
  • Encrypted volumes (LUKS, FileVault, BitLocker) — opaque to us once mounted; they appear as whatever filesystem is layered on top.