#!/bin/bash
# Pre-fwupdate hook: pre-downgrade UDC schema marker if the incoming
# firmware ships an older UDC than the running one.
#
# Why: UDC schema migrations live in the running binary. When FW image
# swap replaces the running UDC with an older binary, the older binary
# lacks the Downgrade code for any v_X that came after its release. The
# only point at which Downgrade is possible is right now, while the
# current (newer) binary is still on disk. After this hook returns,
# squashfs is swapped and the older binary takes over with a stale
# schema marker — the drift this hook exists to prevent.
#
# Contract: invoked by ubntnas during `system upgrade` (via
# /usr/lib/ubnt/hooks/system/upgrade-top) with these env vars:
#   FIRMWARE_PATH   absolute path to the incoming .bin
#   OLD_VERSION_RAW/SHORT, NEW_VERSION_RAW/SHORT   FW versions
# Hook runs BEFORE rootfs extraction (libubntnas/system.go:592 vs :677).
#
# We extract the rootfs squashfs ourselves, read the shipped
# /etc/default/unifi-drive-config.version, and if it is older than the
# running binary we invoke `udc migrate -t <target>` to walk the
# Downgrade chain while we still can.
#
# Failure-safe: any extraction, mount, or migration failure is logged
# and tolerated. We never propagate a non-zero exit to the hook runner —
# a buggy hook must not abort the firmware upgrade.
#
# Disk cost: extracting the rootfs squashfs writes ~620MB to a temp
# directory. /tmp on UNAS Pro is only 4G tmpfs and may already be
# holding a downloaded fw-image.bin (~680MB) plus a fw-updater binary,
# so /tmp can be too tight. We prefer /var/tmp (overlay-backed, ~14-22G
# free on emmc) and fall back to /tmp only if /var/tmp is unavailable.
# A free-space precheck skips the hook entirely rather than risk a
# half-extracted truncated squashfs giving us a garbage version string.
#
# A cleaner long-term design would have ubntnas expose the incoming
# UDC version as an env var (e.g. NEW_PACKAGE_VERSION_unifi_drive_config)
# so this hook avoids the extract entirely. See UD-XXXX.

set +e

LOG_TAG="udcd-fwupgrade"
log() { logger -t "$LOG_TAG" "$@"; echo "[$LOG_TAG] $*"; }

# Bytes required for the rootfs extract. squashfs from production
# images runs ~620MB; we round up and add headroom for inode overhead
# and any concurrent /tmp users.
REQUIRED_BYTES=$((900 * 1024 * 1024))

# Pick a tmpdir backed by enough free space. /var/tmp is preferred
# because it lives on the overlay (emmc, typically 14G+ free) rather
# than tmpfs. /tmp is a last resort and may be too small on UNAS Pro.
pick_tmpdir() {
    for candidate in /var/tmp /tmp; do
        [ -d "$candidate" ] || continue
        avail_kb=$(df -k -P "$candidate" 2>/dev/null | awk 'NR==2 {print $4}')
        [ -z "$avail_kb" ] && continue
        avail_bytes=$((avail_kb * 1024))
        if [ "$avail_bytes" -ge "$REQUIRED_BYTES" ]; then
            echo "$candidate"
            return 0
        fi
    done
    return 1
}

if [ -z "$FIRMWARE_PATH" ] || [ ! -f "$FIRMWARE_PATH" ]; then
    log "no FIRMWARE_PATH or file missing; skip"
    exit 0
fi

command -v udc >/dev/null || { log "udc not available; skip"; exit 0; }
command -v /sbin/fwextract >/dev/null || { log "fwextract not available; skip"; exit 0; }

TMPDIR_BASE=$(pick_tmpdir)
if [ -z "$TMPDIR_BASE" ]; then
    log "no tmpdir with $((REQUIRED_BYTES / 1024 / 1024))MB free; skip"
    exit 0
fi

WORK=$(mktemp -d -p "$TMPDIR_BASE" udcd-fwprobe.XXXXXX) || {
    log "mktemp under $TMPDIR_BASE failed; skip"
    exit 0
}
trap 'umount "$WORK/mnt" 2>/dev/null; rm -rf "$WORK"' EXIT

log "staging incoming rootfs under $WORK (free in $TMPDIR_BASE: $(df -h -P "$TMPDIR_BASE" | awk 'NR==2 {print $4}'))"

if ! /sbin/fwextract -o "$WORK/rootfs.bin" -t rootfs "$FIRMWARE_PATH" >/dev/null 2>&1; then
    log "fwextract failed; skip"
    exit 0
fi

mkdir -p "$WORK/mnt"
if ! mount -t squashfs -o loop,ro "$WORK/rootfs.bin" "$WORK/mnt" 2>/dev/null; then
    log "squashfs mount failed; skip"
    exit 0
fi

NEW_UDC_VER_FILE="$WORK/mnt/etc/default/unifi-drive-config.version"
if [ ! -f "$NEW_UDC_VER_FILE" ]; then
    log "incoming FW has no UDC version file; skip"
    exit 0
fi

NEW_UDC_VER=$(tr -d '[:space:]' < "$NEW_UDC_VER_FILE")
if [ -z "$NEW_UDC_VER" ]; then
    log "incoming UDC version is empty; skip"
    exit 0
fi

CURRENT_UDC_VER=$(udc version 2>/dev/null | tr -d '[:space:]')
if [ -z "$CURRENT_UDC_VER" ]; then
    log "current UDC version unreadable; skip"
    exit 0
fi

if ! dpkg --compare-versions "$NEW_UDC_VER" lt "$CURRENT_UDC_VER"; then
    log "incoming UDC $NEW_UDC_VER is not older than running $CURRENT_UDC_VER; skip"
    exit 0
fi

log "pre-downgrading UDC schema from $CURRENT_UDC_VER to $NEW_UDC_VER before FW swap"

# Pipe through logger but capture udc migrate's exit status, not
# logger's. Without PIPESTATUS[0] a failed migrate would log as
# "complete" — masking exactly the silent-drift symptom this hook
# exists to prevent.
udc migrate -t "$NEW_UDC_VER" 2>&1 | logger -t "$LOG_TAG"
if [ "${PIPESTATUS[0]}" -eq 0 ]; then
    log "schema pre-downgrade complete"
else
    log "udc migrate -t $NEW_UDC_VER failed (continuing — fwupdate will proceed)"
fi

exit 0
