#!/bin/bash # === 日志配置 === # 将日志同时输出到标准输出和主进程日志,确保在Space/Docker日志中可见 log_info() { MSG="[WebDAV-Backup] [$(date '+%H:%M:%S')] INFO: $1" if [ -w /proc/1/fd/1 ]; then echo "$MSG" > /proc/1/fd/1; else echo "$MSG"; fi } log_error() { MSG="[WebDAV-Backup] [$(date '+%H:%M:%S')] ERROR: $1" if [ -w /proc/1/fd/1 ]; then echo "$MSG" > /proc/1/fd/1; else echo "$MSG" >&2; fi } # === 1. 检查环境变量 === if [[ -z "$WEBDAV_URL" ]] || [[ -z "$WEBDAV_USERNAME" ]] || [[ -z "$WEBDAV_PASSWORD" ]]; then log_error "未启用备份功能 - 缺少 WEBDAV_URL, WEBDAV_USERNAME 或 WEBDAV_PASSWORD" exit 0 fi # 处理 WebDAV 路径 (如果 URL 结尾没有 / 则补上) WEBDAV_URL=${WEBDAV_URL%/} # 如果设置了子路径 WEBDAV_BACKUP_PATH=${WEBDAV_BACKUP_PATH:-""} # 移除子路径前后的 / WEBDAV_BACKUP_PATH=${WEBDAV_BACKUP_PATH#/} WEBDAV_BACKUP_PATH=${WEBDAV_BACKUP_PATH%/} if [ -n "$WEBDAV_BACKUP_PATH" ]; then FULL_WEBDAV_URL="${WEBDAV_URL}/${WEBDAV_BACKUP_PATH}" else FULL_WEBDAV_URL="${WEBDAV_URL}" fi # === 2. 目录设置 === TEMP_DIR="/tmp/app_backup" DATA_DIR="/home/node/app/data" mkdir -p $TEMP_DIR chmod -R 777 $TEMP_DIR mkdir -p $DATA_DIR chmod -R 777 $DATA_DIR log_info "临时目录: $TEMP_DIR" log_info "数据目录: $DATA_DIR" log_info "WebDAV URL: $FULL_WEBDAV_URL" # === 3. 安装依赖 === if ! command -v python3 > /dev/null 2>&1; then log_info "正在安装Python..." apk add --no-cache python3 py3-pip fi # 确保安装了 webdavclient3 和 requests log_info "正在安装/更新 WebDAV 依赖..." pip3 install --no-cache-dir requests webdavclient3 if [ $? -ne 0 ]; then log_error "依赖安装失败,尝试重试..." pip3 install --no-cache-dir requests webdavclient3 fi # === 4. 测试连接 === log_info "正在测试 WebDAV 连接..." HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -u "$WEBDAV_USERNAME:$WEBDAV_PASSWORD" -X PROPFIND "$FULL_WEBDAV_URL/") if [[ "$HTTP_CODE" == "207" ]] || [[ "$HTTP_CODE" == "200" ]]; then log_info "WebDAV 连接成功 (HTTP $HTTP_CODE)" else log_error "WebDAV 连接失败 (HTTP $HTTP_CODE),正在尝试创建目录..." # 尝试创建目录 MKCOL_CODE=$(curl -s -o /dev/null -w "%{http_code}" -u "$WEBDAV_USERNAME:$WEBDAV_PASSWORD" -X MKCOL "$FULL_WEBDAV_URL/") if [[ "$MKCOL_CODE" == "201" ]] || [[ "$MKCOL_CODE" == "200" ]]; then log_info "远程目录创建成功" else log_error "无法连接也无法创建目录,请检查配置。HTTP: $MKCOL_CODE" # 不退出,继续尝试,也许是根目录权限问题 fi fi # === 5. 定义功能函数 === # 上传并清理旧备份 upload_backup() { file_path="$1" file_name="$2" if [ ! -f "$file_path" ]; then log_error "备份文件不存在: $file_path" return 1 fi file_size=$(du -h "$file_path" | cut -f1) log_info "开始上传: $file_name ($file_size)" # 1. 使用 CURL 上传 (比 Python 更稳定且显示进度) # 使用 -T 上传文件 HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -u "$WEBDAV_USERNAME:$WEBDAV_PASSWORD" -T "$file_path" "$FULL_WEBDAV_URL/$file_name") if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "204" ]] || [[ "$HTTP_CODE" == "200" ]]; then log_info "上传成功 (HTTP $HTTP_CODE)" else log_error "上传失败 (HTTP $HTTP_CODE)" return 1 fi # 2. 使用 Python 清理旧文件 (保留最近5份) log_info "正在检查旧备份..." python3 -c " import sys from webdav3.client import Client options = { 'webdav_hostname': '$FULL_WEBDAV_URL', 'webdav_login': '$WEBDAV_USERNAME', 'webdav_password': '$WEBDAV_PASSWORD' } try: client = Client(options) # 获取文件列表 files = client.list() # 筛选备份文件 backups = [f for f in files if f.endswith('.tar.gz') and f.startswith('app_backup_')] backups.sort() # 保留最近 5 个 MAX_BACKUPS = 5 if len(backups) > MAX_BACKUPS: to_delete = backups[:len(backups) - MAX_BACKUPS] for file_name in to_delete: try: # webdav3 的 list 返回的可能是相对路径,clean 需要正确路径 client.clean(file_name) print(f'已删除旧备份: {file_name}') except Exception as e: print(f'删除 {file_name} 失败: {str(e)}') else: print(f'当前备份数量 ({len(backups)}) 未超过限制,无需清理') except Exception as e: print(f'清理旧备份时出错: {str(e)}') " return 0 } # 下载最新备份 download_latest_backup() { log_info "正在检查远程备份..." # 使用 Python 处理复杂的 WebDAV 列表和下载逻辑 python3 -c " import sys import os import tarfile import requests import shutil from webdav3.client import Client # 配置 options = { 'webdav_hostname': '$FULL_WEBDAV_URL', 'webdav_login': '$WEBDAV_USERNAME', 'webdav_password': '$WEBDAV_PASSWORD' } try: client = Client(options) # 1. 列出文件 files = client.list() # 筛选备份 backups = [f for f in files if f.endswith('.tar.gz') and f.startswith('app_backup_')] if not backups: print('未找到远程备份文件,跳过恢复') sys.exit(0) # 2. 找到最新的 latest_backup = sorted(backups)[-1] print(f'发现最新备份: {latest_backup}') # 3. 下载 local_path = os.path.join('$TEMP_DIR', latest_backup) download_url = f'$FULL_WEBDAV_URL/{latest_backup}' print(f'开始下载...') # 使用 requests 流式下载 with requests.get(download_url, auth=('$WEBDAV_USERNAME', '$WEBDAV_PASSWORD'), stream=True) as r: r.raise_for_status() with open(local_path, 'wb') as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) print(f'下载完成: {local_path}') # 4. 恢复 target_dir = '$DATA_DIR' if os.path.exists(local_path): # 确保目录存在 os.makedirs(target_dir, exist_ok=True) # 检查是否需要清空现有数据 (通常首次启动是空的,但以防万一) # 这里为了安全,我们覆盖解压,而不是先删除 rm -rf print(f'正在解压到 {target_dir}...') with tarfile.open(local_path, 'r:gz') as tar: tar.extractall(target_dir) print('备份恢复成功!') # 清理下载的临时文件 os.remove(local_path) else: print('下载的文件丢失,恢复失败') sys.exit(1) except Exception as e: print(f'恢复备份过程中出错: {str(e)}') # 不退出脚本,允许继续运行,只是恢复失败 sys.exit(1) " } # === 6. 主流程 === # 首次启动时尝试恢复 download_latest_backup # === 7. 循环同步 === sync_data() { log_info "数据同步循环服务已启动" while true; do # 默认 6 小时同步一次 (21600秒),避免 WebDAV 拥堵 SYNC_INTERVAL=${SYNC_INTERVAL:-21600} # 检查目录 if [ -d "$DATA_DIR" ]; then # 检查文件数量 file_count=$(find "$DATA_DIR" -type f | wc -l) if [ "$file_count" -eq 0 ]; then log_info "数据目录为空 ($file_count 文件),跳过备份" else log_info "开始创建备份 (文件数: $file_count)..." timestamp=$(date +%Y%m%d_%H%M%S) backup_file="app_backup_${timestamp}.tar.gz" backup_path="${TEMP_DIR}/${backup_file}" # 压缩 tar -czf "$backup_path" -C "$DATA_DIR" . if [ $? -eq 0 ]; then # 上传 upload_backup "$backup_path" "$backup_file" # 本地清理 rm -f "$backup_path" else log_error "压缩文件创建失败" fi fi else log_error "数据目录不存在: $DATA_DIR" mkdir -p "$DATA_DIR" fi log_info "下次同步将在 ${SYNC_INTERVAL} 秒后进行..." sleep $SYNC_INTERVAL done } # 启动同步循环 sync_data