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
Hardlink capability is tested, never inferred
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. |
Related
- ADR-0006 Single data mount
- A2 Setup wizard — where the test first runs
- A4 Reconfiguration — moving the data root
- C1 Diagnostics · D5 Disk space