|
|
#!/bin/bash |
|
|
|
|
|
|
|
|
LOG_PREFIX="[Koishi-Cloud-Sync]" |
|
|
|
|
|
if [ -z "$WEBDAV_URL" ] || [ -z "$WEBDAV_USERNAME" ] || [ -z "$WEBDAV_PASSWORD" ]; then |
|
|
echo "$LOG_PREFIX [WARN] WebDAV 未配置,直接启动 Koishi。" |
|
|
exec yarn start |
|
|
fi |
|
|
|
|
|
|
|
|
CLEAN_URL=$(echo "${WEBDAV_URL}" | sed 's:/*$::') |
|
|
WEBDAV_BACKUP_PATH=${WEBDAV_BACKUP_PATH:-""} |
|
|
FULL_WEBDAV_URL="${CLEAN_URL}${WEBDAV_BACKUP_PATH:+/$WEBDAV_BACKUP_PATH}" |
|
|
|
|
|
|
|
|
FILE_PREFIX="koishi_backup_" |
|
|
|
|
|
|
|
|
restore_backup() { |
|
|
echo "$LOG_PREFIX [Restore] 正在从 WebDAV 恢复数据..." |
|
|
python3 <<EOF |
|
|
import sys, os, tarfile, requests |
|
|
from webdav3.client import Client |
|
|
|
|
|
options = { |
|
|
"webdav_hostname": "${FULL_WEBDAV_URL}", |
|
|
"webdav_login": "${WEBDAV_USERNAME}", |
|
|
"webdav_password": "${WEBDAV_PASSWORD}", |
|
|
} |
|
|
client = Client(options) |
|
|
|
|
|
try: |
|
|
# 这里的 list() 会受 hostname 里的子路径影响,处理文件名过滤 |
|
|
files = client.list() |
|
|
backups = sorted([f for f in files if f.startswith("${FILE_PREFIX}") and f.endswith(".tar.gz")]) |
|
|
|
|
|
if not backups: |
|
|
print("$LOG_PREFIX [Restore] WebDAV 上没有发现备份文件,跳过恢复。") |
|
|
sys.exit(0) |
|
|
|
|
|
latest = backups[-1] |
|
|
print(f"$LOG_PREFIX [Restore] 发现最新备份: {latest}") |
|
|
|
|
|
# 构造下载链接 |
|
|
download_url = f"${FULL_WEBDAV_URL}/{latest}" |
|
|
r = requests.get(download_url, auth=("${WEBDAV_USERNAME}", "${WEBDAV_PASSWORD}"), stream=True) |
|
|
|
|
|
if r.status_code == 200: |
|
|
tmp_path = f"/tmp/{latest}" |
|
|
with open(tmp_path, "wb") as f: |
|
|
for chunk in r.iter_content(8192): |
|
|
f.write(chunk) |
|
|
|
|
|
with tarfile.open(tmp_path, "r:gz") as tar: |
|
|
tar.extractall("/app") |
|
|
print("$LOG_PREFIX [Restore] 数据恢复成功!") |
|
|
else: |
|
|
print(f"$LOG_PREFIX [Restore] 下载失败,状态码: {r.status_code}") |
|
|
except Exception as e: |
|
|
print(f"$LOG_PREFIX [Restore] 发生错误: {e}") |
|
|
EOF |
|
|
} |
|
|
|
|
|
|
|
|
sync_loop() { |
|
|
|
|
|
sleep 60 |
|
|
|
|
|
while true; do |
|
|
ts=$(date +%Y%m%d_%H%M%S) |
|
|
file_name="${FILE_PREFIX}${ts}.tar.gz" |
|
|
local_tmp="/tmp/${file_name}" |
|
|
|
|
|
echo "$LOG_PREFIX [Sync] 正在打包数据 (包含 node_modules)..." |
|
|
|
|
|
tar -czf "$local_tmp" -C /app . |
|
|
|
|
|
echo "$LOG_PREFIX [Sync] 打包完成 ($(du -h $local_tmp | cut -f1)),准备上传..." |
|
|
|
|
|
python3 <<EOF |
|
|
from webdav3.client import Client |
|
|
import os |
|
|
|
|
|
options = { |
|
|
"webdav_hostname": "${FULL_WEBDAV_URL}", |
|
|
"webdav_login": "${WEBDAV_USERNAME}", |
|
|
"webdav_password": "${WEBDAV_PASSWORD}", |
|
|
} |
|
|
client = Client(options) |
|
|
|
|
|
try: |
|
|
# webdav3 的 upload_file 第一个参数是远程文件名,第二个是本地路径 |
|
|
# 注意:如果 options 已经包含完整路径,此处只需传文件名 |
|
|
client.upload_file("$file_name", "$local_tmp") |
|
|
print(f"$LOG_PREFIX [Sync] 上传成功: $file_name") |
|
|
|
|
|
# 清理旧备份 |
|
|
files = sorted([f for f in client.list() if f.startswith("${FILE_PREFIX}") and f.endswith(".tar.gz")]) |
|
|
if len(files) > 5: |
|
|
for old_file in files[:-5]: |
|
|
client.clean(old_file) |
|
|
print(f"$LOG_PREFIX [Clean] 已删除旧备份: {old_file}") |
|
|
except Exception as e: |
|
|
print(f"$LOG_PREFIX [Error] 上传过程中出错: {e}") |
|
|
EOF |
|
|
|
|
|
rm -f "$local_tmp" |
|
|
|
|
|
|
|
|
echo "$LOG_PREFIX [Sync] 本次任务结束,等待下一次循环..." |
|
|
sleep ${SYNC_INTERVAL:-7200} |
|
|
done |
|
|
} |
|
|
|
|
|
|
|
|
restore_backup |
|
|
|
|
|
|
|
|
sync_loop & |
|
|
|
|
|
|
|
|
echo "$LOG_PREFIX 正在启动 Koishi 服务..." |
|
|
exec yarn start |