docs(media): stat through the overlay can outlive the file it points at

Measured 2026-11-02 while a TV dedupe pass moved folders on the raw path: the
merged tree still answered stat at the old coordinates, link count 0, while ls of
the same parent no longer listed the name. Overlayfs keeps a dentry for a lower
file moved underneath it, so a tool that tests existence with [ -e ] against the
view can believe a file its own readdir denies — the media servers walk by
readdir, so this bites a script and never a user.

Recorded with it: every pass that moves or measures media walks /mnt/entropy, and
the one reader allowed to point at the view is the tool that needs show identity.
This commit is contained in:
Frank committed 2026-11-02 00:44:51 -08:00
1 parent 9f9043c037
commit 51ed5c0b2a
1 file changed
+15 -1
@@ -3,7 +3,7 @@ type: reference
title: Media Metadata View title: Media Metadata View
description: "The SSD-backed overlay the two media servers read and write: shape, guards, commands, laws, rollback." description: "The SSD-backed overlay the two media servers read and write: shape, guards, commands, laws, rollback."
tags: [dankserver, media-server, jellyfin, plex, storage] tags: [dankserver, media-server, jellyfin, plex, storage]
timestamp: 2026-11-01T01:00:00Z timestamp: 2026-11-02T01:00:00Z
--- ---
# Media Metadata View # Media Metadata View
@@ -134,6 +134,19 @@ the defaults are the host values above.
upper layer*, not for changes on the array underneath. After any relayout or upper layer*, not for changes on the array underneath. After any relayout or
externally-written metadata, trigger a library refresh — see externally-written metadata, trigger a library refresh — see
[[Media Relayout]] for the refresh procedure. [[Media Relayout]] for the refresh procedure.
- **`stat` through the view can outlive the file.** Measured 2026-11-02 while a TV
dedupe pass moved folders on the raw path: the merged tree still answered `stat`
at the old coordinates — link count `0`, and `ls` of the same parent no longer
listed the name. Overlayfs keeps a dentry for a lower file that was moved
underneath it, so a script that tests existence with `[ -e ]` against the view can
believe a file its own `readdir` denies. Prove existence by listing the parent.
The media servers walk by `readdir`, so ghosts never reach the library — this bites
a tool, never a user, which is why no server symptom accompanies it.
- **Tools that move or measure media walk the raw array, never the view** — every
relayout, prune and detritus pass, and every journal it writes. The view is for
Jellyfin's writes and Plex's reads only. One deliberate exception: `tvshow.nfo`
sidecars live in the upper layer, so a tool reading show identity points at the
view (`media-tvdupes.py --meta /var/lib/media-meta/view/TVShows`).
## Failure signatures ## Failure signatures
@@ -148,6 +161,7 @@ the defaults are the host values above.
| A media server's scan record reads `"Status": "Aborted"` | Its container restarted (a cutover, an image bump, an OOM) and the walk was discarded; systemd's `Restart=always` restarts the *server*, never the scan. Trigger a refresh — see [[Media Relayout]]. | | A media server's scan record reads `"Status": "Aborted"` | Its container restarted (a cutover, an image bump, an OOM) and the walk was discarded; systemd's `Restart=always` restarts the *server*, never the scan. Trigger a refresh — see [[Media Relayout]]. |
| The Jellyfin API answers 401 to every token header | Almost always the header **form**, not the key: 12.4 authenticates only `Authorization: MediaBrowser Token="…"` and refuses a bare `X-Emby-Token` for every key, valid or not — and one collapsed backslash in the curl config produces the same 401. `[[Jellyfin]]` owns the block that works. Before concluding anything, prove the key against the server's own table: `select count(*) from ApiKeys where AccessToken = ?` on `jellyfin.db` opened `mode=ro&immutable=1`. | | The Jellyfin API answers 401 to every token header | Almost always the header **form**, not the key: 12.4 authenticates only `Authorization: MediaBrowser Token="…"` and refuses a bare `X-Emby-Token` for every key, valid or not — and one collapsed backslash in the curl config produces the same 401. `[[Jellyfin]]` owns the block that works. Before concluding anything, prove the key against the server's own table: `select count(*) from ApiKeys where AccessToken = ?` on `jellyfin.db` opened `mode=ro&immutable=1`. |
| `systemctl status` finds no such unit | Look under the escaped name, `var-lib-media\x2dmeta-view.mount`, not the repo file name. | | `systemctl status` finds no such unit | Look under the escaped name, `var-lib-media\x2dmeta-view.mount`, not the repo file name. |
| Stale dentry through the view: `stat` succeeds with link count `0` while the parent's `ls` omits the name | The lower file was moved on the raw path after the view cached its dentry. Nothing is lost; existence through the view is proved by the parent listing. |
## Rollback ## Rollback