backupLocationEnsureDir and the config write both went through runFileOp / runFileWrite, which run as the container user. Backup location configs live under the system tree, which is owned by the manager — so the mkdir was denied, the write then failed with "No such file or directory", and locationAdd still printed "Location N added". The result was a location that existed in name only: every later command that sourced its config found nothing. It surfaced in the first-run restore path, where the installer adds the location it is about to read from and then fails with "Backup location 2 has no config". Use runInstallOp/runInstallWrite, which run as the manager and can write there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
131 lines
4.8 KiB
Bash
131 lines
4.8 KiB
Bash
#!/bin/bash
|
|
|
|
# Per-location directory layout helpers. Each backup location is one
|
|
# directory under configs/backup/locations/<idx>/ containing:
|
|
#
|
|
# location.config — sourced at startup, holds CFG_BACKUP_LOC_<idx>_*
|
|
# including PASSWORD (auto-randomized from
|
|
# RANDOMIZEDPASSWORD<N> on first install)
|
|
# ssh.key — private SSH key when AUTH=key (chmod 0600)
|
|
# kopia.config — kopia adapter's connection state (chmod 0600)
|
|
#
|
|
# Everything for one location is co-located here so add/remove operations
|
|
# are mkdir / rm of one directory.
|
|
|
|
backupLocationsDir()
|
|
{
|
|
echo "$configs_dir/backup/locations"
|
|
}
|
|
|
|
backupLocationDir()
|
|
{
|
|
local idx="$1"
|
|
echo "$(backupLocationsDir)/$idx"
|
|
}
|
|
|
|
backupLocationConfig()
|
|
{
|
|
local idx="$1"
|
|
echo "$(backupLocationDir "$idx")/location.config"
|
|
}
|
|
|
|
backupLocationSshKey()
|
|
{
|
|
local idx="$1"
|
|
echo "$(backupLocationDir "$idx")/ssh.key"
|
|
}
|
|
|
|
backupLocationKopiaConfig()
|
|
{
|
|
local idx="$1"
|
|
echo "$(backupLocationDir "$idx")/kopia.config"
|
|
}
|
|
|
|
# Owner used when chowning per-location files. Falls back to sudo_user_name
|
|
# when docker_install_user hasn't been resolved (CLI startup before
|
|
# checkInstallTypeRequirement runs).
|
|
backupLocationOwner()
|
|
{
|
|
echo "${docker_install_user:-${sudo_user_name:-libreportal}}"
|
|
}
|
|
|
|
# NOTE: runInstallOp, not runFileOp.
|
|
#
|
|
# These directories live under configs/, which is MANAGER-owned — runFileOp runs
|
|
# as the container user, which cannot even mkdir there. The mkdir failed
|
|
# silently, the config write then failed with "No such file or directory", and
|
|
# locationAdd still printed "Location N added". So `backup location add` could
|
|
# not create a location at all, while reporting that it had.
|
|
#
|
|
# 0750 rather than 0700: the backup engine runs as the container user via
|
|
# runBackupOp and has to traverse in to read location.config. World-readable is
|
|
# what location 1 ended up with historically; tightening the config file itself
|
|
# needs a root helper action, since the manager cannot chgrp to the container
|
|
# user's group (verified: "Operation not permitted"). Tracked in
|
|
# docs/roadmap/first-run-restore.md §4.1.
|
|
backupLocationEnsureDir()
|
|
{
|
|
local idx="$1"
|
|
local dir
|
|
dir=$(backupLocationDir "$idx")
|
|
runInstallOp mkdir -p "$dir"
|
|
runInstallOp chmod 0755 "$dir"
|
|
}
|
|
|
|
backupLocationResolvedPath()
|
|
{
|
|
local idx="$1"
|
|
local mode
|
|
mode=$(resticLocationField "$idx" PATH_MODE)
|
|
if [[ "$mode" == "auto" ]]; then
|
|
# Base dir is the configurable Default Backup Location (Backup Engine
|
|
# config); each location gets its own numbered subfolder.
|
|
local base="${CFG_BACKUP_DEFAULT_PATH:-${backup_dir:-$docker_dir/backups}}"
|
|
echo "${base%/}/${idx}"
|
|
else
|
|
resticLocationField "$idx" PATH
|
|
fi
|
|
}
|
|
|
|
# Safety guard for LOCAL backup locations — especially external/removable drives.
|
|
# Engine-agnostic; call before init or any backup write. Returns non-zero only on
|
|
# the mount failure (so the caller refuses to write), warns (non-fatal) otherwise.
|
|
# - Filesystem warning: the ownership model chowns the repo to the backup user,
|
|
# which needs POSIX permissions. FAT/exFAT/NTFS can't, so warn.
|
|
# - Mount-presence refusal: when the location sets REQUIRE_MOUNT=true (an
|
|
# external/removable drive), refuse if the path isn't on a real mount — else
|
|
# restic would silently write to the underlying dir on the SYSTEM disk and
|
|
# fill it when the drive is unplugged.
|
|
backupLocationLocalGuard()
|
|
{
|
|
local idx="$1"
|
|
[[ -z "$idx" ]] && return 0
|
|
[[ "$(resticLocationType "$idx")" == "local" ]] || return 0
|
|
|
|
local base probe
|
|
base=$(backupLocationResolvedPath "$idx"); base="${base%/}"
|
|
[[ -z "$base" ]] && return 0
|
|
# Probe the nearest EXISTING ancestor (the repo dir may not exist yet).
|
|
probe="$base"
|
|
while [[ "$probe" != "/" && ! -d "$probe" ]]; do probe="$(dirname "$probe")"; done
|
|
|
|
if command -v findmnt >/dev/null 2>&1; then
|
|
local fstype
|
|
fstype=$(findmnt -no FSTYPE --target "$probe" 2>/dev/null | tail -1)
|
|
case "$fstype" in
|
|
vfat|exfat|ntfs|ntfs3|msdos|fuseblk)
|
|
isNotice "Backup location $(resticLocationName "$idx"): '$probe' is $fstype — no POSIX ownership/permissions; the repo chown may fail. Prefer ext4/xfs/btrfs." ;;
|
|
esac
|
|
fi
|
|
|
|
if [[ "$(resticLocationField "$idx" REQUIRE_MOUNT)" == "true" ]]; then
|
|
local tgt=""
|
|
command -v findmnt >/dev/null 2>&1 && tgt=$(findmnt -no TARGET --target "$probe" 2>/dev/null | tail -1)
|
|
if [[ -z "$tgt" || "$tgt" == "/" ]]; then
|
|
isError "Backup location $(resticLocationName "$idx"): REQUIRE_MOUNT is set but '$base' is not on a mounted drive (target: ${tgt:-unknown}). Refusing to back up to the system disk — mount the drive and retry."
|
|
return 1
|
|
fi
|
|
fi
|
|
return 0
|
|
}
|