#!/bin/bash # Per-location directory layout helpers. Each backup location is one # directory under configs/backup/locations// containing: # # location.config — sourced at startup, holds CFG_BACKUP_LOC__* # including PASSWORD (auto-randomized from # RANDOMIZEDPASSWORD 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 }