#!/bin/bash
# Single source of truth for aligning volume ownership with the runtime user.
#
# Usage: fix-ownership [--dry-run] [--sentinel FILE] UID GID PATH [PATH...]
#
#   --dry-run    report what would change, touch nothing
#   --sentinel   skip entirely when FILE already records "SCHEMA:UID:GID";
#                write it after a successful run (used by the boot path so
#                multi-TB volumes are swept once per UID/schema change, not
#                on every boot)
#
# Only files whose uid OR gid differs are touched, so re-runs are cheap.
# Top-level /config additionally grants group frigate-data TRAVERSE ONLY
# (g+rx) so the separate go2rtc user can reach its pre-created HomeKit file
# on hosts where /config is mounted 0700. Never g+w: directory write means
# unlink rights over frigate.db/config.yml, and would let a compromised
# go2rtc plant /config/go2rtc, which the go2rtc run script executes
# preferentially, as root under the escape hatch.

set -o errexit -o nounset -o pipefail

# Permissions-layout epoch. Bump to force a one-time re-sweep on upgrade
# (e.g. when the privilege-drop release must capture files created as root
# since the previous sweep).
schema=1

dry_run=0
sentinel=""

while [[ "${1:-}" == --* ]]; do
    case "$1" in
        --dry-run) dry_run=1; shift ;;
        --sentinel)
            if [[ -z "${2:-}" ]]; then
                echo "[ERROR] fix-ownership: --sentinel requires a file argument" >&2
                exit 2
            fi
            sentinel="$2"; shift 2 ;;
        *) echo "[ERROR] fix-ownership: unknown option $1" >&2; exit 2 ;;
    esac
done

if [[ $# -lt 3 ]]; then
    echo "Usage: fix-ownership [--dry-run] [--sentinel FILE] UID GID PATH..." >&2
    exit 2
fi

target_uid="$1"
target_gid="$2"
shift 2

if [[ "$(id -u)" -ne 0 ]]; then
    echo "[INFO] fix-ownership: not running as root, skipping (ownership is managed by the host in --user mode)"
    exit 0
fi

# A dry run always inspects: the sentinel records what a past sweep did, not
# what the volume looks like now, and reporting from it would hide later drift.
if [[ "$dry_run" -eq 0 && -n "$sentinel" && -f "$sentinel" && "$(cat "$sentinel")" == "${schema}:${target_uid}:${target_gid}" ]]; then
    echo "[INFO] fix-ownership: ${target_uid}:${target_gid} (schema ${schema}) already applied, skipping"
    exit 0
fi

# A sweep that could not chown everything must not be recorded as complete:
# the sentinel would make every later boot skip it and the entries would stay
# unreachable once services run unprivileged.
swept_clean=1

for path in "$@"; do
    # An absent root is an incomplete sweep, not a finished one: /media/frigate
    # is not in the image, so a boot before the volume is mounted would
    # otherwise record success and the volume would never be swept once added.
    if [[ ! -d "$path" ]]; then
        swept_clean=0
        echo "[WARN] fix-ownership: $path does not exist, skipping; will retry on next boot"
        continue
    fi

    # find may fail mid-walk on a live volume (file deleted under it) or on a
    # stale mount. Tolerate it rather than aborting under errexit, but never
    # read a failed scan as "nothing to do": that would record the sweep as
    # complete without having looked.
    if ! count=$(find "$path" \( -not -uid "$target_uid" -o -not -gid "$target_gid" \) -printf '.' 2>/dev/null | wc -c); then
        swept_clean=0
        echo "[WARN] fix-ownership: could not scan ${path}; will retry on next boot"
        continue
    fi

    if [[ "$count" -eq 0 ]]; then
        echo "[INFO] fix-ownership: $path already owned by ${target_uid}:${target_gid}, nothing to do"
        continue
    fi

    # find does not descend symlinks and chown -h retargets the link itself, so
    # anything behind a symlinked directory is outside this sweep. Following
    # them is not an option: a link could walk the chown out of the volume.
    if [[ -n "$(find "$path" -type l -xtype d -print -quit 2>/dev/null)" ]]; then
        echo "[WARN] fix-ownership: ${path} contains symlinked directories; ownership behind them is not managed and must be aligned by hand"
    fi

    echo "[WARN] fix-ownership: adjusting ownership of ${count} entries under ${path}; on large recordings volumes this can take a long time"
    if [[ "$dry_run" -eq 1 ]]; then
        echo "[INFO] fix-ownership: dry run, not changing ${path}"
        continue
    fi

    find "$path" \( -not -uid "$target_uid" -o -not -gid "$target_gid" \) \
        -exec chown -h "${target_uid}:${target_gid}" {} + || {
        swept_clean=0
        echo "[WARN] fix-ownership: some entries under ${path} could not be updated (deleted mid-sweep or chown denied); will retry on next mismatch"
    }
done

# go2rtc (separate user) must be able to REACH its HomeKit state in /config.
# Write access is per-file, not per-directory: go2rtc's PatchConfig rewrites
# the first -config file via os.WriteFile (in-place truncate, no rename,
# verified against go2rtc v1.9.14 internal/app/config.go), and the file is
# always pre-created by setup_homekit_config before go2rtc starts, so
# O_CREATE never needs directory write. See header comment for why g+w is
# forbidden here.
if [[ "$dry_run" -eq 0 && -d /config ]]; then
    chgrp frigate-data /config 2>/dev/null || true
    chmod g+rx /config 2>/dev/null || true
fi

if [[ "$dry_run" -eq 0 && -n "$sentinel" && "$swept_clean" -eq 1 ]]; then
    echo "${schema}:${target_uid}:${target_gid}" > "$sentinel" || \
        echo "[WARN] fix-ownership: could not write ${sentinel}; the sweep will run again on next boot"
fi
