#!/usr/bin/env bash # backup-db.sh — pangolin1 控制面 SQLite 每日备份到 NAS(异地容灾,#26 / F2)。 # # 经 ssh 在 pangolin1 上用 sqlite3 online `.backup` 取 WAL 一致快照(不阻塞服务、 # 不像裸 cp 那样可能拿到半写状态),远端先跑 PRAGMA integrity_check 确认没坏,再把 # 快照 gzip 流回 runner,落到 NAS 备份盘(默认 /volume1/docker/backups/pangolin, # 该盘已挂进 runner 容器)。保留 30 天。异地 = 家里 NAS ≠ VPS 机房,构成容灾。 # # 不在 pangolin1 上安装/常驻任何东西:调度在 Gitea Actions(backup.yml),每次经 # ssh 临时跑一次 sqlite3。需环境:DEPLOY_SSH_KEY(secret)。从 repo 根调用。 set -euo pipefail # shellcheck source=scripts/ci/lib-ssh.sh . scripts/ci/lib-ssh.sh BACKUP_DIR="${BACKUP_DIR:-/volume1/docker/backups/pangolin}" mkdir -p "$BACKUP_DIR" # 时间戳文件名(bash 内建 strftime,免 $() 命令替换)。 printf -v TS '%(%Y%m%d_%H%M%S)T' -1 DEST="${BACKUP_DIR}/pangolin_${TS}.db.gz" # setup_ssh 内部先注册 EXIT trap 清理临时私钥,再写 key/known_hosts,导出 $SSH。 setup_ssh echo "==> backup-db: sqlite3 .backup pangolin1(${DEPLOY_HOST})-> ${DEST}" # 远端单引号 heredoc:$BK/$$ 在 pangolin1 上展开。以 pangolin 用户(DB 属主)跑 # online .backup + integrity_check;校验通过才 cat 回流;临时文件用完即删。 # integrity_check 失败 → 远端非零退出 → 本地 pipefail 令整条命令失败 → set -e 中止。 $SSH "root@${DEPLOY_HOST}" 'bash -s' <<'ENDSSH' | gzip > "$DEST" set -euo pipefail BK="/tmp/pangolin-bk-$$.db" trap 'rm -f "$BK"' EXIT runuser -u pangolin -- sqlite3 "/var/lib/pangolin/pangolin.db" ".backup ${BK}" if ! runuser -u pangolin -- sqlite3 "$BK" "PRAGMA integrity_check;" | grep -q '^ok$'; then echo "backup-db(remote): integrity_check 失败" >&2 exit 1 fi cat "$BK" ENDSSH # 本地二次校验:gzip 完好 + 非空 sanity。空 gzip 仅 ~20 字节;真备份(即便很小的库) # gzip 后也远大于 1KB,故用 >1k 区分「空/断流」与「有效备份」(不用 50k:小库压缩后可能 <50k)。 gzip -t "$DEST" if ! find "$DEST" -size +1k -print -quit | grep -q .; then echo "==> backup-db: 备份异常小(<1k,疑似空/断流),中止" >&2 rm -f "$DEST" exit 1 fi # 保留最近 30 天。 find "$BACKUP_DIR" -name 'pangolin_*.db.gz' -mtime +30 -delete echo "==> backup-db: 备份完成 ${DEST}"