The main sweep — ~260 call sites across ~100 files move from string
concatenation on a single root to appDir/storageAppDirs/storageAppConfigs.
On a single-root install the resolved paths are identical, so this is a
no-op until a location is registered.
Enumerators were the interesting half. `for d in "$containers_dir"/*/`
appears in the menus, the registry/artifact scanners and the DNS setup —
and a shell glob cannot list a rootless 751 tree at all, which is the
same bug config_find_file.sh already documents in a comment. Routing them
through storageAppDirs (which enumerates as the owning user) fixes that
alongside the multi-root work.
Three places needed judgement rather than substitution:
db_app_scan.sh deletes database rows and port allocations for apps whose
folder is missing, and reaps "empty" app dirs. With a storage location
unmounted, every app on it looks exactly like that. Each of those
branches now gates on appStorageAvailable first — an app on an unplugged
drive is skipped with a notice, never deleted.
instance_create.sh rewrites cloned hooks so an instance touches its own
directory instead of the base app's. Its sed matched ${containers_dir}<type>,
which this sweep just replaced with $(appDir <type>) — so it would have
silently stopped redirecting, and an instance would have written to the
original's files (the adguard auth adapter case its own comment warns
about). Now matches both appDir forms, verified against bare, quoted,
unrelated-app, legacy and prose cases.
peer_shell/peer_pull streamed and extracted relative to the primary root.
Both now use the app's own root, and peer_shell keeps a single-root
fallback since it runs as a restricted SSH shell with no LibrePortal env.
Also fixes a pre-existing bug found on the way: webui_app_config.sh
tested "$containers_dir/frontend/data/last_update", one level short of the
real tree under the libreportal app dir, so the WebUI refresh trigger
after a config update has never once fired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
4.4 KiB
Bash
143 lines
4.4 KiB
Bash
#!/bin/bash
|
|
|
|
resticBackupAppToLocation()
|
|
{
|
|
local idx="$1"
|
|
local app_name="$2"
|
|
local manifest_sha="$3"
|
|
local source_path="$(appDir "$app_name")"
|
|
|
|
if [[ ! -d "$source_path" ]]; then
|
|
isError "Source path missing for $app_name: $source_path"
|
|
return 1
|
|
fi
|
|
|
|
resticEnvExport "$idx" || return 1
|
|
|
|
local host_tag="${CFG_INSTALL_NAME:-libreportal}"
|
|
local extra_tags=(
|
|
--tag "app=$app_name"
|
|
--tag "host=$host_tag"
|
|
--tag "engine=libreportal"
|
|
)
|
|
[[ -n "$manifest_sha" ]] && extra_tags+=(--tag "manifest=$manifest_sha")
|
|
|
|
# On the live path backup_app_start sets $backup_exclude_paths to the raw
|
|
# DB data dirs the dumps replace; keep them out of the snapshot.
|
|
local exclude_args=()
|
|
local p
|
|
while IFS= read -r p; do
|
|
[[ -n "$p" ]] && exclude_args+=(--exclude "$p")
|
|
done <<< "${backup_exclude_paths:-}"
|
|
|
|
local loc_name
|
|
loc_name=$(resticLocationName "$idx")
|
|
# Logs go to stderr so this function's stdout is ONLY the snapshot id —
|
|
# the caller captures it with $() and feeds it to verify/retention.
|
|
isNotice "Snapshotting $app_name → $loc_name" >&2
|
|
local output
|
|
output=$(runBackupOp restic backup \
|
|
--host "$host_tag" \
|
|
"${extra_tags[@]}" \
|
|
"${exclude_args[@]}" \
|
|
--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 "Backup created in $loc_name: ${snapshot_id:0:8}" >&2
|
|
echo "$snapshot_id"
|
|
else
|
|
isError "Backup to $loc_name failed for $app_name" >&2
|
|
echo "$output" | tail -10 >&2
|
|
# restic exit 3 = snapshot written but some source files were
|
|
# unreadable. It stays in the repo but is NOT reported as this run's
|
|
# result — an incomplete snapshot must never be recorded as a good
|
|
# backup (it can be missing exactly the files that matter, e.g. keys).
|
|
if [[ $rc -eq 3 && -n "$snapshot_id" ]]; then
|
|
isError "An INCOMPLETE snapshot ${snapshot_id:0:8} was still written to $loc_name — it is missing the unreadable files listed above; do not rely on it" >&2
|
|
fi
|
|
fi
|
|
|
|
resticEnvUnset
|
|
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
|
|
if [[ $rc -eq 3 && -n "$snapshot_id" ]]; then
|
|
isError "An INCOMPLETE snapshot ${snapshot_id:0:8} was still written to $loc_name — it is missing the unreadable files listed above; do not rely on it" >&2
|
|
fi
|
|
fi
|
|
|
|
resticEnvUnset
|
|
return $rc
|
|
}
|
|
|
|
resticBackupAppAllLocations()
|
|
{
|
|
local app_name="$1"
|
|
local manifest_sha="$2"
|
|
local failed=0
|
|
local succeeded=0
|
|
local idx
|
|
|
|
while IFS= read -r idx; do
|
|
[[ -z "$idx" ]] && continue
|
|
if resticBackupAppToLocation "$idx" "$app_name" "$manifest_sha" >/dev/null; then
|
|
succeeded=$((succeeded + 1))
|
|
else
|
|
failed=$((failed + 1))
|
|
fi
|
|
done < <(resticEnabledLocations)
|
|
|
|
if [[ $succeeded -eq 0 ]]; then
|
|
isError "All backup targets failed for $app_name"
|
|
return 1
|
|
fi
|
|
if [[ $failed -gt 0 ]]; then
|
|
isNotice "Backup of $app_name succeeded on $succeeded location(s), failed on $failed"
|
|
else
|
|
isSuccessful "Backup of $app_name succeeded on $succeeded location(s)"
|
|
fi
|
|
return 0
|
|
}
|