#!/usr/bin/env bash set -euo pipefail # Creates a consistent SQLite backup through the running web container, then archives Hub uploads. # Optional: BACKUP_RETENTION_DAYS=30 BACKUP_OFFSITE_DIR=/mnt/backup/mebbling ./scripts/backup.sh # For encrypted remote copies: BACKUP_AGE_RECIPIENT=age1... BACKUP_RCLONE_REMOTE='remote:bucket/mebbling' ./scripts/backup.sh root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$root_dir" stamp="$(date +%Y%m%d-%H%M%S)" backup_dir="data/backups/$stamp" mkdir -p "$backup_dir" docker compose exec -T -e BACKUP_PATH="/app/data/backups/$stamp/hub.db" web node -e ' const Database = require("better-sqlite3"); const db = new Database(process.env.DATABASE_PATH); db.backup(process.env.BACKUP_PATH).then(() => db.close()).catch((error) => { console.error(error); process.exit(1); }); ' tar -czf "$backup_dir/uploads.tar.gz" -C public uploads docker compose exec -T -e BACKUP_PATH="/app/data/backups/$stamp/hub.db" web node -e ' const Database = require("better-sqlite3"); const db = new Database(process.env.BACKUP_PATH, { readonly: true }); const row = db.prepare("PRAGMA integrity_check").get(); db.close(); if (row.integrity_check !== "ok") { console.error("SQLite integrity check failed"); process.exit(1); } ' tar -tzf "$backup_dir/uploads.tar.gz" >/dev/null (cd "$backup_dir" && sha256sum hub.db uploads.tar.gz > SHA256SUMS) "$root_dir/scripts/verify-backup.sh" "$backup_dir" archive="$backup_dir/backup.tar.gz" tar -czf "$archive" -C "$backup_dir" hub.db uploads.tar.gz SHA256SUMS offsite_payload="$backup_dir" if [[ -n "${BACKUP_AGE_RECIPIENT:-}" ]]; then command -v age >/dev/null || { echo "age is required when BACKUP_AGE_RECIPIENT is set" >&2; exit 2; } age -r "$BACKUP_AGE_RECIPIENT" -o "$archive.age" "$archive" offsite_payload="$archive.age" fi if [[ -n "${BACKUP_OFFSITE_DIR:-}" ]]; then destination="$BACKUP_OFFSITE_DIR/$stamp"; mkdir -p "$destination" if [[ "$offsite_payload" == "$backup_dir" ]]; then cp -a "$backup_dir/." "$destination/"; else cp -a "$offsite_payload" "$destination/"; fi printf 'Copied verified backup to: %s\n' "$destination" fi if [[ -n "${BACKUP_RCLONE_REMOTE:-}" ]]; then command -v rclone >/dev/null || { echo "rclone is required when BACKUP_RCLONE_REMOTE is set" >&2; exit 2; } if [[ "$offsite_payload" == "$backup_dir" ]]; then rclone copy "$backup_dir" "$BACKUP_RCLONE_REMOTE/$stamp"; else rclone copy "$offsite_payload" "$BACKUP_RCLONE_REMOTE/$stamp"; fi printf 'Copied verified backup using rclone to: %s/%s\n' "$BACKUP_RCLONE_REMOTE" "$stamp" fi if [[ -n "${BACKUP_RETENTION_DAYS:-}" ]]; then [[ "$BACKUP_RETENTION_DAYS" =~ ^[0-9]+$ ]] || { echo "BACKUP_RETENTION_DAYS must be a non-negative integer" >&2; exit 2; } find data/backups -mindepth 1 -maxdepth 1 -type d -mtime "+$BACKUP_RETENTION_DAYS" -exec rm -rf {} + fi printf 'Created and verified backup: %s\n' "$backup_dir"