Reads files the backup user can't see from the host (container-owned, e.g. Nextcloud's www-data data dir) by streaming them out THROUGH the container (docker exec tar) — no host root, no host read perms, works rooted + rootless. Extracts to staging as plain files so restic keeps full dedup + per-file restore (not a piped tar blob); the live path is excluded from the snapshot. Restore streams the staging copy back through a throwaway in-namespace container that recreates the tree with the app's uid:gid. Declared via a libreportal.backup.files compose label; Nextcloud (html, 33:33) is the first to use it. Live capture failure falls back to stop-snapshot-start. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
150 lines
4.8 KiB
Bash
Executable File
150 lines
4.8 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
backupAppStart()
|
|
{
|
|
local app_name="$1"
|
|
local stored_app_name="$app_name"
|
|
|
|
if [[ -z "$app_name" ]]; then
|
|
isError "backupAppStart called with empty app_name"
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! -d "$containers_dir$app_name" ]]; then
|
|
isError "Cannot back up '$app_name' — not installed at $containers_dir$app_name"
|
|
return 1
|
|
fi
|
|
|
|
if [[ -z "$(resticEnabledLocations)" ]]; then
|
|
isError "No backup locations enabled — configure at least one on the Locations page before running backups."
|
|
return 1
|
|
fi
|
|
|
|
engineEnsureAllLocationsReady
|
|
|
|
isHeader "Backing up $stored_app_name"
|
|
|
|
((menu_number++))
|
|
echo ""
|
|
echo "---- $menu_number. Running pre-backup hook (if present)"
|
|
echo ""
|
|
backupAppRunHook "$stored_app_name" pre
|
|
|
|
local strategy
|
|
strategy=$(backupResolveStrategy "$stored_app_name")
|
|
|
|
((menu_number++))
|
|
echo ""
|
|
echo "---- $menu_number. Quiescing container(s) for $stored_app_name (strategy: $strategy)"
|
|
echo ""
|
|
if [[ "$strategy" == "pause-snapshot-unpause" ]]; then
|
|
dockerComposePause "$stored_app_name" 2>/dev/null || dockerComposeDown "$stored_app_name"
|
|
elif [[ "$strategy" == "live" ]]; then
|
|
isNotice "Live strategy — containers stay running; databases dumped + private files captured via their containers"
|
|
if ! backupDbDump "$stored_app_name" || ! backupFilesCapture "$stored_app_name"; then
|
|
isError "Live capture failed — falling back to stop-snapshot-start for safety"
|
|
sudo rm -rf "${containers_dir:?}$stored_app_name/.lp-backup"
|
|
strategy="stop-snapshot-start"
|
|
dockerComposeDown "$stored_app_name"
|
|
fi
|
|
else
|
|
dockerComposeDown "$stored_app_name"
|
|
fi
|
|
|
|
((menu_number++))
|
|
echo ""
|
|
echo "---- $menu_number. Writing backup manifest"
|
|
echo ""
|
|
local manifest_sha
|
|
manifest_sha=$(manifestWrite "$stored_app_name")
|
|
checkSuccess "Manifest written (sha: ${manifest_sha:0:8})"
|
|
|
|
((menu_number++))
|
|
echo ""
|
|
echo "---- $menu_number. Snapshotting to all enabled locations"
|
|
echo ""
|
|
# On the live path the raw DB data dirs (superseded by the dumps) and the
|
|
# private file trees (superseded by the container-side captures) are excluded
|
|
# so the snapshot carries only the consistent copies. Other strategies
|
|
# quiesced the app, so keep everything.
|
|
backup_exclude_paths=""
|
|
if [[ "$strategy" == "live" ]]; then
|
|
backup_exclude_paths=$(printf '%s\n%s\n' \
|
|
"$(backupDbExcludePaths "$stored_app_name")" \
|
|
"$(backupFilesExcludePaths "$stored_app_name")")
|
|
fi
|
|
local primary_snapshot_id=""
|
|
local primary_idx=""
|
|
local first_loc=true
|
|
local idx
|
|
while IFS= read -r idx; do
|
|
[[ -z "$idx" ]] && continue
|
|
local snap_id
|
|
snap_id=$(engineBackupApp "$idx" "$stored_app_name" "$manifest_sha")
|
|
if [[ "$first_loc" == true && -n "$snap_id" ]]; then
|
|
primary_snapshot_id="$snap_id"
|
|
primary_idx="$idx"
|
|
first_loc=false
|
|
fi
|
|
done < <(resticEnabledLocations)
|
|
|
|
((menu_number++))
|
|
echo ""
|
|
echo "---- $menu_number. Restarting container(s) for $stored_app_name"
|
|
echo ""
|
|
if [[ "$strategy" == "pause-snapshot-unpause" ]]; then
|
|
dockerComposeUnpause "$stored_app_name" 2>/dev/null || dockerComposeUp "$stored_app_name"
|
|
elif [[ "$strategy" != "live" ]]; then
|
|
dockerComposeUp "$stored_app_name"
|
|
fi
|
|
|
|
((menu_number++))
|
|
echo ""
|
|
echo "---- $menu_number. Running post-backup hook (if present)"
|
|
echo ""
|
|
backupAppRunHook "$stored_app_name" post
|
|
|
|
if [[ "$CFG_BACKUP_VERIFY_AFTER" == "true" && -n "$primary_snapshot_id" ]]; then
|
|
((menu_number++))
|
|
echo ""
|
|
echo "---- $menu_number. Verifying snapshot integrity"
|
|
echo ""
|
|
backupVerifySnapshot "$primary_idx" "$primary_snapshot_id" "$stored_app_name"
|
|
fi
|
|
|
|
((menu_number++))
|
|
echo ""
|
|
echo "---- $menu_number. Applying retention policy"
|
|
echo ""
|
|
engineForgetAppAllLocations "$stored_app_name"
|
|
|
|
((menu_number++))
|
|
echo ""
|
|
echo "---- $menu_number. Logging backup into database"
|
|
echo ""
|
|
databaseBackupInsert "$stored_app_name"
|
|
|
|
if [[ "$CFG_REQUIREMENT_WEBUI" == "true" ]]; then
|
|
((menu_number++))
|
|
echo ""
|
|
echo "---- $menu_number. Updating WebUI backup data"
|
|
echo ""
|
|
webuiGenerateBackupLocations
|
|
webuiGenerateBackupDashboard
|
|
webuiGenerateBackupSnapshots all
|
|
webuiGenerateBackupAppStatus "$stored_app_name"
|
|
fi
|
|
|
|
echo ""
|
|
echo "A backup of $stored_app_name has been taken on $current_date at $current_time" >> "$logs_dir$backup_log_file"
|
|
echo ""
|
|
|
|
menu_number=0
|
|
}
|
|
|
|
backupSchedule()
|
|
{
|
|
local app_name="$1"
|
|
backupAppStart "$app_name"
|
|
}
|