LibrePortal/scripts/backup/engine/engine_dispatch.sh
librelad 61cebb5ab8 feat(backup): external/removable drive safety guards (phase 3b)
backupLocationLocalGuard (engine-agnostic, in location_paths.sh), wired into the
dispatcher before init, readiness, and every backup write (engineInitLocation /
engineEnsureLocationReady / engineBackupApp):

- Filesystem warning: the ownership model chowns the repo to the backup user, which
  needs POSIX permissions — warn (non-fatal) on FAT/exFAT/NTFS via findmnt FSTYPE.
- Mount-presence refusal: a location with CFG_BACKUP_LOC_<idx>_REQUIRE_MOUNT=true
  (an external/removable disk) is refused when its path isn't on a real mount
  (findmnt TARGET is '/' or unknown) — so an unplugged drive never silently fills
  the system disk. Opt-in; default false leaves on-disk locations unaffected.

New REQUIRE_MOUNT field documented in the location.config template (location_add.sh)
so it surfaces on the Locations page. Verified: REQUIRE_MOUNT+unmounted refuses;
default allows; non-local no-ops.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 15:35:00 +01:00

122 lines
4.8 KiB
Bash

#!/bin/bash
# Per-location engine dispatcher. Resolves the engine for a given location
# (CFG_BACKUP_LOC_N_ENGINE → CFG_BACKUP_ENGINE → 'restic'), then forwards to
# the engine adapter's `<engine><FunctionName>` implementation. Adapters live
# in scripts/backup/engine/<engine>_*.sh; today restic_*.sh is the only one.
engineForLocation()
{
local idx="$1"
local var="CFG_BACKUP_LOC_${idx}_ENGINE"
local e="${!var}"
[[ -z "$e" ]] && e="${CFG_BACKUP_ENGINE:-restic}"
echo "$e"
}
engineKnownIds()
{
# List adapter implementations discovered by looking for the canonical
# `<engine>BackupAppToLocation` function name registered at source time.
compgen -A function 2>/dev/null | grep -oE '^[a-z]+BackupAppToLocation$' | sed 's/BackupAppToLocation//' | sort -u
}
engineDispatch()
{
# Internal helper: call $1=<engine><FunctionName> with the remaining args.
# Falls back with a clear error if the adapter doesn't implement it.
local fn="$1"
shift
if ! declare -f "$fn" >/dev/null 2>&1; then
isError "Backup engine has no '$fn' implementation"
return 1
fi
"$fn" "$@"
}
# ---- Idx-scoped dispatchers ----------------------------------------------------
# Local/removable-drive safety guard runs before init, readiness, and any backup
# write (see backupLocationLocalGuard) — refuses to write when a REQUIRE_MOUNT
# drive isn't mounted, so restic never fills the system disk.
engineInitLocation() { local i="$1"; backupLocationLocalGuard "$i" || return 1; engineDispatch "$(engineForLocation "$i")InitLocation" "$i"; }
engineEnsureLocationReady() { local i="$1"; backupLocationLocalGuard "$i" || return 1; engineDispatch "$(engineForLocation "$i")EnsureLocationReady" "$i"; }
enginePasswordEnsure() { local i="$1"; engineDispatch "$(engineForLocation "$i")PasswordEnsure" "$i"; }
engineLocationUri() { local i="$1"; engineDispatch "$(engineForLocation "$i")LocationUri" "$i"; }
engineLocationStats() { local i="$1"; engineDispatch "$(engineForLocation "$i")LocationStats" "$i"; }
engineEnvExport() { local i="$1"; engineDispatch "$(engineForLocation "$i")EnvExport" "$i"; }
engineEnvUnset() { local i="$1"; engineDispatch "$(engineForLocation "${i:-1}")EnvUnset"; }
engineBackupApp() { local i="$1"; shift; backupLocationLocalGuard "$i" || return 1; engineDispatch "$(engineForLocation "$i")BackupAppToLocation" "$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" "$@"; }
engineSnapshotListFiles() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")SnapshotListFiles" "$i" "$@"; }
engineForgetApp() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")ForgetApp" "$i" "$@"; }
engineCheckLocation() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")CheckLocation" "$i" "$@"; }
engineDumpFile() { local i="$1"; shift; engineDispatch "$(engineForLocation "$i")DumpFile" "$i" "$@"; }
# ---- Aggregate helpers (iterate enabled locations) ---------------------------
engineInstallAll()
{
if ! declare -f resticEnabledLocations >/dev/null 2>&1; then
isError "engineInstallAll: location helpers not loaded yet"
return 1
fi
declare -A seen
local idx engine fn
while IFS= read -r idx; do
[[ -z "$idx" ]] && continue
engine=$(engineForLocation "$idx")
[[ -n "${seen[$engine]}" ]] && continue
seen[$engine]=1
fn="${engine}Install"
if declare -f "$fn" >/dev/null 2>&1; then
"$fn"
fi
done < <(resticEnabledLocations)
}
engineInitAllLocations()
{
isHeader "Backup Location Initialization"
local idx
while IFS= read -r idx; do
[[ -z "$idx" ]] && continue
engineInitLocation "$idx"
done < <(resticEnabledLocations)
}
engineEnsureAllLocationsReady()
{
engineInstallAll
local idx
while IFS= read -r idx; do
[[ -z "$idx" ]] && continue
engineEnsureLocationReady "$idx"
done < <(resticEnabledLocations)
}
engineForgetAppAllLocations()
{
local app="$1"
local idx
while IFS= read -r idx; do
[[ -z "$idx" ]] && continue
engineForgetApp "$idx" "$app"
done < <(resticEnabledLocations)
}
engineCheckAllLocations()
{
local pct="$1"
local idx
local failed=0
while IFS= read -r idx; do
[[ -z "$idx" ]] && continue
engineCheckLocation "$idx" "$pct" || failed=$((failed + 1))
done < <(resticEnabledLocations)
return $failed
}