#!/bin/bash # App Updater command handler — `libreportal updater ` # --------------------------------------------------------------------------- # Dispatched automatically by cli_initialize.sh (category -> cliHandleUpdaterCommands). # Subcommands (the features/updater WebUI buttons route to these as tasks): # check refresh the version/CVE data (runs the WebUI generator) # apply update one app — DISASTER-RECOVERY FIRST: snapshot the app # via the backup engine, then pull + recreate; on failure, # roll back to the snapshot automatically # apply-all [a,b] apply to a comma-list (or every update-available app) # rollback restore the app's most recent pre-update snapshot # # State-changing subcommands use the standard task-exec split: invoked normally # they enqueue a task (so the WebUI + CLI share locking + the audit trail); # the task processor re-invokes them with LIBREPORTAL_TASK_EXEC=1 to do the work. cliHandleUpdaterCommands() { local sub="$initial_command2" local app="$initial_command3" case "$sub" in ""|"check") # Quick + safe — just regenerates the read-only data files. Source # the generator explicitly if the lazy loader hasn't mapped it yet # (new file; the array regen self-heals it on deploy, this covers # the gap before that). if ! declare -F webuiUpdaterScan >/dev/null 2>&1; then source "$install_scripts_dir/webui/data/generators/updater/webui_updater_scan.sh" 2>/dev/null fi webuiUpdaterScan ;; "apply"|"now") if [[ -z "$app" ]]; then isError "Usage: libreportal updater apply "; return 1; fi if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then updaterApplyApp "$app" else cliTaskRun "libreportal updater apply $app" "updater_apply" "$app" "" fi ;; "apply-all") local list="$app" # optional comma-list in $initial_command3 if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then updaterApplyAll "$list" else cliTaskRun "libreportal updater apply-all $list" "updater_apply_all" "updater" "" fi ;; "rollback") if [[ -z "$app" ]]; then isError "Usage: libreportal updater rollback "; return 1; fi if [[ "$LIBREPORTAL_TASK_EXEC" == "1" ]]; then updaterRollbackApp "$app" else cliTaskRun "libreportal updater rollback $app" "updater_rollback" "$app" "" fi ;; *) cliShowUpdaterHelp ;; esac } # Update one app with disaster-recovery: snapshot -> pull -> recreate -> verify, # auto-rolling-back on failure. Uses existing primitives (the backup CLI for the # snapshot, docker compose for the image swap) so it shares their locking/logging. updaterApplyApp() { local app="$1" local app_dir="$containers_dir/$app" if [[ ! -d "$app_dir" ]]; then isError "App '$app' is not installed."; return 1; fi isHeader "Updating $app (a recovery snapshot is taken first)" # 1. DISASTER RECOVERY — snapshot before touching anything. isNotice "Snapshotting $app before update…" if ! libreportal backup app "$app" >/dev/null 2>&1; then isNotice "Pre-update snapshot did not complete cleanly — continuing is risky; aborting $app update." updaterRecordHistory "$app" "update" "" "" "aborted-no-snapshot" return 1 fi # 2. Capture the current image so we can record from->to / roll back. local before; before="$(grep -m1 -E '^\s*image:' "$app_dir/docker-compose.yml" 2>/dev/null | sed -E 's/^\s*image:\s*//; s/["'"'"']//g')" # 3. Pull + recreate (uses the real, install-type-aware compose helpers). isNotice "Pulling new image(s) for $app…" if updaterComposePull "$app" && dockerComposeUp "$app" >/dev/null 2>&1; then local after; after="$(grep -m1 -E '^\s*image:' "$app_dir/docker-compose.yml" 2>/dev/null | sed -E 's/^\s*image:\s*//; s/["'"'"']//g')" updaterRecordHistory "$app" "update" "$before" "$after" "ok" isSuccessful "$app updated. Rollback point retained." webuiUpdaterScan >/dev/null 2>&1 || true return 0 fi # 4. Failure -> automatic rollback. isNotice "Update of $app failed — rolling back to the pre-update snapshot…" updaterRollbackApp "$app" "auto" updaterRecordHistory "$app" "update" "$before" "" "rolled-back" return 1 } updaterApplyAll() { local list="$1" failures=0 if [[ -z "$list" ]]; then isNotice "No app list given; nothing to do (the WebUI passes the update-available apps)." return 0 fi local IFS=',' for app in $list; do [[ -z "$app" ]] && continue updaterApplyApp "$app" || failures=$((failures+1)) done [[ $failures -gt 0 ]] && isNotice "$failures app(s) failed and were rolled back." || isSuccessful "All requested apps updated." } # Roll an app back to its most recent snapshot. $2='auto' suppresses the header # (called from the failure path of an apply). updaterRollbackApp() { local app="$1" mode="$2" [[ "$mode" != "auto" ]] && isHeader "Rolling $app back to its pre-update snapshot" # Delegate to the backup engine's restore (latest snapshot for this app). if libreportal backup app "$app" restore latest >/dev/null 2>&1; then dockerComposeUp "$app" >/dev/null 2>&1 || true [[ "$mode" != "auto" ]] && updaterRecordHistory "$app" "rollback" "" "" "rolled-back" isSuccessful "$app restored from its pre-update snapshot." return 0 fi isError "Could not roll $app back automatically — restore manually from the Backups page." return 1 } # Force a fresh image pull for an app (mirrors up_app.sh's install-type split). # dockerComposeUp uses --quiet-pull which won't re-fetch a moved tag, so we pull # explicitly first to actually pick up a new image. updaterComposePull() { local app="$1" dir="${containers_dir%/}/$1" [ -d "$dir" ] || return 1 if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then dockerCommandRunInstallUser "cd $dir && docker compose pull" >/dev/null 2>&1 else ( cd "$dir" && docker compose pull >/dev/null 2>&1 ) fi } # Append an entry to history.json (best-effort; needs jq, skips silently if absent). updaterRecordHistory() { local app="$1" action="$2" from="$3" to="$4" result="$5" local f="$containers_dir/libreportal/frontend/data/updater/generated/history.json" command -v jq >/dev/null 2>&1 || return 0 [ -f "$f" ] || printf '{ "entries": [] }\n' | runFileWrite "$f" local ts; ts="$(date -Iseconds 2>/dev/null || date)" local tmp; tmp="$(mktemp)" if jq --arg ts "$ts" --arg app "$app" --arg action "$action" --arg from "$from" --arg to "$to" --arg result "$result" \ '.entries = ([{ts:$ts, app:$app, action:$action, from:$from, to:$to, result:$result}] + (.entries // []))[0:200]' \ "$f" > "$tmp" 2>/dev/null; then runFileWrite "$f" < "$tmp" fi rm -f "$tmp" }