#!/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