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>
172 lines
4.3 KiB
Bash
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
|
|
}
|