← The specification

Storage & hardlink management

Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page

Status: Accepted · Audience: Operator · Area: C — Trust & correctness


Purpose

Make the filesystem contract real, and make its violation visible.

P1 says downloads and media live under one mount so imports hardlink rather than copy. When that breaks, nothing announces it. Imports still succeed. The library still fills. The only symptoms are a disk consuming twice what it should and torrents that can’t seed from the library copy — both discovered late, usually when the disk is full.

This feature turns an invisible property into a checked, reported one.

Behaviour

Create a file in the data root, hardlink it, stat both, compare inode and link count, clean up. Filesystem type is a hint; a successful link is a fact.

The distinction matters because the exceptions are common: exFAT can’t hardlink at all, SMB mounts on macOS won’t expose them usably, and Windows’ WSL2 boundary breaks them for anything on the Windows side.

Storage mode follows from the result

Mode Condition Consequence
local Hardlinks work Imports link. Instant, free, seeding preserved.
external Hardlinks work on a removable or secondary volume Same, plus availability monitoring
nas Hardlinks unavailable across a network mount Imports copy. *arrs configured accordingly.
degraded Expected to link but no longer can Alert — something changed

Mode is derived from evidence, not chosen from a menu. The operator picks a location; lemonfiber determines what that location can do.

The location is one setting, DATA_ROOT: a path on this machine, or an NFS export written nfs://<server>/<export>. Every service that takes data is mounted from it alike, by the generator rather than by an overlay (plugin-manifest, ARCH-R240).

Degradation is stated in consequences, not properties

“Hardlinks unsupported” means nothing to most operators. What it means is:

Imports will copy instead of link. Each import takes minutes rather than being instant, uses twice the disk while it runs, and torrents can’t seed from the library copy — you’d need to keep both.

Then the options: choose a different location, or continue in copy mode with the *arrs configured to match.

The single-mount rule is enforced structurally

Every container receives one data mount with subdirectories beneath it. Splitting /downloads and /media into separate mounts is the anti-pattern (ADR-0006) and is rejected at manifest validation — not left to be discovered in production.

Space is projected, not just reported

Free space alone is insufficient. What matters is free space against the size of what’s queued. Projected exhaustion is computed and alerted before it happens, because a disk that fills mid-import leaves partial files and a stalled queue.

Availability is monitored, not assumed

External drives get unplugged; network mounts drop. A data root that vanishes while services run is dangerous — the *arrs may write into the now-empty mount point, creating a phantom library on the system disk. Detect it and stop.

Ownership and permissions are checked

Files the operator can’t read, or that services can’t write, produce failures far from their cause. Checked directly, with the platform’s actual semantics — real on native Linux, largely mapped away on Docker Desktop.

On the companion

Degradation is stated in consequences, not properties is the argument the companion applies everywhere, and this is where it is first made. N12-R3 requires what removing a candidate would cost shown with the candidate (N12), and the storage mode is what determines that cost: where imports link, a seeding copy and a library copy are one set of bytes and removing one frees nothing; where they copy, the same act frees the whole file. Hardlinks unsupported is a property nobody can act on from a phone. Removing this frees 14 GB and removing this frees nothing are the same fact stated as a consequence, and only the second form survives the trip.

Availability is monitored, not assumed is N12-R10: space that could not be read is told apart from space that is comfortable. A data root that vanished and a data root with room to spare both arrive as an absence of warning, and the app is the surface least able to tell which one it is looking at — the drive is in another room, and the phone is not.

States

State Meaning
healthy Present, writable, hardlinks working, adequate space
copy-mode Present and writable; hardlinks unavailable and configured for
degraded Previously linking, now not
space-critical Full or projected to fill imminently
unavailable Data root not present
permission-denied Present but not usable by the operator or services

Edge cases

Situation Behaviour
exFAT volume chosen Detect; explain hardlinks are impossible on exFAT and that this cannot be worked around.
SMB mount on macOS Detect; explain the degradation and recommend NFS where available.
Data root is an NFS export Report nas mode from the setting itself; say that the checks reading a host path (hardlinks, space, presence) are not run on this machine rather than report them as passed (ARCH-R240).
Windows path outside WSL2 Detect the boundary crossing; explain that the data root should live inside the WSL2 filesystem.
Hardlinks worked, now don’t degraded and alert. Usually a remount with different options.
Data root disappears while running Stop services rather than let them write into an empty mount point.
Data root reappears Report; do not auto-start. The operator decides whether state is trustworthy.
Free space adequate, projection exhausts it Alert on the projection, not just the current figure.
Space reclaimed by another process Re-evaluate rather than staying alerted on stale data.
Permissions correct for operator, wrong for containers Distinguish the two. Different causes, different remedies.
Case-insensitive filesystem Detect; some libraries have names differing only by case.
Path contains characters a service can’t handle Detect at selection, not at first import.
Data root inside a Docker volume Detect; explain reduced host visibility and backup implications.
Symlinked data root Resolve it; hardlink tests must run against the real filesystem.

Acceptance criteria

ID Requirement
C5-R1 Hardlink capability MUST be determined by creating and inspecting a hardlink, never inferred from filesystem type.
C5-R2 Storage mode MUST be derived from the observed result, not selected by the operator.
C5-R3 Loss of hardlink capability MUST be expressed as consequences — import time, disk usage, seeding — not as a filesystem property.
C5-R4 Where hardlinks are unavailable, the *arrs MUST be configured to copy, and this MUST be stated.
C5-R5 A manifest declaring more than one mount beneath the data root MUST fail validation.
C5-R6 Projected space exhaustion MUST be computed from queued content and alerted before it occurs.
C5-R7 Data root availability MUST be monitored while services run.
C5-R8 Loss of the data root MUST stop dependent services rather than allow writes into an empty mount point.
C5-R9 A reappearing data root MUST NOT trigger automatic restart.
C5-R10 Operator-facing and service-facing permission problems MUST be reported distinctly.
C5-R11 Loss of previously working hardlink capability MUST be detected and alerted.
C5-R12 The data root path MUST be validated for service compatibility at selection time.
C5-R13 A symlinked data root MUST be resolved before capability testing.
C5-R14 Platform-specific hardlink limitations — exFAT, SMB on macOS, the WSL2 boundary — MUST be detected and named specifically.