|
|
#!/bin/bash |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
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_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 |
|
|
|
|
|
|
|
|
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" |
|
|
|
|
|
|
|
|
if ! command -v python3 > /dev/null 2>&1; then |
|
|
log_info "正在安装Python..." |
|
|
apk add --no-cache python3 py3-pip |
|
|
fi |
|
|
|
|
|
|
|
|
log_info "正在安装/更新 WebDAV 依赖..." |
|
|
pip3 install --no-cache-dir requests webdavclient3 |
|
|
if [ $? -ne 0 ]; then |
|
|
log_error "依赖安装失败,尝试重试..." |
|
|
pip3 install --no-cache-dir requests webdavclient3 |
|
|
fi |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)" |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 "正在检查远程备份..." |
|
|
|
|
|
|
|
|
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) |
|
|
" |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
download_latest_backup |
|
|
|
|
|
|
|
|
sync_data() { |
|
|
log_info "数据同步循环服务已启动" |
|
|
|
|
|
while true; do |
|
|
|
|
|
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 |