LibrePortal/scripts/backup/engine/borg_restore.sh
librelad 226ebe1717 backup: give borg and kopia the same ownership mapping as restic
The user-namespace prefix that lets an unprivileged restore put back a file's
original owner was only wired into restic. borg extract and kopia snapshot
restore run as the same backup user with the same lack of CAP_CHOWN, so both
lost <container-uid>:<backup-user> exactly the way restic did — an app whose
data comes back owned by the backup user cannot write it, which is how grafana
kept dying with "attempt to write a readonly database".

borg is quieter about it than restic: it does not print an "ignoring error"
line at all, so there was nothing to notice.

Move the prefix to engine_dispatch.sh as backupUsernsPrefix — it was never
restic-specific — and use it from all three engines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 06:20:31 +01:00

79 lines
2.3 KiB
Bash

#!/bin/bash
borgRestoreSnapshot()
{
local idx="$1"
local snapshot_id="$2"
local target_dir="$3"
local include_path="$4"
if [[ -z "$snapshot_id" || -z "$target_dir" ]]; then
isError "borgRestoreSnapshot requires snapshot_id and target_dir"
return 1
fi
borgEnvExport "$idx" || return 1
runFileOp mkdir -p "$target_dir"
isNotice "Restoring $snapshot_id from $(resticLocationName "$idx")$target_dir"
# Same reason restic needs it: extraction runs as the backup user, which has
# no CAP_CHOWN, so without the namespace every <container-uid>:<backup-user>
# file comes back owned by the backup user and the app cannot write its own
# data. borg is quieter about it than restic — it does not even print an
# "ignoring error" line — so this went unnoticed for longer.
local ns_prefix=()
mapfile -t ns_prefix < <(backupUsernsPrefix)
local rc
if [[ -n "$include_path" ]]; then
local stripped="${include_path#/}"
( cd "$target_dir" && runBackupOp "${ns_prefix[@]}" borg extract "::$snapshot_id" "$stripped" )
rc=$?
else
( cd "$target_dir" && runBackupOp "${ns_prefix[@]}" borg extract "::$snapshot_id" )
rc=$?
fi
borgEnvUnset
return $rc
}
borgRestoreSystemLatest()
{
local idx="$1"
local target_dir="$2"
local host="${3:-$CFG_INSTALL_NAME}"
borgEnvExport "$idx" || return 1
local snapshot_id
snapshot_id=$(runBackupOp borg list --json --glob-archives "system-${host}-*" --last 1 2>/dev/null \
| grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)
borgEnvUnset
if [[ -z "$snapshot_id" ]]; then
isError "No system-config archive found in $(resticLocationName "$idx") for host=$host"
return 1
fi
# Whole-archive extract into staging (no include subpath).
borgRestoreSnapshot "$idx" "$snapshot_id" "$target_dir"
}
borgDumpFile()
{
local idx="$1"
local snapshot_id="$2"
local file_path="$3"
local target_file="$4"
borgEnvExport "$idx" || return 1
local stripped="${file_path#/}"
if [[ -n "$target_file" ]]; then
runBackupOp borg extract --stdout "::$snapshot_id" "$stripped" | runFileWrite "$target_file"
else
runBackupOp borg extract --stdout "::$snapshot_id" "$stripped"
fi
local rc=$?
borgEnvUnset
return $rc
}