#!/bin/bash
#
# air-usb-recordings
#
# Optional helper that redirects /var/lib/cast/recordings to a USB
# stick when one is plugged in with the filesystem label `AIRREC`.
# Runs as a cast.service ExecStartPre so the redirect is in place
# before cast-server opens the recordings directory.
#
# # Why "by label" not "by UUID"
#
# Operators want plug-and-play: format the stick once with `mkfs.ext4
# -L AIRREC /dev/sdX1`, then any time it's plugged in, recordings
# flow there. UUID-based config would require updating /etc/fstab
# every time the operator swaps sticks.
#
# # Why ext4 and not vfat / exfat
#
# Recordings can grow past 4 GB during long missions. vfat caps a
# single file at 4 GB. exfat works but Pi OS Lite doesn't ship the
# kernel module; we'd need to apt install exfat-fuse and accept the
# FUSE performance penalty. ext4 is native, fast, no extra packages.
# The trade-off: the operator can't read the stick on a Windows
# machine without ext4 driver support (use Paragon's free reader,
# or copy off via the web UI's recordings download).
#
# # Idempotency / failure modes
#
# - No USB plugged in:        no-op, exit 0. Recordings stay on SD.
# - USB plugged but no AIRREC label: no-op, exit 0. Operator's other
#                             USB sticks aren't touched.
# - AIRREC stick already mounted at /mnt/usb: no-op, ensure symlink
#                             still in place, exit 0.
# - /var/lib/cast/recordings has files but isn't a symlink:
#                             refuse to replace (we don't move
#                             operator-recorded files silently),
#                             exit 0 with a journal warning.
# - Mount fails (corrupt FS, removed mid-boot, etc.): exit 0 with
#                             a journal warning. Recordings fall
#                             back to SD; cast-server boots clean.
#
# Always exits 0 — cast.service's ExecStartPre cannot block startup
# on a missing/optional storage device.

set -euo pipefail

LABEL="AIRREC"
MNT="/mnt/usb"
TARGET_DIR="/var/lib/cast/recordings"

log() { logger -t air-usb-recordings -- "$*"; echo "[air-usb-recordings] $*" >&2; }

# 1. Find the device. /dev/disk/by-label/ is populated by udev rules
#    that fire on block-device insertion. Symlink target = the
#    actual partition device (e.g. /dev/sda1).
DEV=""
if [ -L "/dev/disk/by-label/$LABEL" ]; then
    DEV=$(readlink -f "/dev/disk/by-label/$LABEL")
fi

if [ -z "$DEV" ]; then
    # No labelled device — recordings stay on SD. Quiet by default
    # (logger goes to journal at debug level via tag); operator who
    # wants to verify can run this script manually.
    exit 0
fi

# 2. Mount it if not already mounted. A double-mount on the same
#    target is a soft error in modern util-linux but not actually
#    harmful — `mountpoint -q` short-circuits the second call.
mkdir -p "$MNT"
if ! mountpoint -q "$MNT"; then
    # noatime: same SD-write reduction logic as the rootfs mount —
    # USB sticks have similar wear concerns but bigger cells.
    # nodev,nosuid: defence-in-depth. The recordings directory holds
    # MKV files only; nothing on this mount should ever be exec'd
    # or invoke setuid.
    if ! mount -t ext4 -o noatime,nodev,nosuid "$DEV" "$MNT" 2>/dev/null; then
        log "mount failed: $DEV → $MNT (corrupt FS? wrong filesystem?). recordings stay on SD."
        exit 0
    fi
    log "mounted $DEV → $MNT (label=$LABEL)"
fi

# 3. Ensure /mnt/usb/recordings exists and is owned by cast:cast so
#    cast-server (running as `cast`) can write into it.
USB_REC="$MNT/recordings"
mkdir -p "$USB_REC"
# chown only when needed — if the stick was provisioned on a system
# where the owner is already cast, this is already correct and the chown is
# a no-op SD write. Cheap to skip when not needed.
if [ "$(stat -c '%U:%G' "$USB_REC" 2>/dev/null || echo)" != "cast:cast" ]; then
    chown cast:cast "$USB_REC" 2>/dev/null || true
fi

# 4. Bind-mount $USB_REC over $TARGET_DIR. We use bind-mount instead
#    of a symlink because cast.service runs with `ProtectSystem=
#    strict` — its mount-namespace makes /mnt read-only from cast-
#    server's view, so any symlink resolving to /mnt/usb/* would
#    EROFS on write. A bind-mount keeps the path inside cast.
#    service's ReadWritePaths (=/var/lib/cast) but redirects the
#    storage backing to the USB. cast-server sees the same path,
#    writes succeed.
#
#    Bind-mount cases:
#    a) Target is empty regular dir → safe to bind-mount over.
#    b) Target is already bind-mounted from $USB_REC → no-op.
#    c) Target has SD-recorded files → REFUSE (operator's files
#       would be hidden by the bind-mount; surface the conflict).
mkdir -p "$TARGET_DIR"
# Already bind-mounted to the right source?
current_src=$(findmnt -n -o SOURCE --target "$TARGET_DIR" 2>/dev/null || true)
mnt_src=$(findmnt -n -o SOURCE --target "$MNT" 2>/dev/null || true)
if [ -n "$current_src" ] && [ "$current_src" = "$mnt_src" ]; then
    # Already in place (we previously bind-mounted $MNT/recordings,
    # which findmnt reports as the underlying device — same as $MNT
    # since recordings/ lives on the same filesystem).
    exit 0
fi
# Refuse if SD-side has unmigrated files. mountpoint -q tells us
# whether $TARGET_DIR is itself a mount; if not, ls reflects the
# underlying SD directory contents.
if ! mountpoint -q "$TARGET_DIR"; then
    if [ -n "$(ls -A "$TARGET_DIR" 2>/dev/null)" ]; then
        log "WARNING: $TARGET_DIR has SD-recorded files; refusing silent bind-mount. \
Move them to $USB_REC manually (sudo mv $TARGET_DIR/* $USB_REC/) then re-run this script."
        exit 0
    fi
fi
# Do the bind-mount. Failures here are non-fatal — recordings
# fall back to SD storage and cast-server boots clean.
if mount --bind "$USB_REC" "$TARGET_DIR" 2>/dev/null; then
    log "bind-mounted $USB_REC → $TARGET_DIR (recordings now on USB)"
else
    log "WARNING: bind-mount $USB_REC → $TARGET_DIR failed; recordings stay on SD"
fi

exit 0
