diff --git a/scripts/media-apply.py b/scripts/media-apply.py index cf7fe65..88fe766 100755 --- a/scripts/media-apply.py +++ b/scripts/media-apply.py @@ -32,6 +32,12 @@ cross a filesystem (rename cannot do it and a silent copy at 24 TB would not). --force relaxes only the guards that are opinion (journal already rolled back, paths outside --root); it never allows overwriting a file. +--metadata-view names the overlay the media servers read their library through +(default /var/lib/media-meta/view). The sidecar follow-up printed after an apply +or rollback is conditioned on that path being a mountpoint: where no view is +mounted the servers bind the array itself, the sidecars moved with the media, +and a reap order would point at a directory that is not there. + The journal defaults to `media-relayout.jsonl` BESIDE the library root, never inside it: a log inside the library is a file Jellyfin will index. """ @@ -114,13 +120,24 @@ def read_plan(path, root, force): return mkdirs, keeps, moves -# A relayout moves bytes on the raw array; the sidecars the media servers wrote -# live in the SSD overlay and stay at the old coordinates. Naming the follow-up -# here is the point of use for that fact — see the Media Metadata View page. +# The media servers may read the library through the SSD overlay view +# (scripts/media-view.sh). A relayout moves bytes on the raw array, so sidecars +# the servers wrote through that view stay at the old coordinates — naming the +# follow-up here is the point of use for that fact, but only where the view +# exists: where it is not mounted the servers bind the array itself, the +# sidecars moved with the media, and a reap order points at a directory that +# is not there. See the Media Metadata View page for the host's state. +METADATA_VIEW = "/var/lib/media-meta/view" METADATA_HINT = ("note: sidecars written through the metadata view did not follow" " these moves — next: scripts/media-view.sh reap") +def metadata_note(args): + """Print the sidecar follow-up only when the overlay is really mounted.""" + if os.path.ismount(args.metadata_view): + print(METADATA_HINT) + + def do_apply(args, root, journal, mkdirs, keeps, moves): """Preflight everything, then folders, then renames. Never a partial start.""" problems = [] @@ -202,7 +219,7 @@ def do_apply(args, root, journal, mkdirs, keeps, moves): f" {dirs_new} folders created, {len(keeps)} files stayed put") print(f"journal: {journal.path} — undo with: media-apply.py rollback" f" --journal {journal.path} --root {root} --apply") - print(METADATA_HINT) + metadata_note(args) return 0 @@ -386,7 +403,7 @@ def do_rollback(args, root, journal): t=datetime.datetime.now(datetime.timezone.utc).isoformat()) print(f"rolled back: {undone} files back where the census found them," f" {dirs_gone} empty folders removed, {recreated} swept folders recreated") - print(METADATA_HINT) + metadata_note(args) return 0 @@ -398,6 +415,9 @@ def main(): ap.add_argument("--journal") ap.add_argument("--apply", action="store_true", help="actually change the library") ap.add_argument("--force", action="store_true", help="relax the opinion guards") + ap.add_argument("--metadata-view", default=METADATA_VIEW, + help="overlay the media servers read through; the sidecar" + " note prints only when this path is a mountpoint") args = ap.parse_args() root = os.path.abspath(args.root) diff --git a/tests/media_relayout.bats b/tests/media_relayout.bats index b6afef0..3884a90 100755 --- a/tests/media_relayout.bats +++ b/tests/media_relayout.bats @@ -374,7 +374,10 @@ PY # apply_plan — plan, then apply for real; the journal goes in the tmpdir. apply_plan() { JOURNAL="$BATS_TEST_TMPDIR/journal.jsonl" - run python3 "$APPLY" apply --plan "$OUT" --root "$LIB" --journal "$JOURNAL" --apply + # `/` is always a mountpoint, so the overlay note behaves as it does on a + # host that runs the metadata view. + run python3 "$APPLY" apply --plan "$OUT" --root "$LIB" \ + --journal "$JOURNAL" --metadata-view / --apply } @test "a run without --apply says what it would do and touches nothing" { @@ -409,6 +412,32 @@ print(sum(1 for l in open(sys.argv[1]) if json.loads(l).get("op") == "move"))' " [ "$status" -eq 0 ] } +# The overlay note is only true where the metadata view is really mounted. On a +# host whose media servers bind the array directly (dankserver today: both +# quadlets mount /mnt/entropy, /var/lib/media-meta does not exist) the sidecars +# moved with the media, and "next: run reap" is an order that cannot be carried +# out. --metadata-view is how the caller names that path; the default is the +# host's view path, and the note rides only when it is a mountpoint. +@test "an apply with no mounted metadata view does not order a reap" { + plan + JOURNAL="$BATS_TEST_TMPDIR/journal.jsonl" + run python3 "$APPLY" apply --plan "$OUT" --root "$LIB" --journal "$JOURNAL" --apply + [ "$status" -eq 0 ] + [[ "$output" == *"moved"* ]] + [[ "$output" != *"media-view.sh reap"* ]] +} + +@test "a rollback with no mounted metadata view does not order a reap" { + plan + JOURNAL="$BATS_TEST_TMPDIR/journal.jsonl" + run python3 "$APPLY" apply --plan "$OUT" --root "$LIB" --journal "$JOURNAL" --apply + [ "$status" -eq 0 ] + run python3 "$APPLY" rollback --journal "$JOURNAL" --root "$LIB" --apply + [ "$status" -eq 0 ] + [[ "$output" == *"rolled back"* ]] + [[ "$output" != *"media-view.sh reap"* ]] +} + @test "a target already holding a different file stops the whole run" { plan JOURNAL="$BATS_TEST_TMPDIR/journal.jsonl"