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>
This commit is contained in:
parent
5bc1abb5e1
commit
61cebb5ab8
@ -36,15 +36,18 @@ engineDispatch()
|
||||
|
||||
# ---- Idx-scoped dispatchers ----------------------------------------------------
|
||||
|
||||
engineInitLocation() { local i="$1"; engineDispatch "$(engineForLocation "$i")InitLocation" "$i"; }
|
||||
engineEnsureLocationReady() { local i="$1"; engineDispatch "$(engineForLocation "$i")EnsureLocationReady" "$i"; }
|
||||
# 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; engineDispatch "$(engineForLocation "$i")BackupAppToLocation" "$i" "$@"; }
|
||||
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" "$@"; }
|
||||
|
||||
@ -37,6 +37,7 @@ locationAdd()
|
||||
echo "CFG_BACKUP_LOC_${idx}_TYPE=${type} # Type - Backend [local:Local / mounted path|sftp:SFTP|rest:REST|s3:S3|b2:Backblaze B2|gs:Google Cloud Storage|azure:Azure|rclone:rclone]"
|
||||
echo "CFG_BACKUP_LOC_${idx}_PATH_MODE=${default_path_mode} # Path Mode - Automatic uses the Default Backup Location from the Backup Engine config (one subfolder per location); Custom uses the path below [auto:Automatic|custom:Custom path]"
|
||||
echo "CFG_BACKUP_LOC_${idx}_PATH=${default_path} # Custom Path - Filesystem path on this server (used when Path Mode = Custom)"
|
||||
echo "CFG_BACKUP_LOC_${idx}_REQUIRE_MOUNT=false # Require Mounted Drive - For an external/removable disk: refuse to back up unless the path is on a real mount, so an unplugged drive never silently fills the system disk [true:Yes|false:No]"
|
||||
echo "CFG_BACKUP_LOC_${idx}_URI= # URI Override - Custom restic URI (leave blank to build from the fields below) **ADVANCED**"
|
||||
echo "CFG_BACKUP_LOC_${idx}_SSH_USER= # SSH User - For sftp type"
|
||||
echo "CFG_BACKUP_LOC_${idx}_SSH_HOST= # SSH Host - For sftp type"
|
||||
|
||||
@ -75,3 +75,45 @@ backupLocationResolvedPath()
|
||||
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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user