guma / sync_data.sh
gallygc's picture
Update sync_data.sh
7a08427 verified
Raw History Blame Contribute Delete
8.03 kB
#!/bin/sh
set -eu
APP_DIR="/home/node/bbb"
DATA_DIR="${APP_DIR}/data"
cd "$APP_DIR"
echo "===== Uptime Kuma startup ====="
echo "App dir: $APP_DIR"
echo "Data dir: $DATA_DIR"
# 双保险:如果外部没传,就默认 bypass
export UPTIME_KUMA_WS_ORIGIN_CHECK="${UPTIME_KUMA_WS_ORIGIN_CHECK:-bypass}"
echo "UPTIME_KUMA_WS_ORIGIN_CHECK=$UPTIME_KUMA_WS_ORIGIN_CHECK"
# 如果没有配置 WebDAV,就直接启动 Uptime Kuma
if [ -z "${WEBDAV_URL:-}" ] || [ -z "${WEBDAV_USERNAME:-}" ] || [ -z "${WEBDAV_PASSWORD:-}" ]; then
echo "Starting without backup functionality - missing WEBDAV_URL, WEBDAV_USERNAME, or WEBDAV_PASSWORD"
exec node server/server.js
fi
# 组装 WebDAV 地址
WEBDAV_BACKUP_PATH="${WEBDAV_BACKUP_PATH:-}"
FULL_WEBDAV_URL="${WEBDAV_URL}"
if [ -n "$WEBDAV_BACKUP_PATH" ]; then
FULL_WEBDAV_URL="${WEBDAV_URL%/}/${WEBDAV_BACKUP_PATH#/}"
fi
export FULL_WEBDAV_URL
export APP_DIR
export DATA_DIR
echo "WebDAV URL: $FULL_WEBDAV_URL"
restore_backup() {
echo "Downloading latest backup from WebDAV..."
python3 - <<'PY'
import os
import sys
import tarfile
import shutil
import requests
from pathlib import Path
from webdav3.client import Client
FULL_WEBDAV_URL = os.environ["FULL_WEBDAV_URL"]
WEBDAV_USERNAME = os.environ["WEBDAV_USERNAME"]
WEBDAV_PASSWORD = os.environ["WEBDAV_PASSWORD"]
DATA_DIR = os.environ["DATA_DIR"]
TMP_DIR = Path("/tmp/uptime_kuma_restore")
TMP_DIR.mkdir(parents=True, exist_ok=True)
def safe_extract(tar: tarfile.TarFile, path: str):
base = Path(path).resolve()
for member in tar.getmembers():
member_path = (base / member.name).resolve()
if not str(member_path).startswith(str(base)):
raise Exception(f"Unsafe path in tar archive: {member.name}")
tar.extractall(path)
options = {
"webdav_hostname": FULL_WEBDAV_URL,
"webdav_login": WEBDAV_USERNAME,
"webdav_password": WEBDAV_PASSWORD,
}
client = Client(options)
try:
entries = client.list()
except Exception as e:
print(f"[restore] Failed to list WebDAV directory: {e}")
sys.exit(1)
backups =[f for f in entries if f.endswith(".tar.gz") and f.startswith("uptime_kuma_backup_")]
if not backups:
print("[restore] No backup file found on WebDAV. Skip restore.")
sys.exit(0)
latest_backup = sorted(backups)[-1]
print(f"[restore] Latest backup: {latest_backup}")
backup_path = TMP_DIR / latest_backup
extract_path = TMP_DIR / "extract"
if extract_path.exists():
shutil.rmtree(extract_path)
extract_path.mkdir(parents=True, exist_ok=True)
download_url = f"{FULL_WEBDAV_URL.rstrip('/')}/{latest_backup}"
try:
with requests.get(download_url, auth=(WEBDAV_USERNAME, WEBDAV_PASSWORD), stream=True, timeout=120) as r:
if r.status_code != 200:
print(f"[restore] Download failed: HTTP {r.status_code}")
sys.exit(1)
with open(backup_path, "wb") as f:
for chunk in r.iter_content(chunk_size=1024 * 1024):
if chunk:
f.write(chunk)
except Exception as e:
print(f"[restore] Exception while downloading backup: {e}")
sys.exit(1)
print(f"[restore] Downloaded to: {backup_path}")
try:
with tarfile.open(backup_path, "r:gz") as tar:
safe_extract(tar, str(extract_path))
except Exception as e:
print(f"[restore] Failed to extract backup: {e}")
sys.exit(1)
restored_data_dir = extract_path / "data"
if not restored_data_dir.exists() or not restored_data_dir.is_dir():
print("[restore] Invalid backup: 'data/' directory not found in archive")
sys.exit(1)
# 至少要求 db-config.json 存在,否则会再次进入 setup 模式
db_config = restored_data_dir / "db-config.json"
if not db_config.exists():
print("[restore] Invalid backup: missing data/db-config.json")
print("[restore] Skip replacing current data directory to avoid broken restore.")
sys.exit(1)
target_data_dir = Path(DATA_DIR)
backup_old_dir = Path(f"{DATA_DIR}.bak")
if backup_old_dir.exists():
shutil.rmtree(backup_old_dir)
if target_data_dir.exists():
print(f"[restore] Backing up current data directory to: {backup_old_dir}")
shutil.move(str(target_data_dir), str(backup_old_dir))
try:
shutil.copytree(restored_data_dir, target_data_dir)
print(f"[restore] Restore completed successfully to: {target_data_dir}")
except Exception as e:
print(f"[restore] Failed to place restored data: {e}")
if target_data_dir.exists():
shutil.rmtree(target_data_dir)
if backup_old_dir.exists():
shutil.move(str(backup_old_dir), str(target_data_dir))
sys.exit(1)
if backup_old_dir.exists():
shutil.rmtree(backup_old_dir, ignore_errors=True)
print("[restore] Backup restore finished.")
PY
}
sync_data_loop() {
while true; do
echo "Starting sync process at $(date)"
if [ -d "$DATA_DIR" ]; then
timestamp=$(date +%Y%m%d_%H%M%S)
backup_file="uptime_kuma_backup_${timestamp}.tar.gz"
backup_path="/tmp/${backup_file}"
STAGING_DIR="/tmp/kuma_backup_stage_${timestamp}"
rm -rf "${STAGING_DIR}"
mkdir -p "${STAGING_DIR}"
# 1. 复制现有的 data 目录所有配置、缓存文件
cp -a "$DATA_DIR" "${STAGING_DIR}/" || true
# 2. 从暂存区删除正在被占用的数据库文件
rm -f "${STAGING_DIR}/data/kuma.db" "${STAGING_DIR}/data/kuma.db-wal" "${STAGING_DIR}/data/kuma.db-shm"
# 3. 使用 sqlite3 将安全的数据库快照写出到暂存区
if [ -f "$DATA_DIR/kuma.db" ]; then
sqlite3 "$DATA_DIR/kuma.db" ".backup '${STAGING_DIR}/data/kuma.db'"
fi
# 4. 在绝对静止的暂存目录中进行打包
cd "${STAGING_DIR}"
tar -czf "$backup_path" data
cd "$APP_DIR"
# 5. 清理暂存区
rm -rf "${STAGING_DIR}"
# 开始上传打包好的文件
if curl -fsS -u "$WEBDAV_USERNAME:$WEBDAV_PASSWORD" \
-T "$backup_path" \
"${FULL_WEBDAV_URL%/}/${backup_file}"; then
echo "Successfully uploaded ${backup_file} to WebDAV"
else
echo "Failed to upload ${backup_file} to WebDAV"
rm -f "$backup_path"
SYNC_INTERVAL="${SYNC_INTERVAL:-600}"
echo "Next sync in ${SYNC_INTERVAL} seconds..."
sleep "$SYNC_INTERVAL"
continue
fi
# 上传成功后执行清理旧备份的 Python 脚本
python3 - <<'PY'
import os
from webdav3.client import Client
FULL_WEBDAV_URL = os.environ["FULL_WEBDAV_URL"]
WEBDAV_USERNAME = os.environ["WEBDAV_USERNAME"]
WEBDAV_PASSWORD = os.environ["WEBDAV_PASSWORD"]
options = {
"webdav_hostname": FULL_WEBDAV_URL,
"webdav_login": WEBDAV_USERNAME,
"webdav_password": WEBDAV_PASSWORD,
}
client = Client(options)
try:
entries = client.list()
backups =[f for f in entries if f.endswith(".tar.gz") and f.startswith("uptime_kuma_backup_")]
backups.sort()
if len(backups) > 10:
old_files = backups[:len(backups) - 10]
for file in old_files:
client.clean(file)
print(f"Successfully deleted old backup: {file}")
else:
print(f"Only {len(backups)} backups found, no cleanup needed.")
except Exception as e:
print(f"Cleanup failed: {e}")
PY
rm -f "$backup_path"
else
echo "$DATA_DIR does not exist, waiting for next sync..."
fi
SYNC_INTERVAL="${SYNC_INTERVAL:-600}"
echo "Next sync in ${SYNC_INTERVAL} seconds..."
sleep "$SYNC_INTERVAL"
done
}
# 启动前尝试恢复一次
if restore_backup; then
echo "Restore step completed."
else
echo "Restore failed or skipped. Continue starting app."
fi
# 后台同步(只负责备份)
sync_data_loop &
# 前台启动 Uptime Kuma(只启动一次)
exec node server/server.js