librelad 61cebb5ab8 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>
2026-05-25 15:35:00 +01:00

75 lines
4.4 KiB
Bash

#!/bin/bash
locationAdd()
{
local name="$1"
local type="${2:-local}"
if [[ -z "$name" ]]; then
isError "locationAdd requires a name"
return 1
fi
case "$type" in
local|sftp|rest|s3|b2|gs|azure|rclone) ;;
*) isError "Unsupported location type: $type"; return 1 ;;
esac
local idx
idx=$(resticNextFreeIndex)
local default_engine="${CFG_BACKUP_ENGINE:-restic}"
local default_path_mode="auto"
local default_path=""
backupLocationEnsureDir "$idx"
local cfg_file
cfg_file=$(backupLocationConfig "$idx")
local owner
owner=$(backupLocationOwner)
{
echo "# Backup location $idx — added $(date -Iseconds)."
echo "# Edit via the Locations page on /backup, or directly here."
echo "CFG_BACKUP_LOC_${idx}_NAME=\"${name}\" # Location Name - Friendly label shown in the UI"
echo "CFG_BACKUP_LOC_${idx}_ENABLED=false # Enabled - Snapshot to this location"
echo "CFG_BACKUP_LOC_${idx}_ENGINE=${default_engine} # Engine - Backup engine used at this location [restic:Restic|borg:BorgBackup|kopia:Kopia] **ADVANCED**"
echo "CFG_BACKUP_LOC_${idx}_PASSWORD=RANDOMIZEDPASSWORD1 # Repository Password - Used to encrypt/decrypt snapshots — back up offline!"
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"
echo "CFG_BACKUP_LOC_${idx}_SSH_PORT=22 # SSH Port - For sftp type **ADVANCED**"
echo "CFG_BACKUP_LOC_${idx}_SSH_PATH= # SSH Remote Path - Path on the remote host where the repo lives"
echo "CFG_BACKUP_LOC_${idx}_SSH_AUTH=key # SSH Authentication - [key:SSH key (managed by LibrePortal)|password:Password (via sshpass)]"
echo "CFG_BACKUP_LOC_${idx}_SSH_PASS= # SSH Password - Used only when SSH Authentication is set to Password"
echo "CFG_BACKUP_LOC_${idx}_S3_ACCESS_KEY="
echo "CFG_BACKUP_LOC_${idx}_S3_SECRET_KEY="
echo "CFG_BACKUP_LOC_${idx}_B2_ACCOUNT_ID="
echo "CFG_BACKUP_LOC_${idx}_B2_ACCOUNT_KEY="
echo "CFG_BACKUP_LOC_${idx}_APPEND_ONLY=false # Append-only - Refuse forget/prune for this location (ransomware-safe) **ADVANCED**"
echo "CFG_BACKUP_LOC_${idx}_CUSTOM_RETENTION=false"
echo "CFG_BACKUP_LOC_${idx}_KEEP_LAST="
echo "CFG_BACKUP_LOC_${idx}_KEEP_DAILY="
echo "CFG_BACKUP_LOC_${idx}_KEEP_WEEKLY="
echo "CFG_BACKUP_LOC_${idx}_KEEP_MONTHLY="
echo "CFG_BACKUP_LOC_${idx}_KEEP_YEARLY="
} | runFileWrite "$cfg_file" >/dev/null
runFileOp chown "$owner":"$owner" "$cfg_file"
runFileOp chmod 0640 "$cfg_file"
if declare -f replacePlainPasswords >/dev/null 2>&1; then
replacePlainPasswords "$cfg_file"
fi
source "$cfg_file"
isSuccessful "Location $idx '$name' added at $(backupLocationDir "$idx") (type: $type, engine: $default_engine, disabled by default)"
if declare -f webuiGenerateBackupLocations >/dev/null 2>&1; then
webuiGenerateBackupLocations
fi
echo "$idx"
}