Files
felhom.eu/documentation/audits/media-all-drives-2026-10-14/tools/make_drill.py
T
admin 8395aee4c2
gates / gates (push) Successful in 7m46s
Media all drives, Parts A-C on Tester 1: S1-S3 proven (hardlink with one bind per drive; today's shape copies); R-1014..R-1019 filed
Two test drives enrolled through the dashboard; no test media in any backup tier (A6, three proofs); Tester 1 follows a
credential-free local mirror of the drill catalog (R-992 worked around a third way). The live-fix task may start.
2026-10-14 17:36:50 +02:00

85 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""make_drill.py — Part B2: rewrite the six media templates IN THE DRILL CATALOG to the layout under test.
The layout (one bind per drive, the same path inside the container as in the guest, `userdata/media` only):
/mnt/felhom-drives/<drive>/userdata/media -> /mnt/felhom-drives/<drive>/userdata/media (rw; :ro for Plex/Jellyfin)
for <drive> in DRIVES. Images are NOT touched. Plain host paths on purpose: the controller's bind classifier knows only
the ${..._PATH} roots (stacks/classify_binds.go classifyRoot), so these binds are in no app capture set (A6).
Because the ${USERDATA_PATH} binds go away, the `.felhom.yml` `backup.userdata` entries and `data_paths` that named them
are removed too — appbackup.ValidateBackupSpec rejects an entry that matches no compose bind, and a rejected block
degrades the app to the legacy path. qBittorrent's and Transmission's `backup.volumes` (their config) stay.
Default save paths: drive A's `media/torrents` (qBittorrent, seeded on first boot) and `media/transmission/complete`
(Transmission, set by its entrypoint on a fresh config only). Everything else is unchanged.
python3 make_drill.py [--check] # edits /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates in place
"""
import re, sys
D = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates"
DRIVES = ("media_a", "media_b")
M = {d: f"/mnt/felhom-drives/{d}/userdata/media" for d in DRIVES}
A = M["media_a"]
def binds(indent, ro):
suf = ":ro" if ro else ""
return "".join(f"{indent}- {M[d]}:{M[d]}{suf}\n" for d in DRIVES)
def sub1(s, old, new, app):
if old not in s:
sys.exit(f"{app}: expected text not found: {old!r}")
return s.replace(old, new, 1)
def compose(app):
p = f"{D}/{app}/docker-compose.yml"
s = open(p).read()
if M["media_a"] in s:
return f"{app}: compose already in the new layout"
i = " "
if app == "sonarr":
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/tv:/media/tv\n{i}- ${{USERDATA_PATH}}/downloads:/downloads\n", binds(i, False), app)
elif app == "radarr":
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/movies:/media/movies\n{i}- ${{USERDATA_PATH}}/downloads:/downloads\n", binds(i, False), app)
elif app == "qbittorrent":
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/torrents:/downloads\n", binds(i, False), app)
s = sub1(s, "'Session\\DefaultSavePath=/downloads'", f"'Session\\DefaultSavePath={A}/torrents'", app)
s = sub1(s, "'Session\\TempPath=/downloads/.temp'", f"'Session\\TempPath={A}/torrents/.temp'", app)
elif app == "transmission":
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/transmission:/downloads\n", binds(i, False), app)
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/transmission/watch:/watch\n", f"{i}- {A}/transmission/watch:/watch\n", app)
# the default download folders are set ONCE, on a fresh config (a household's later choice is kept)
s = sub1(s, """ cp /defaults/settings.json "$$CONF"\n""",
f""" cp /defaults/settings.json "$$CONF"\n jq '.["download-dir"] = "{A}/transmission/complete" | .["incomplete-dir"] = "{A}/transmission/incomplete"' "$$CONF" > /tmp/d.json && mv /tmp/d.json "$$CONF"\n""", app)
elif app in ("plex", "jellyfin"):
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media:/media:ro\n", binds(i, True), app)
if "${USERDATA_PATH}" in re.sub(r"^#.*$", "", s, flags=re.M):
sys.exit(f"{app}: a ${{USERDATA_PATH}} bind is left")
open(p, "w").write(s)
return f"{app}: compose rewritten"
def felhom_yml(app):
p = f"{D}/{app}/.felhom.yml"
s = open(p).read()
before = s
# drop backup.userdata (the list under it) — and the whole backup block when nothing else is left in it
s = re.sub(r"(^backup:\n(?: volumes:\n(?: .*\n| .*\n| .*\n)*)?) userdata:\n(?: .*\n| .*\n)+", r"\1", s, flags=re.M)
s = re.sub(r"^# --- Backup classification[^\n]*\n^backup:\n(?=\n|#|[a-z])", "", s, flags=re.M)
# drop data_paths (top level only; the i18n.en copy is display text and names no bind)
s = re.sub(r"^# --- Customer-facing folder annotation[^\n]*\n^data_paths:\n(?: .*\n)+", "", s, flags=re.M)
if s != before:
open(p, "w").write(s)
return f"{app}: .felhom.yml backup.userdata/data_paths removed"
return f"{app}: .felhom.yml unchanged"
for app in ("sonarr", "radarr", "qbittorrent", "transmission", "plex", "jellyfin"):
print(compose(app))
print(felhom_yml(app))