From 973444bdbb9d9c7680e3d3de23b84d86d37e8fe9 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Mon, 11 May 2026 03:51:13 +0200 Subject: [PATCH] feat(backup): deploy cron-based SQLite backups to production - Create deploy/xamxam-backup.cron with hourly (30d) and daily (90d) jobs - Add just recipes for deploying backup infrastructure: - deploy-backup-script: upload backup-sqlite.sh to /usr/local/bin - deploy-backup-cron: install cron.d file, create /var/backups/xamxam + log - deploy-backup: one-shot convenience (script + cron) - deploy-check-backup-log: tail the backup log - deploy-list-backups: ls remote backup directory - trigger-backup: manually invoke backup on server - test-restore: scp, gunzip, verify a remote snapshot - Add reminder to run deploy-backup after first deploy - Replace 'Contenu (Markdown)' label with 'Syntax Markdown' link (cheatsheet) --- TODO.md | 12 +- .../admin/form-help-inline-fragment.php | 3 +- app/templates/admin/acces.php | 13 + deploy/xamxam-backup.cron | 8 + justfile | 326 +++++++++++------- scripts/backup-sqlite.sh | 5 +- 6 files changed, 230 insertions(+), 137 deletions(-) create mode 100644 deploy/xamxam-backup.cron diff --git a/TODO.md b/TODO.md index b399708..32ac85f 100644 --- a/TODO.md +++ b/TODO.md @@ -47,10 +47,14 @@ - [x] Create `scripts/backup-sqlite.sh` (hot backup via `sqlite3 .backup`, gzip, retention pruning) - [x] Test locally — backup created, restores correctly - [x] Add `just backup-snapshot` command for local ad-hoc backups -- [ ] Deploy backup script to server (`/usr/local/bin/backup-sqlite.sh`) -- [ ] Create `/var/backups/xamxam/` directory on server -- [ ] Add cron jobs (hourly 30d + daily 90d) -- [ ] Test restore from production backup +- [x] Deploy backup script to server (`/usr/local/bin/backup-sqlite.sh`) — `just deploy-backup-script` +- [x] Create `/var/backups/xamxam/` directory on server — part of `just deploy-backup-cron` +- [x] Add cron jobs (hourly 30d + daily 90d) — `just deploy-backup-cron` +- [x] Test restore from production backup — `just test-restore ` +- [x] Manual backup trigger — `just trigger-backup` +- [x] Check backup log — `just deploy-check-backup-log` +- [x] List remote backups — `just deploy-list-backups` +- [x] One-shot deploy — `just deploy-backup` (script + cron) ### Phase 5 — Remote Sync *(for later)* - [ ] (Deferred) diff --git a/app/public/admin/form-help-inline-fragment.php b/app/public/admin/form-help-inline-fragment.php index 885d051..5d66763 100644 --- a/app/public/admin/form-help-inline-fragment.php +++ b/app/public/admin/form-help-inline-fragment.php @@ -157,9 +157,10 @@ function renderEditor(Database $db, string $key): void + -
+
diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php index a0474ac..f7a7ead 100644 --- a/app/templates/admin/acces.php +++ b/app/templates/admin/acces.php @@ -753,6 +753,19 @@ +%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +\\\\\\\ to: pqnovwxx eb519770 "fix(production): fix multiple remote server errors from nginx logs" (rebased revision) ++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: pqnovwxx eb519770 "fix(production): fix multiple remote server errors from nginx logs" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: pylyqurz a49fe41b "feat(backup): deploy cron-based SQLite backups to production" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: pylyqurz b7080cbb "feat(backup): deploy cron-based SQLite backups to production" (rebased revision) +++ $linkName = $link['name'] ?? ''; ++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; ?> diff --git a/deploy/xamxam-backup.cron b/deploy/xamxam-backup.cron new file mode 100644 index 0000000..a4237c1 --- /dev/null +++ b/deploy/xamxam-backup.cron @@ -0,0 +1,8 @@ +# XAMXAM SQLite backup cron jobs +# Installed to /etc/cron.d/xamxam-backup (system cron format: minute hour dom month dow user command) +# +# Hourly snapshot — kept 30 days +0 * * * * www-data /usr/local/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1 + +# Daily snapshot at 2am — kept 90 days +0 2 * * * www-data RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1 diff --git a/justfile b/justfile index adb4aeb..8683724 100644 --- a/justfile +++ b/justfile @@ -62,7 +62,7 @@ deploy: app/ xamxam:/var/www/xamxam/ # Upload deploy-server.sh for post-deploy permission fix rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh - ssh xamxam "sudo bash /tmp/deploy-server.sh" + ssh -t xamxam "sudo bash /tmp/deploy-server.sh" ssh xamxam "rm -f /tmp/deploy-server.sh" ssh xamxam "mkdir -p /var/www/xamxam/var/{cache,logs,tmp}" ssh xamxam "cd /var/www/xamxam && php -r 'if (!file_exists(\"/var/www/xamxam/storage/xamxam.db\")) { \$db = new PDO(\"sqlite:/var/www/xamxam/storage/xamxam.db\"); \$db->exec(file_get_contents(\"/var/www/xamxam/storage/schema.sql\")); echo \"Database created from schema.\\n\"; } else { echo \"Database already exists.\\n\"; }'" @@ -73,42 +73,45 @@ deploy: # Sync .env separately (excluded above to avoid accidental overwrite on subsequent deploys) @just deploy-env @just deploy-verify-permissions + @echo "" + @echo "ℹ️ First deploy? Also run: just deploy-backup" + @echo "" [group('deploy')] deploy-env: - #!/usr/bin/env bash - set -euo pipefail - # Upload app/.env only if it exists locally; never overwrites a remote .env that already has APP_KEY. - if [ ! -f app/.env ]; then - echo "WARNING: app/.env not found locally — skipping." - exit 0 - fi - if ssh xamxam '[ -f /var/www/xamxam/.env ]'; then - echo "Remote .env already exists — skipping to avoid overwriting key." - echo "Run 'just reencrypt-password' if you rotated APP_KEY." - else - rsync -v --progress app/.env xamxam:/var/www/xamxam/.env - ssh xamxam "chmod 640 /var/www/xamxam/.env && chown www-data:xamxam /var/www/xamxam/.env" - echo ".env uploaded." - fi + #!/usr/bin/env bash + set -euo pipefail + # Upload app/.env only if it exists locally; never overwrites a remote .env that already has APP_KEY. + if [ ! -f app/.env ]; then + echo "WARNING: app/.env not found locally — skipping." + exit 0 + fi + if ssh xamxam '[ -f /var/www/xamxam/.env ]'; then + echo "Remote .env already exists — skipping to avoid overwriting key." + echo "Run 'just reencrypt-password' if you rotated APP_KEY." + else + rsync -v --progress app/.env xamxam:/var/www/xamxam/.env + ssh xamxam "chmod 640 /var/www/xamxam/.env && chown www-data:xamxam /var/www/xamxam/.env" + echo ".env uploaded." + fi [group('deploy')] reencrypt-password new_key_b64="": - #!/usr/bin/env bash - set -euo pipefail - # Re-encrypt the SMTP password in the remote DB after rotating APP_KEY. - # Usage: - # 1. Generate a new key: php -r "echo base64_encode(random_bytes(32));" - # 2. Run: just reencrypt-password - # 3. Update app/.env locally with the new key, then run: just deploy-env - if [ -z "{{new_key_b64}}" ]; then - echo "Usage: just reencrypt-password " - echo "Generate a key: php -r \"echo base64_encode(random_bytes(32));\"" - exit 1 - fi - # Run the re-encryption script on the server using the current key (from remote .env) - # and the supplied new key. - ssh xamxam "php /var/www/xamxam/scripts/reencrypt-smtp-password.php '{{new_key_b64}}' /var/www/xamxam/storage/xamxam.db" + #!/usr/bin/env bash + set -euo pipefail + # Re-encrypt the SMTP password in the remote DB after rotating APP_KEY. + # Usage: + # 1. Generate a new key: php -r "echo base64_encode(random_bytes(32));" + # 2. Run: just reencrypt-password + # 3. Update app/.env locally with the new key, then run: just deploy-env + if [ -z "{{new_key_b64}}" ]; then + echo "Usage: just reencrypt-password " + echo "Generate a key: php -r \"echo base64_encode(random_bytes(32));\"" + exit 1 + fi + # Run the re-encryption script on the server using the current key (from remote .env) + # and the supplied new key. + ssh xamxam "php /var/www/xamxam/scripts/reencrypt-smtp-password.php '{{new_key_b64}}' /var/www/xamxam/storage/xamxam.db" [group('deploy')] deploy-db: @@ -118,115 +121,115 @@ deploy-db: [group('deploy')] deploy-verify-permissions: - #!/usr/bin/env bash - set -euo pipefail - APP_DIR="/var/www/xamxam" - WEB_USER="www-data" - APP_GROUP="xamxam" - ERRORS=0 + #!/usr/bin/env bash + set -euo pipefail + APP_DIR="/var/www/xamxam" + WEB_USER="www-data" + APP_GROUP="xamxam" + ERRORS=0 - RED='\033[0;31m' - GREEN='\033[0;32m' - YELLOW='\033[1;33m' - NC='\033[0m' + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + NC='\033[0m' - ok() { printf "${GREEN}✓${NC} %s\n" "$*"; } - err() { printf "${RED}✗${NC} %s\n" "$*" >&2; ERRORS=$((ERRORS + 1)); } - warn() { printf "${YELLOW}!${NC} %s\n" "$*"; } + ok() { printf "${GREEN}✓${NC} %s\n" "$*"; } + err() { printf "${RED}✗${NC} %s\n" "$*" >&2; ERRORS=$((ERRORS + 1)); } + warn() { printf "${YELLOW}!${NC} %s\n" "$*"; } - printf "🔍 Verifying permissions on %s…\n\n" "$APP_DIR" + printf "🔍 Verifying permissions on %s…\n\n" "$APP_DIR" - # ── Ownership ────────────────────────────────────────────────────────────────── - echo "── Ownership ───────────────────────────────────" - while IFS= read -r line; do - owner=$(echo "$line" | awk '{print $3}') - group=$(echo "$line" | awk '{print $4}') - path=$(echo "$line" | awk '{print $NF}') - if [ "$owner" != "$WEB_USER" ] || [ "$group" != "$APP_GROUP" ]; then - err "$path → $owner:$group (expected $WEB_USER:$APP_GROUP)" - else - ok "$path → $owner:$group" - fi - done < <(ssh xamxam "stat -c '%U %G %n' $APP_DIR $APP_DIR/app $APP_DIR/storage $APP_DIR/var 2>/dev/null") + # ── Ownership ────────────────────────────────────────────────────────────────── + echo "── Ownership ───────────────────────────────────" + while IFS= read -r line; do + owner=$(echo "$line" | awk '{print $3}') + group=$(echo "$line" | awk '{print $4}') + path=$(echo "$line" | awk '{print $NF}') + if [ "$owner" != "$WEB_USER" ] || [ "$group" != "$APP_GROUP" ]; then + err "$path → $owner:$group (expected $WEB_USER:$APP_GROUP)" + else + ok "$path → $owner:$group" + fi + done < <(ssh xamxam "stat -c '%U %G %n' $APP_DIR $APP_DIR/app $APP_DIR/storage $APP_DIR/var 2>/dev/null") - # ── Key directories: 2775 ───────────────────────────────────────────────────── - echo "── Directory permissions (expected 2775) ───────" - while IFS= read -r line; do - perms=$(echo "$line" | awk '{print $1}') - path=$(echo "$line" | awk '{print $NF}') - if [ "$perms" != "drwxrwsr-x" ]; then - err "$path → $perms (expected drwxrwsr-x / 2775)" - else - ok "$path → $perms" - fi - done < <(ssh xamxam "find $APP_DIR -maxdepth 2 -type d -exec stat -c '%A %n' {} \\; 2>/dev/null | sort") + # ── Key directories: 2775 ───────────────────────────────────────────────────── + echo "── Directory permissions (expected 2775) ───────" + while IFS= read -r line; do + perms=$(echo "$line" | awk '{print $1}') + path=$(echo "$line" | awk '{print $NF}') + if [ "$perms" != "drwxrwsr-x" ]; then + err "$path → $perms (expected drwxrwsr-x / 2775)" + else + ok "$path → $perms" + fi + done < <(ssh xamxam "find $APP_DIR -maxdepth 2 -type d -exec stat -c '%A %n' {} \\; 2>/dev/null | sort") - # ── Key files: 664 ──────────────────────────────────────────────────────────── - echo "── File permissions (expected 664 / 660) ───────" - # Spot-check a few critical files - while IFS= read -r path; do - perms=$(ssh xamxam "stat -c '%a %U %G' '$path' 2>/dev/null" || echo "MISSING") - if [ "$perms" = "MISSING" ]; then - err "$path → FILE MISSING" - else - perm_num=$(echo "$perms" | awk '{print $1}') - owner=$(echo "$perms" | awk '{print $2}') - group=$(echo "$perms" | awk '{print $3}') - case "$path" in - */storage/xamxam.db|*/storage/*.db) - expected_perm="660" ;; - *) - expected_perm="664" ;; - esac - if [ "$perm_num" != "$expected_perm" ]; then - err "$path → $perm_num ($owner:$group), expected $expected_perm $WEB_USER:$APP_GROUP" - elif [ "$owner" != "$WEB_USER" ]; then - err "$path → owner $owner, expected $WEB_USER (perm $perm_num OK)" - else - ok "$path → $perm_num $owner:$group" - fi - fi - done < <(printf '%s\n' \ - "$APP_DIR/.env" \ - "$APP_DIR/app/router.php" \ - "$APP_DIR/storage/xamxam.db") + # ── Key files: 664 ──────────────────────────────────────────────────────────── + echo "── File permissions (expected 664 / 660) ───────" + # Spot-check a few critical files + while IFS= read -r path; do + perms=$(ssh xamxam "stat -c '%a %U %G' '$path' 2>/dev/null" || echo "MISSING") + if [ "$perms" = "MISSING" ]; then + err "$path → FILE MISSING" + else + perm_num=$(echo "$perms" | awk '{print $1}') + owner=$(echo "$perms" | awk '{print $2}') + group=$(echo "$perms" | awk '{print $3}') + case "$path" in + */storage/xamxam.db|*/storage/*.db) + expected_perm="660" ;; + *) + expected_perm="664" ;; + esac + if [ "$perm_num" != "$expected_perm" ]; then + err "$path → $perm_num ($owner:$group), expected $expected_perm $WEB_USER:$APP_GROUP" + elif [ "$owner" != "$WEB_USER" ]; then + err "$path → owner $owner, expected $WEB_USER (perm $perm_num OK)" + else + ok "$path → $perm_num $owner:$group" + fi + fi + done < <(printf '%s\n' \ + "$APP_DIR/.env" \ + "$APP_DIR/app/router.php" \ + "$APP_DIR/storage/xamxam.db") - # ── var/ subdirectories must be writable ────────────────────────────────────── - echo "── var/ writability ────────────────────────────" - for subdir in cache logs tmp; do - if ssh xamxam "[ -w /var/www/xamxam/var/$subdir ]"; then - ok "var/$subdir → writable" - else - err "var/$subdir → NOT WRITABLE" - fi - done + # ── var/ subdirectories must be writable ────────────────────────────────────── + echo "── var/ writability ────────────────────────────" + for subdir in cache logs tmp; do + if ssh xamxam "[ -w /var/www/xamxam/var/$subdir ]"; then + ok "var/$subdir → writable" + else + err "var/$subdir → NOT WRITABLE" + fi + done - # ── storage/cache/rate_limit writable ───────────────────────────────────────── - if ssh xamxam "[ -w /var/www/xamxam/storage/cache/rate_limit ]"; then - ok "storage/cache/rate_limit → writable" - else - err "storage/cache/rate_limit → NOT WRITABLE" - fi + # ── storage/cache/rate_limit writable ───────────────────────────────────────── + if ssh xamxam "[ -w /var/www/xamxam/storage/cache/rate_limit ]"; then + ok "storage/cache/rate_limit → writable" + else + err "storage/cache/rate_limit → NOT WRITABLE" + fi - # ── .env must be 640 ────────────────────────────────────────────────────────── - env_perm=$(ssh xamxam "stat -c '%a' /var/www/xamxam/.env 2>/dev/null" || echo "") - if [ "$env_perm" = "640" ]; then - ok ".env → 640" - elif [ -z "$env_perm" ]; then - warn ".env → MISSING" - else - err ".env → $env_perm (expected 640)" - fi + # ── .env must be 640 ────────────────────────────────────────────────────────── + env_perm=$(ssh xamxam "stat -c '%a' /var/www/xamxam/.env 2>/dev/null" || echo "") + if [ "$env_perm" = "640" ]; then + ok ".env → 640" + elif [ -z "$env_perm" ]; then + warn ".env → MISSING" + else + err ".env → $env_perm (expected 640)" + fi - # ── Summary ─────────────────────────────────────────────────────────────────── - echo "" - if [ "$ERRORS" -eq 0 ]; then - printf "${GREEN}✅ All permissions OK${NC}\n" - else - printf "${RED}❌ %d permission error(s) found${NC}\n" "$ERRORS" - printf "${YELLOW}Fix with: sudo bash /tmp/deploy-server.sh${NC}\n" - exit 1 - fi + # ── Summary ─────────────────────────────────────────────────────────────────── + echo "" + if [ "$ERRORS" -eq 0 ]; then + printf "${GREEN}✅ All permissions OK${NC}\n" + else + printf "${RED}❌ %d permission error(s) found${NC}\n" "$ERRORS" + printf "${YELLOW}Fix with: sudo bash /tmp/deploy-server.sh${NC}\n" + exit 1 + fi [group('deploy')] deploy-nginx: @@ -236,7 +239,7 @@ deploy-nginx: @echo "📋 Deploying nginx configuration…" rsync -v nginx/xamxam.conf xamxam:/tmp/xamxam.conf rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh - ssh xamxam "sudo DEPLOY_USER=\$USER bash /tmp/deploy-server.sh" + ssh -t xamxam "sudo DEPLOY_USER=\$USER bash /tmp/deploy-server.sh" ssh xamxam "rm -f /tmp/deploy-server.sh /tmp/xamxam.conf" [group('deploy')] @@ -249,6 +252,67 @@ deploy-script script_name: @echo " sudo DEPLOY_USER=\$USER bash /tmp/{{script_name}}.sh" @echo "" +[group('deploy')] +deploy-backup-script: + # Upload backup-sqlite.sh to /usr/local/bin on the server (requires sudo) + # Run once after initial deploy or when the backup script changes. + @echo "📋 Deploying backup script…" + rsync -v scripts/backup-sqlite.sh xamxam:/tmp/backup-sqlite.sh + ssh -t xamxam "sudo install -o root -g root -m 755 /tmp/backup-sqlite.sh /usr/local/bin/backup-sqlite.sh && rm -f /tmp/backup-sqlite.sh" + @echo "✅ backup-sqlite.sh installed to /usr/local/bin/" + +[group('deploy')] +deploy-backup-cron: + # Install cron jobs for hourly (30d retention) and daily (90d) backups. + # Uses /etc/cron.d/xamxam-backup (system cron format: minute hour dom month dow user command) + # Creates backup directory and log file on the server. + @echo "📋 Installing backup cron jobs…" + rsync -v deploy/xamxam-backup.cron xamxam:/tmp/xamxam-backup.cron + ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-backup.cron /etc/cron.d/xamxam-backup && rm -f /tmp/xamxam-backup.cron" + ssh -t xamxam "sudo mkdir -p /var/backups/xamxam && sudo chown www-data:www-data /var/backups/xamxam && sudo chmod 755 /var/backups/xamxam" + ssh -t xamxam "sudo touch /var/log/sqlite-backup.log && sudo chown www-data:www-data /var/log/sqlite-backup.log && sudo chmod 644 /var/log/sqlite-backup.log" + @echo "✅ Cron jobs installed." + @echo " Cron file: /etc/cron.d/xamxam-backup" + @echo " Backup dir: /var/backups/xamxam" + @echo " Log file: /var/log/sqlite-backup.log" + @echo "" + @echo "Verify with: just deploy-check-backup-log" + +[group('deploy')] +deploy-backup: deploy-backup-script deploy-backup-cron + # One-shot: deploy backup script + install cron jobs + set up directories. + +[group('deploy')] +deploy-check-backup-log: + # Show the last 20 lines of the SQLite backup log on the server. + ssh -t xamxam "sudo tail -20 /var/log/sqlite-backup.log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'" + +[group('deploy')] +deploy-list-backups: + # List all existing backups on the server (most recent last). + ssh xamxam "ls -lth /var/backups/xamxam/ 2>/dev/null || echo 'No backups yet.'" + +[group('deploy')] +test-restore remote_gz_path: + # Test-restore a production backup snapshot to a local temp DB and verify. + # Usage: just test-restore /var/backups/xamxam/db-2026-05-11T14-00-00.db.gz + @scp xamxam:'{{remote_gz_path}}' /tmp/xamxam-restore-test.db.gz + @gunzip -c /tmp/xamxam-restore-test.db.gz > /tmp/xamxam-restore-test.db + @echo "Tables in snapshot:" + @sqlite3 /tmp/xamxam-restore-test.db ".tables" + @echo "" + @echo "Thesis count:" + @sqlite3 /tmp/xamxam-restore-test.db "SELECT COUNT(*) FROM theses WHERE deleted_at IS NULL;" + @echo "" + @echo "✅ Snapshot is valid. Remove temp files:" + @echo " rm /tmp/xamxam-restore-test.db /tmp/xamxam-restore-test.db.gz" + @rm -f /tmp/xamxam-restore-test.db.gz + +[group('deploy')] +trigger-backup: + # Manually trigger the backup script on the server now (doesn't wait for cron). + ssh -t xamxam "sudo -u www-data /usr/local/bin/backup-sqlite.sh" + # ============================================================================ # Testing # ============================================================================ diff --git a/scripts/backup-sqlite.sh b/scripts/backup-sqlite.sh index df5dea8..c6d4fb8 100755 --- a/scripts/backup-sqlite.sh +++ b/scripts/backup-sqlite.sh @@ -22,7 +22,10 @@ TIMESTAMP=$(date +"%Y-%m-%dT%H-%M-%S") BACKUP_FILE="$BACKUP_DIR/db-$TIMESTAMP.db.gz" TMP_SNAPSHOT="/tmp/xamxam-snapshot-$$.db" -mkdir -p "$BACKUP_DIR" +mkdir -p "$BACKUP_DIR" 2>/dev/null || { + echo "ERROR: Cannot create backup directory '$BACKUP_DIR'. Run: just deploy-backup-cron" >&2 + exit 1 +} # Safe hot backup using SQLite's online backup API sqlite3 "$DB_PATH" ".backup $TMP_SNAPSHOT"