deployment-handoff/phase03/backup-restore-nextcloud.sh
2026-07-17 00:19:46 -06:00

138 lines
6.7 KiB
Bash

#!/bin/sh
set -eu
[ "$(id -u)" = "0" ] || { echo "FAIL must_run_as_root"; exit 1; }
umask 077
rclone_image='rclone/rclone@sha256:623378ad0ff3ebd5cebf77720843c0e02edfe46e2d5b5ac6bed54c6371780dfb'
restic_image='restic/restic@sha256:424a4e1fcc6fe2557b5614239dc71a2c793acb33a83ea217171bd7edc1862dcb'
docker_bin=/usr/local/bin/docker
agent=/usr/local/bin/xtao-secret-agent
base=/volume1/xtao-data/backup-controller
bin="$base/bin"
backup_root=/volume2/xtao-backup/nextcloud
tmp=$(mktemp -d /tmp/xtao-nextcloud-backup.XXXXXX)
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
mkdir -p "$bin" "$backup_root/dumps" "$backup_root/restores" "$backup_root/logs"
chmod 700 "$backup_root" "$backup_root/dumps" "$backup_root/restores" "$backup_root/logs"
"$agent" raw-field --entry external/google-drive/offsite-backup --field rclone_token_json > "$tmp/token.json"
"$agent" raw-field --entry external/google-drive/offsite-backup --field root_folder_id > "$tmp/root_folder_id"
"$agent" raw-field --entry external/google-drive/offsite-backup --field oauth_client_id > "$tmp/oauth_client_id"
"$agent" raw-field --entry external/google-drive/offsite-backup --field oauth_client_secret > "$tmp/oauth_client_secret"
"$agent" raw-field --entry backup/google-drive/restic-repository --field password > "$tmp/restic.pass"
chmod 600 "$tmp/token.json" "$tmp/root_folder_id" "$tmp/oauth_client_id" "$tmp/oauth_client_secret" "$tmp/restic.pass"
python3 - "$tmp/token.json" "$tmp/root_folder_id" "$tmp/oauth_client_id" "$tmp/oauth_client_secret" "$tmp/rclone.conf" <<'PY'
import json, pathlib, sys
token_path, root_path, client_id_path, client_secret_path, config_path = map(pathlib.Path, sys.argv[1:])
token = token_path.read_text(encoding="utf-8").strip()
json.loads(token)
root = root_path.read_text(encoding="utf-8").strip()
if not root or root == "CODEX_GENERATE":
raise SystemExit("FAIL invalid_root_folder_id")
client_id = client_id_path.read_text(encoding="utf-8").strip()
client_secret = client_secret_path.read_text(encoding="utf-8").strip()
client_lines = ""
if client_id and client_id != "NONE":
client_lines += f"client_id = {client_id}\n"
if client_secret:
client_lines += f"client_secret = {client_secret}\n"
config_path.write_text(
"[xtao-gdrive]\n"
"type = drive\n"
"scope = drive.file\n"
f"{client_lines}"
f"root_folder_id = {root}\n"
f"token = {token}\n",
encoding="utf-8",
)
PY
chmod 600 "$tmp/rclone.conf"
if [ ! -x "$bin/rclone" ]; then
cid=$("$docker_bin" create --entrypoint /bin/sh "$rclone_image" -c true)
"$docker_bin" cp "$cid:/usr/local/bin/rclone" "$tmp/rclone"
"$docker_bin" rm "$cid" >/dev/null
install -m 700 "$tmp/rclone" "$bin/rclone"
fi
dbuser=$("$docker_bin" exec -u www-data nextcloud-nextcloud-web-1 php -r 'include "/var/www/html/config/config.php"; echo $CONFIG["dbuser"];')
dbname=$("$docker_bin" exec -u www-data nextcloud-nextcloud-web-1 php -r 'include "/var/www/html/config/config.php"; echo $CONFIG["dbname"];')
case "$dbuser" in *[!A-Za-z0-9_-]*|"") echo "FAIL unexpected_dbuser"; exit 3;; esac
case "$dbname" in *[!A-Za-z0-9_-]*|"") echo "FAIL unexpected_dbname"; exit 3;; esac
ts=$(date -u +%Y%m%dT%H%M%SZ)
dump="$backup_root/dumps/nextcloud-$ts.sql"
"$docker_bin" exec -i nextcloud-nextcloud-db-1 pg_dump -U nextcloud "$dbname" > "$dump"
chmod 600 "$dump"
sample=$(find /volume1/xtao-data/nextcloud/data/xtao-admin/files -maxdepth 1 -type f -name 'phase03-small-*.bin' | sort | tail -n 1)
[ -n "$sample" ] || { echo "FAIL missing_uploaded_canary_file"; exit 3; }
sample_name=$(basename "$sample")
sha256sum "$sample" | awk '{print $1}' > "$tmp/sample.sha256"
restic_env="-e RCLONE_CONFIG=/config/rclone/rclone.conf -e RESTIC_PASSWORD_FILE=/secrets/restic.pass -e RESTIC_REPOSITORY=rclone:xtao-gdrive:restic/nas"
set +e
"$docker_bin" run --rm --network bridge \
-v "$tmp:/config/rclone" \
-v "$tmp/restic.pass:/secrets/restic.pass:ro" \
-v "$bin/rclone:/usr/local/bin/rclone:ro" \
$restic_env \
"$restic_image" init >/tmp/xtao-nextcloud-restic-init.out 2>&1
rc=$?
set -e
if [ "$rc" -ne 0 ] && ! grep -Eq 'already initialized|config file already exists' /tmp/xtao-nextcloud-restic-init.out; then
cat /tmp/xtao-nextcloud-restic-init.out
echo "FAIL restic_init"
exit 2
fi
"$docker_bin" run --rm --network bridge \
-v "$tmp:/config/rclone" \
-v "$tmp/restic.pass:/secrets/restic.pass:ro" \
-v "$bin/rclone:/usr/local/bin/rclone:ro" \
-v "/volume1/xtao-data/nextcloud/data/xtao-admin/files:/backup/files:ro" \
-v "/volume1/xtao-data/nextcloud/html/config:/backup/config:ro" \
-v "$backup_root/dumps:/backup/dbdumps:ro" \
$restic_env \
"$restic_image" backup --tag phase03-nextcloud-$ts /backup/files /backup/config /backup/dbdumps >/tmp/xtao-nextcloud-restic-backup.out
restore="$backup_root/restores/restore-$ts"
mkdir -p "$restore"
chmod 700 "$restore"
"$docker_bin" run --rm --network bridge \
-v "$tmp:/config/rclone" \
-v "$tmp/restic.pass:/secrets/restic.pass:ro" \
-v "$bin/rclone:/usr/local/bin/rclone:ro" \
-v "$restore:/restore" \
$restic_env \
"$restic_image" restore latest --tag "phase03-nextcloud-$ts" --target /restore >/tmp/xtao-nextcloud-restic-restore.out
restored_sample="$restore/backup/files/$sample_name"
[ -f "$restored_sample" ] || { echo "FAIL restored_sample_missing"; exit 3; }
sha256sum "$restored_sample" | awk '{print $1}' > "$tmp/restored.sha256"
cmp "$tmp/sample.sha256" "$tmp/restored.sha256"
[ -f "$restore/backup/config/config.php" ] || { echo "FAIL restored_config_missing"; exit 3; }
[ -f "$restore/backup/dbdumps/$(basename "$dump")" ] || { echo "FAIL restored_dump_missing"; exit 3; }
restore_db="nc_restore_$(date -u +%H%M%S)"
"$docker_bin" exec -i nextcloud-nextcloud-db-1 createdb -U nextcloud "$restore_db"
set +e
"$docker_bin" exec -i nextcloud-nextcloud-db-1 psql -U nextcloud "$restore_db" < "$restore/backup/dbdumps/$(basename "$dump")" >/tmp/xtao-nextcloud-db-restore.out 2>&1
restore_rc=$?
set -e
if [ "$restore_rc" -ne 0 ]; then
"$docker_bin" exec -i nextcloud-nextcloud-db-1 dropdb -U nextcloud "$restore_db" || true
cat /tmp/xtao-nextcloud-db-restore.out
echo "FAIL isolated_db_restore"
exit 3
fi
user_count=$("$docker_bin" exec -i nextcloud-nextcloud-db-1 psql -U nextcloud "$restore_db" -Atc "select count(*) from oc_users where uid='xtao-admin';")
file_count=$("$docker_bin" exec -i nextcloud-nextcloud-db-1 psql -U nextcloud "$restore_db" -Atc "select count(*) from oc_filecache where path='files/$sample_name';")
"$docker_bin" exec -i nextcloud-nextcloud-db-1 dropdb -U nextcloud "$restore_db"
[ "$user_count" = "1" ] || { echo "FAIL restored_user_missing"; exit 3; }
[ "$file_count" != "0" ] || { echo "FAIL restored_filecache_missing"; exit 3; }
printf 'PASS nextcloud_backup_restore restic=PASS isolated_db=PASS restored_file=%s values=redacted\n' "$sample_name"