diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c561a0..bbd4e53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ ### Added +- Added verified SQLite/upload backups with retention, optional offsite copy, restore script, and installable daily WSL cron schedule. - Added expiring, one-time source invitation links with viewer and editor roles; only owners and editors can publish to a shared Memos source. - Added resumable, page-token-based Memos imports with configurable batch size and first-import limit; posts are only hidden after a complete scan. - Added owner/member-only JSON and Markdown exports for individual posts and complete sources. diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 1a428c8..fc42071 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -13,15 +13,27 @@ - `hub.db`:由正在執行的 SQLite 資料庫建立的一致性備份。 - `uploads.tar.gz`:Hub 本機上傳的附件。 -`data/backups/` 已由 Git 排除。請將備份複製到另一台主機或加密的雲端儲存;只留在同一台機器不算完整備份。 +每次備份都會執行 SQLite `integrity_check`、驗證附件壓縮檔,並在 `SHA256SUMS` 記錄雜湊。`data/backups/` 已由 Git 排除。 + +可設定保留與異地複製(例如掛載的 NAS、加密磁碟或 rclone 掛載點): + +```bash +BACKUP_RETENTION_DAYS=30 BACKUP_OFFSITE_DIR=/mnt/nas/mebbling ./scripts/backup.sh +``` + +在 WSL 主機安裝每日 03:15 排程: + +```bash +./scripts/install-backup-cron.sh +``` ## 還原 -1. 停止服務:`docker compose down`。 -2. 備份目前的 `data/hub.db` 與 `public/uploads/`,以免操作失誤。 -3. 將選定備份中的 `hub.db` 覆蓋為 `data/hub.db`。 -4. 解開附件:`tar -xzf data/backups/<時間>/uploads.tar.gz -C public`。 -5. 重新啟動:`docker compose up -d`。 +先在非正式環境執行還原演練;腳本會驗證雜湊、停止服務、保留原本資料庫副本、還原資料庫與附件後重新啟動: + +```bash +CONFIRM_RESTORE=YES ./scripts/restore.sh data/backups/<時間> +``` 請始終一起還原資料庫與附件,否則貼文中的附件連結可能失效。 diff --git a/scripts/backup.sh b/scripts/backup.sh index 2c82928..a3ee085 100644 --- a/scripts/backup.sh +++ b/scripts/backup.sh @@ -2,6 +2,7 @@ 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 root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$root_dir" stamp="$(date +%Y%m%d-%H%M%S)" @@ -15,4 +16,21 @@ docker compose exec -T -e BACKUP_PATH="/app/data/backups/$stamp/hub.db" web node ' tar -czf "$backup_dir/uploads.tar.gz" -C public uploads -printf 'Created backup: %s\n' "$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.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) + +if [[ -n "${BACKUP_OFFSITE_DIR:-}" ]]; then + destination="$BACKUP_OFFSITE_DIR/$stamp"; mkdir -p "$destination" + cp -a "$backup_dir/." "$destination/" + printf 'Copied verified backup to: %s\n' "$destination" +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" diff --git a/scripts/install-backup-cron.sh b/scripts/install-backup-cron.sh new file mode 100644 index 0000000..cc70d7a --- /dev/null +++ b/scripts/install-backup-cron.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Installs a daily 03:15 host cron job. Run from WSL after setting an offsite mount if desired. +root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +line="15 3 * * * cd $root_dir && BACKUP_RETENTION_DAYS=30 ./scripts/backup.sh >> data/backup-cron.log 2>&1" +(crontab -l 2>/dev/null | grep -Fv "scripts/backup.sh"; echo "$line") | crontab - +printf 'Installed daily backup cron job. Inspect with: crontab -l\n' diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100644 index 0000000..80ed061 --- /dev/null +++ b/scripts/restore.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then echo "Usage: CONFIRM_RESTORE=YES ./scripts/restore.sh data/backups/YYYYMMDD-HHMMSS" >&2; exit 2; fi +if [[ "${CONFIRM_RESTORE:-}" != "YES" ]]; then echo "Refusing restore. Set CONFIRM_RESTORE=YES after verifying the backup path." >&2; exit 2; fi +backup_dir="$1"; [[ -f "$backup_dir/hub.db" && -f "$backup_dir/uploads.tar.gz" && -f "$backup_dir/SHA256SUMS" ]] || { echo "Backup is incomplete" >&2; exit 2; } +(cd "$backup_dir" && sha256sum -c SHA256SUMS) +root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; cd "$root_dir" +docker compose down +mkdir -p data public +mv data/hub.db "data/hub.db.before-restore.$(date +%Y%m%d-%H%M%S)" 2>/dev/null || true +rm -rf public/uploads +cp "$backup_dir/hub.db" data/hub.db +tar -xzf "$backup_dir/uploads.tar.gz" -C public +docker compose up -d +printf 'Restore started from: %s\n' "$backup_dir"