Merge claude/1

This commit is contained in:
librelad 2026-05-26 00:20:31 +01:00
commit f2dc3f27d9
7 changed files with 182 additions and 6 deletions

View File

@ -177,10 +177,31 @@ is a dev-only escape hatch.
`containers/<app>/tools/<app>_<tool_id>.sh` (function `app<App><PascalToolId>`).
- **Logic helpers** (anything that isn't a Tools-tab action — e.g. the auth
adapter, post-install fixups): `containers/<app>/scripts/<app>_*.sh`.
- **Static data / container payloads:** `containers/<app>/resources/` — it's
**pruned** by the container scan (never sourced), so it may nest freely AND is
the home for any `.sh` that *executes on load* (a payload run inside a
container, e.g. headscale's `tailscale.sh`). Rule: a sourced function file →
`scripts/`; a script that runs when invoked → `resources/`.
- The container scan **live-sources** every `.sh` under `containers/<app>/`
(`tools/` and `scripts/`), and `webui_tools.sh` **auto-merges** the
`.tools.json`. So dropping the folder onto an install is all it takes — no
central edits, no array regen.
- **Reserved folder names:** `tools/`, `scripts/`, `resources/` are
LibrePortal's. An app's compose must **not** bind-mount to `./tools`,
`./scripts`, or `./resources` — name app mount dirs anything else (`./data`,
`./config`, …) or nest them under `resources/`. (The scan is `maxdepth 3`, so
one sourced subfolder level is the cap — keep `scripts/` flat; name files
`<app>_<purpose>.sh` instead of nesting.)
- **What gets backed up** (so you know what's safe to regenerate vs. preserve):
per-app backups capture only the live data dir `<containers>/<app>/` — the
deployed compose, the live `<app>.config` (settings + secrets), and the mounted
data (DBs dumped logically; raw DB dirs excluded). The **system config**
(`<system>/configs` — global settings, WebUI creds, **backup-location creds**)
is captured by a separate system-config snapshot. **Not** backed up (reproducible
from the release): `scripts/`, `tools/`, install templates, and the `libreportal`
WebUI app itself (its frontend + generated JSON regenerate; its only state, the
login, lives in the system config). Restore = reinstall the code, then restore
the system config + each app's data dir.
- **New runtime script?** Add it under `scripts/<area>/…` and run
`scripts/source/files/generate_arrays.sh run` (or `libreportal regen arrays`) so
it's sourced (build/standalone tooling under `scripts/release` and

View File

@ -14,14 +14,27 @@ backupAllApps()
app_names+=("$name")
done < <(runInstallOp sqlite3 "$docker_dir/$db_file" "SELECT name FROM apps WHERE status = 1;")
if [[ ${#app_names[@]} -eq 0 ]]; then
isNotice "No installed applications found — nothing to back up"
return 0
fi
local done_count=0
for name in "${app_names[@]}"; do
# The libreportal WebUI app is reproducible — frontend + generated JSON
# regenerate on deploy, and its only state (the login) lives in the system
# config, captured by backupSystemConfig below. Nothing in its data dir is
# worth a snapshot.
if [[ "$name" == "libreportal" ]]; then
isNotice "Skipping '$name' — WebUI is reproducible; its state is in the system config"
continue
fi
backupAppStart "$name"
((done_count++))
done
isSuccessful "Backup pass complete — ${#app_names[@]} apps"
# System config snapshot (global settings, WebUI creds, backup-location creds)
# so a bare-metal restore is self-sufficient — this is why the system root is
# its own backup-able tree. The code/install tree is reproducible from the
# release, so it is deliberately NOT included.
if declare -f backupSystemConfig >/dev/null 2>&1; then
backupSystemConfig
fi
isSuccessful "Backup pass complete — ${done_count} app(s) + system config"
}

View File

@ -48,6 +48,8 @@ engineEnvExport() { local i="$1"; engineDispatch "$(engineForLocation
engineEnvUnset() { local i="$1"; engineDispatch "$(engineForLocation "${i:-1}")EnvUnset"; }
engineBackupApp() { local i="$1"; shift; backupLocationLocalGuard "$i" || return 1; engineDispatch "$(engineForLocation "$i")BackupAppToLocation" "$i" "$@"; }
engineBackupSystem() { local i="$1"; shift; backupLocationLocalGuard "$i" || return 1; engineDispatch "$(engineForLocation "$i")BackupSystemToLocation" "$i" "$@"; }
engineRestoreSystemLatest() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")RestoreSystemLatest" "$i" "$@"; }
engineRestoreSnapshot() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")RestoreSnapshot" "$i" "$@"; }
engineSnapshotLatestId() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SnapshotLatestId" "$i" "$@"; }
engineSnapshotsJson() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SnapshotsJson" "$i" "$@"; }

View File

@ -60,6 +60,48 @@ resticBackupAppToLocation()
return $rc
}
resticBackupSystemToLocation()
{
local idx="$1"
local source_path="${configs_dir%/}"
if [[ ! -d "$source_path" ]]; then
isError "System config path missing: $source_path"
return 1
fi
resticEnvExport "$idx" || return 1
local host_tag="${CFG_INSTALL_NAME:-libreportal}"
local loc_name
loc_name=$(resticLocationName "$idx")
isNotice "Snapshotting system config → $loc_name" >&2
local output
output=$(runBackupOp restic backup \
--host "$host_tag" \
--tag "system=config" \
--tag "host=$host_tag" \
--tag "engine=libreportal" \
--exclude-caches \
--json \
"$source_path" 2>&1)
local rc=$?
local snapshot_id
snapshot_id=$(echo "$output" | grep -o '"snapshot_id":"[^"]*"' | tail -1 | cut -d'"' -f4)
if [[ $rc -eq 0 ]]; then
isSuccessful "System config backed up to $loc_name: ${snapshot_id:0:8}" >&2
echo "$snapshot_id"
else
isError "System config backup to $loc_name failed" >&2
echo "$output" | tail -10 >&2
fi
resticEnvUnset
return $rc
}
resticBackupAppAllLocations()
{
local app_name="$1"

View File

@ -44,3 +44,26 @@ resticRestoreAppLatest()
local include_path="$containers_dir$app_name"
resticRestoreSnapshot "$idx" "$snapshot_id" "$target_dir" "$include_path"
}
resticRestoreSystemLatest()
{
local idx="$1"
local target_dir="$2"
local host="${3:-$CFG_INSTALL_NAME}"
resticEnvExport "$idx" || return 1
local snapshot_id
snapshot_id=$(runBackupOp restic snapshots \
--tag "system=config" --host "$host" \
--latest 1 --json --no-lock 2>/dev/null | \
grep -o '"short_id":"[^"]*"' | head -1 | cut -d'"' -f4)
resticEnvUnset
if [[ -z "$snapshot_id" ]]; then
isError "No system-config snapshot found in $(resticLocationName "$idx") for host=$host"
return 1
fi
# Whole-snapshot restore (the snapshot is just the config tree) into staging.
resticRestoreSnapshot "$idx" "$snapshot_id" "$target_dir"
}

View File

@ -0,0 +1,74 @@
#!/bin/bash
# System-config backup.
#
# Snapshots the system config tree (<system>/configs — global settings, WebUI
# credentials, and crucially the BACKUP-LOCATION credentials) to every enabled
# backup location, so a bare-metal restore is self-sufficient. Without this the
# location creds live only on the box: lose it and you can't even reach your own
# remote backups (chicken-and-egg). It is a lightweight, static-dir snapshot — no
# container quiescing or DB dumps (those are per-app concerns), so it does NOT go
# through backupAppStart. The install tree (code) is reproducible from the release
# and is deliberately NOT included; per-app data is handled by backupAppStart.
backupSystemConfig()
{
local source_path="${configs_dir%/}"
if [[ ! -d "$source_path" ]]; then
isNotice "System config dir not found ($source_path) — skipping system backup"
return 0
fi
if [[ -z "$(resticEnabledLocations)" ]]; then
isNotice "No backup locations enabled — skipping system config backup"
return 0
fi
isHeader "Backing up system config"
engineEnsureAllLocationsReady
local idx ok=0 fail=0
while IFS= read -r idx; do
[[ -z "$idx" ]] && continue
if engineBackupSystem "$idx" >/dev/null; then
ok=$((ok + 1))
else
fail=$((fail + 1))
fi
done < <(resticEnabledLocations)
if [[ $ok -eq 0 ]]; then
isError "System config backup failed on all locations"
return 1
fi
if [[ $fail -gt 0 ]]; then
isNotice "System config backed up to $ok location(s), failed on $fail"
else
isSuccessful "System config backed up to $ok location(s)"
fi
return 0
}
# Restore the latest system-config snapshot from a location into a STAGING dir.
# Deliberately does NOT overwrite live config — recovering creds/settings is a
# review-then-copy step, never an automatic blast over a running control plane.
backupRestoreSystemConfig()
{
local idx="${1:-}"
[[ -z "$idx" ]] && idx=$(resticEnabledLocations | head -1)
if [[ -z "$idx" ]]; then
isError "No enabled backup location to restore the system config from"
return 1
fi
local staging="${restore_dir%/}/system-config"
runFileOp mkdir -p "$staging"
isHeader "Restoring system config (to staging — live config is untouched)"
if engineRestoreSystemLatest "$idx" "$staging"; then
isSuccessful "System config restored to: $staging"
isNotice "Review it, then copy what you need into ${configs_dir} (backup-location creds, logins, settings). Live config was NOT overwritten."
return 0
fi
isError "System config restore failed"
return 1
}

View File

@ -48,6 +48,7 @@ backup_scripts=(
"backup/manifest/manifest_collect.sh"
"backup/manifest/manifest_read.sh"
"backup/manifest/manifest_write.sh"
"backup/system/backup_system.sh"
"backup/verify/backup_verify.sh"
)