92 lines
3.4 KiB
Bash
Executable File
92 lines
3.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# =============================================================================
|
||
# clover 备份脚本(跑在 VPS 宿主机上,建议 cron 每日执行)
|
||
#
|
||
# ./scripts/backup.sh # 备份到 ./backups/
|
||
# BACKUP_DIR=/mnt/backup ./scripts/backup.sh
|
||
#
|
||
# 依赖:curl;使用 .env 里的 RECONCILE_TOKEN 认证(面板设置页也可生成 API token)
|
||
# cron 示例(每日 3:30,保留 14 天):
|
||
# 30 3 * * * cd /root/clover && ./scripts/backup.sh >> /var/log/clover-backup.log 2>&1
|
||
# =============================================================================
|
||
set -euo pipefail
|
||
|
||
cd "$(dirname "$0")/.."
|
||
[ -f .env ] || { echo "找不到 .env(请在项目根目录运行)" >&2; exit 1; }
|
||
|
||
# shellcheck disable=SC1091
|
||
set -a; . ./.env; set +a
|
||
|
||
: "${PANEL_HOSTNAME:?PANEL_HOSTNAME 未配置}"
|
||
: "${RECONCILE_TOKEN:?RECONCILE_TOKEN 未配置(见 .env)}"
|
||
|
||
BASE_URL="${BACKUP_BASE_URL:-https://${PANEL_HOSTNAME}}"
|
||
BACKUP_DIR="${BACKUP_DIR:-./backups}"
|
||
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-14}"
|
||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||
|
||
mkdir -p "${BACKUP_DIR}"
|
||
|
||
echo "[backup] $(date -Is) 开始 → ${BACKUP_DIR}"
|
||
|
||
fetch() {
|
||
local what="$1" out="$2"
|
||
local downloaded=0
|
||
# 默认通过容器内回环访问面板,不依赖公网 DNS/TLS;自定义 URL 时保留 HTTPs 路径。
|
||
if [ -z "${BACKUP_BASE_URL:-}" ] && command -v docker >/dev/null 2>&1 \
|
||
&& [ "$(docker inspect -f '{{.State.Running}}' clover-panel 2>/dev/null || true)" = true ]; then
|
||
if docker exec -e "CLOVER_BACKUP_WHAT=${what}" clover-panel node -e '
|
||
(async () => {
|
||
const response = await fetch(`http://127.0.0.1:3000/api/v1/system/backup?what=${encodeURIComponent(process.env.CLOVER_BACKUP_WHAT)}`, {
|
||
headers: { Authorization: `Bearer ${process.env.RECONCILE_TOKEN || ""}` },
|
||
signal: AbortSignal.timeout(300000),
|
||
});
|
||
if (!response.ok) {
|
||
console.error(`[backup] HTTP ${response.status}: ${await response.text()}`);
|
||
process.exitCode = 1;
|
||
return;
|
||
}
|
||
process.stdout.write(Buffer.from(await response.arrayBuffer()));
|
||
})().catch((error) => { console.error(`[backup] ${error.message}`); process.exitCode = 1; });
|
||
' > "${out}"; then
|
||
downloaded=1
|
||
fi
|
||
fi
|
||
if [ "${downloaded}" -eq 0 ] && curl -fsS --max-time 300 \
|
||
-H "Authorization: Bearer ${RECONCILE_TOKEN}" \
|
||
"${BASE_URL}/api/v1/system/backup?what=${what}" \
|
||
-o "${out}"; then
|
||
downloaded=1
|
||
fi
|
||
|
||
if [ "${downloaded}" -eq 1 ]; then
|
||
local size
|
||
size="$(wc -c < "${out}")"
|
||
echo "[backup] OK ${what} → ${out} (${size} bytes)"
|
||
# 极小文件通常是错误 JSON,判一下
|
||
if [ "${size}" -lt 200 ]; then
|
||
echo "[backup] 警告:${out} 体积异常,内容如下:" >&2
|
||
head -c 300 "${out}" >&2; echo >&2
|
||
return 1
|
||
fi
|
||
else
|
||
echo "[backup] 失败:${what}" >&2
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
fetch db "${BACKUP_DIR}/clover-db-${STAMP}.db"
|
||
fetch dms-config "${BACKUP_DIR}/dms-config-${STAMP}.tar.gz"
|
||
|
||
# 校验 SQLite 快照可读(有 sqlite3 才校验)
|
||
if command -v sqlite3 >/dev/null 2>&1; then
|
||
sqlite3 "${BACKUP_DIR}/clover-db-${STAMP}.db" 'PRAGMA integrity_check;' | head -1
|
||
fi
|
||
|
||
# 轮转:删除超过保留天数的备份
|
||
find "${BACKUP_DIR}" -maxdepth 1 -type f \
|
||
\( -name 'clover-db-*.db' -o -name 'dms-config-*.tar.gz' \) \
|
||
-mtime "+${RETENTION_DAYS}" -print -delete 2>/dev/null || true
|
||
|
||
echo "[backup] $(date -Is) 完成(保留 ${RETENTION_DAYS} 天)"
|