Files
alembic/pipeline/lib/navidrome-scan.sh
T
andrew b135f11557 Security hardening and first-run/portability improvements
Security (P0):
- Remove committed session-secret default; auto-generate and persist a
  random secret to the config volume when SESSION_SECRET is unset
  (prevents forgeable session cookies / auth bypass).
- Validate playlist names to a safe charset and render sldl configs via
  literal Python substitution instead of sed (closes a command-injection
  and path-traversal path through playlist names).
- shlex-quote credential values written to shell-sourced env files, and
  strip newlines from values patched into .conf files.
- Render playlist .conf files 0600; warn at startup if the master key is
  co-located with the config volume; document keeping it separate.

Portability:
- Configurable timezone via TZ (default UTC) instead of hardcoded Edmonton.
- Remove personal defaults (navidrome user "andrew", ephemeral.club URLs).
- Ship generic example seeds; move the cross-album dedup keep-list and the
  legacy playlist import to editable config files; drop the personal
  _upgrade.csv.
- Generic VPN reference in docker-compose.snippet.yml.

First-run experience:
- Redirect to /setup instead of 500 when OIDC is unconfigured; surface a
  missing master key inline; entrypoint exits with an actionable message
  when the config folder is not writable.
- Add unauthenticated /health (JSON) and /setup (checklist) diagnostics.

Docs:
- Write docs/ARCHITECTURE.md and docs/MIGRATION.md (previously referenced
  but missing); expand README with ownership, backups, advanced settings,
  and migration guidance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 14:25:55 -06:00

61 lines
2.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# navidrome-scan.sh — trigger a Navidrome full scan ONLY if the music library
# share is healthy.
#
# Why this exists: with ND_SCANNER_PURGEMISSING=always (which we keep, so
# on-disk deletions propagate to playlists and out to Traktor), a full scan
# that runs while the QNAP NFS share is unavailable makes Navidrome see the
# entire library as "missing" and purge it — wiping every playlist. That is
# exactly what happened on 2026-06-01 and lost the manual dj-* playlists.
#
# This wrapper refuses to trigger the scan unless the share is mounted, a
# canary file is readable, and the library has a sane number of artist dirs.
# Skipping a scan is harmless (the next healthy run picks up changes); running
# one against a dead mount is catastrophic.
set -euo pipefail
: "${MUSIC_DATA_DIR:=/data/music}"
: "${ALEMBIC_CONFIG_DIR:=/config}"
NAVIDROME_ENV="$ALEMBIC_CONFIG_DIR/pipeline/navidrome/admin.env"
[ -f "$NAVIDROME_ENV" ] && source "$NAVIDROME_ENV"
LIB="$MUSIC_DATA_DIR/Library"
CANARY="$LIB/.navidrome-canary"
MIN_ARTIST_DIRS=500 # library has ~1500; 500 is "clearly not wiped"
ND="${ND_BASE:-http://navidrome:4533}"
ND_USER="${ND_USER:-}"
ND_PASS="${ND_PASS:-}"
log() { echo "[$(date -Iseconds)] navidrome-scan: $*"; }
# `mountpoint -q` on the host detected an unmounted/dead NFS share directly.
# Running inside a container against a bind-mounted view, that check doesn't
# apply (a bind mount has no distinct device number to detect) — the canary
# file + artist-dir-count checks below are what actually carry the
# 2026-06-01 protection now: an unhealthy/empty share fails both of them.
# (Confirmed as an intentional adaptation, not a silent drop — see alembic
# migration plan / MIGRATION.md.)
if [ ! -r "$CANARY" ]; then
log "ABORT: canary $CANARY missing/unreadable — share degraded — skipping scan"
exit 0
fi
n=$(find "$LIB" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l)
if [ "$n" -lt "$MIN_ARTIST_DIRS" ]; then
log "ABORT: only $n artist dirs under $LIB (< $MIN_ARTIST_DIRS) — library looks truncated — skipping scan"
exit 0
fi
log "library healthy ($n artist dirs) — triggering full scan"
resp=$(curl -s -G "$ND/rest/startScan.view" \
--data-urlencode "u=$ND_USER" --data-urlencode "p=$ND_PASS" \
--data-urlencode 'v=1.16.0' --data-urlencode 'c=cron' \
--data-urlencode 'f=json' --data-urlencode 'fullScan=true' || true)
# Don't log "scan triggered" on faith: a rotated password or Navidrome error
# would otherwise report success forever while no scan ever runs.
if echo "$resp" | grep -q '"status":"ok"'; then
log "scan triggered"
else
log "ERROR: scan trigger failed — response: ${resp:-<empty/no response>}"
exit 1
fi