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>
79 lines
2.3 KiB
Bash
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
|
|
}
|