librelad 99e81e9ab8 backup: LibrePortal Connect as a destination type, greyed out until it exists
promise.md names Connect as a paid service for "keeping off-site backups", with
two constraints that are load-bearing rather than marketing: it never sees your
data, and every hosted service has a free equivalent in the open code. Nothing
was implemented — no endpoint, no account, no client support.

The client half turns out to be almost entirely there, because a Connect
destination is not a new kind of thing: it is a restic REST repository whose
password never leaves the machine. So `connect` resolves exactly like `rest` in
resticLocationUri — it IS one. It is a separate TYPE only so the UI can tell it
apart from a REST server someone runs themselves, which are identical on disk.

Availability is data, not code: CFG_BACKUP_CONNECT_ENDPOINT (empty) is reported
through the locations feed as connect:{available,endpoint}, and the wizard
renders from that — the option present but disabled, its panel saying what it
will be and that SFTP and S3 do the same job today. The day the service exists,
setting that one value turns it on with no release. Verified both ways.

The device code is a credential, so it goes through the secret channel as a
reference rather than travelling in the wizard payload, which is base64'd into a
world-readable task file.

Design, and what the service still has to provide, in
docs/roadmap/connect-backup-destination.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:21:37 +01:00

81 lines
4.8 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
# connect resolves exactly like rest — it IS a restic REST repository.
# It is its own type only so the UI can tell a LibrePortal Connect
# destination apart from a REST server someone runs themselves, which
# otherwise look identical on disk.
local|sftp|rest|connect|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="
# runInstallWrite/runInstallOp: configs/ is manager-owned, and the container
# user these used to run as cannot write there — the write failed while the
# success message printed anyway.
} | runInstallWrite "$cfg_file" >/dev/null
runInstallOp chmod 0644 "$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"
}