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

172 lines
4.3 KiB
Bash

#!/bin/bash
resticAllLocationIndices()
{
local dir
dir=$(backupLocationsDir)
if [[ ! -d "$dir" ]]; then
# Pre-migration / legacy fallback: read indices from CFG vars.
compgen -v 2>/dev/null | grep -oE '^CFG_BACKUP_LOC_[0-9]+_NAME$' | grep -oE '[0-9]+' | sort -n -u
return
fi
runFileOp find "$dir" -mindepth 2 -maxdepth 2 -name location.config -type f 2>/dev/null \
| awk -F/ '{print $(NF-1)}' \
| grep -E '^[0-9]+$' \
| sort -n -u
}
resticEnabledLocations()
{
local idx enabled_var
while IFS= read -r idx; do
[[ -z "$idx" ]] && continue
enabled_var="CFG_BACKUP_LOC_${idx}_ENABLED"
if [[ "${!enabled_var}" == "true" ]]; then
echo "$idx"
fi
done < <(resticAllLocationIndices)
}
resticLocationField()
{
local idx="$1"
local field="$2"
local var="CFG_BACKUP_LOC_${idx}_${field}"
echo "${!var}"
}
resticLocationName()
{
local idx="$1"
local name
name=$(resticLocationField "$idx" NAME)
[[ -z "$name" ]] && name="Location $idx"
echo "$name"
}
resticLocationType()
{
local idx="$1"
local t
t=$(resticLocationField "$idx" TYPE)
[[ -z "$t" ]] && t="local"
echo "$t"
}
resticLocationEnabled()
{
local idx="$1"
[[ "$(resticLocationField "$idx" ENABLED)" == "true" ]]
}
resticLocationAppendOnly()
{
local idx="$1"
[[ "$(resticLocationField "$idx" APPEND_ONLY)" == "true" ]]
}
resticLocationUri()
{
local idx="$1"
local override
override=$(resticLocationField "$idx" URI)
if [[ -n "$override" ]]; then
echo "$override"
return
fi
local t
t=$(resticLocationType "$idx")
case "$t" in
local)
backupLocationResolvedPath "$idx"
;;
sftp)
local user host path port
user=$(resticLocationField "$idx" SSH_USER)
host=$(resticLocationField "$idx" SSH_HOST)
path=$(resticLocationField "$idx" SSH_PATH)
echo "sftp:${user}@${host}:${path}"
;;
rest|connect)
# Connect is a REST repository on storage we do not run. The URI
# carries the device credentials; the repo PASSWORD stays here and
# is what the data is encrypted with, so the far side only ever
# holds packs it cannot open.
resticLocationField "$idx" URI
;;
s3|b2|gs|azure|rclone)
resticLocationField "$idx" URI
;;
esac
}
resticLocationPassword()
{
local idx="$1"
local var="CFG_BACKUP_LOC_${idx}_PASSWORD"
echo "${!var}"
}
resticEnvExport()
{
local idx="$1"
local repo_uri
repo_uri=$(resticLocationUri "$idx")
if [[ -z "$repo_uri" ]]; then
isError "Location $idx has no URI configured"
return 1
fi
local pass
pass=$(resticLocationPassword "$idx")
if [[ -z "$pass" || "$pass" == RANDOMIZEDPASSWORD* ]]; then
isError "Location $idx has no password set (CFG_BACKUP_LOC_${idx}_PASSWORD). Run the install/scan password pass first."
return 1
fi
export RESTIC_REPOSITORY="$repo_uri"
export RESTIC_PASSWORD="$pass"
local t
t=$(resticLocationType "$idx")
case "$t" in
sftp)
local port auth ssh_cmd
port=$(resticLocationField "$idx" SSH_PORT)
[[ -z "$port" ]] && port=22
auth=$(resticLocationField "$idx" SSH_AUTH)
ssh_cmd=$(backupSshCommand "$idx" "$port" "sftp") || return 1
export RESTIC_SFTP_COMMAND="$ssh_cmd"
;;
s3)
export AWS_ACCESS_KEY_ID="$(resticLocationField "$idx" S3_ACCESS_KEY)"
export AWS_SECRET_ACCESS_KEY="$(resticLocationField "$idx" S3_SECRET_KEY)"
;;
b2)
export B2_ACCOUNT_ID="$(resticLocationField "$idx" B2_ACCOUNT_ID)"
export B2_ACCOUNT_KEY="$(resticLocationField "$idx" B2_ACCOUNT_KEY)"
;;
esac
}
resticEnvUnset()
{
unset RESTIC_REPOSITORY RESTIC_PASSWORD RESTIC_SFTP_COMMAND
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY
unset B2_ACCOUNT_ID B2_ACCOUNT_KEY
unset SSHPASS
}
resticNextFreeIndex()
{
local last
last=$(resticAllLocationIndices | tail -1)
if [[ -z "$last" ]]; then
echo 1
else
echo $((last + 1))
fi
}