Spaces:
Paused
Paused
Update sync_data.sh
Browse files- sync_data.sh +213 -296
sync_data.sh
CHANGED
|
@@ -1,356 +1,273 @@
|
|
| 1 |
#!/bin/bash
|
| 2 |
|
| 3 |
-
# ---
|
|
|
|
| 4 |
if [[ -z "$WEBDAV_URL" ]] || [[ -z "$WEBDAV_USERNAME" ]] || [[ -z "$WEBDAV_PASSWORD" ]]; then
|
| 5 |
-
echo "错误:缺少
|
| 6 |
-
echo "
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
FULL_WEBDAV_URL="${WEBDAV_URL%/}" # 确保基础 URL 末尾无斜杠
|
| 12 |
-
|
| 13 |
-
# --- 检查并创建 WebDAV 备份目录 (修复版 Python 代码) ---
|
| 14 |
-
echo "检查并创建 WebDAV 备份目录 (路径: $WEBDAV_BACKUP_PATH)..."
|
| 15 |
-
python3 -c "
|
| 16 |
-
import os
|
| 17 |
-
import sys
|
| 18 |
-
from urllib.parse import urljoin
|
| 19 |
-
from webdav3.client import Client
|
| 20 |
-
from webdav3.exceptions import RemoteResourceNotFound, MethodNotSupported, NoConnection
|
| 21 |
-
|
| 22 |
-
options = {
|
| 23 |
-
'webdav_hostname': '$WEBDAV_URL',
|
| 24 |
-
'webdav_login': '$WEBDAV_USERNAME',
|
| 25 |
-
'webdav_password': '$WEBDAV_PASSWORD'
|
| 26 |
-
}
|
| 27 |
-
backup_base_path = '$WEBDAV_BACKUP_PATH' # WebDAV 上的相对路径
|
| 28 |
-
|
| 29 |
-
try:
|
| 30 |
-
client = Client(options)
|
| 31 |
-
# 确保基础 URL 可连接 (可选,但有助于早期发现连接问题)
|
| 32 |
-
# client.verify = True # 如果需要验证 SSL 证书
|
| 33 |
-
# if not client.check(): # check() 可能也依赖特定方法,先注释掉
|
| 34 |
-
# print(f'错误:无法连接到 WebDAV 服务器: $WEBDAV_URL')
|
| 35 |
-
# sys.exit(1)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
# 如果所有层级都成功,构造最终的完整 URL 用于后续操作
|
| 39 |
-
# 使用 Python 的 urljoin 来确保路径拼接正确
|
| 40 |
-
final_full_url = urljoin('$WEBDAV_URL' + ('/' if not '$WEBDAV_URL'.endswith('/') else ''), backup_base_path + ('/' if backup_base_path else ''))
|
| 41 |
-
print(f'WebDAV 备份目录准备就绪: {final_full_url}')
|
| 42 |
-
# 将最终的 URL 输出,让 Shell 可以捕获 (如果需要)
|
| 43 |
-
# print(f'FULL_WEBDAV_BACKUP_URL={final_full_url}') # 暂时不需要外部捕获
|
| 44 |
-
|
| 45 |
-
except NoConnection as nc_e:
|
| 46 |
-
print(f'错误:初始化 WebDAV 连接失败: {nc_e}')
|
| 47 |
-
sys.exit(1)
|
| 48 |
-
except Exception as e:
|
| 49 |
-
print(f'错误:处理 WebDAV 目录时发生严重错误: {e}')
|
| 50 |
-
sys.exit(1) # 初始化或目录处理阶段的严重错误直接退出
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
| 63 |
fi
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
# ---
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
| 75 |
python3 -c "
|
| 76 |
-
# ... (你原来那一大坨用于下载和解压的 Python 代码放在这里,从 import sys 开始) ...
|
| 77 |
-
# 确保 logging 和 sys 被正确导入和使用
|
| 78 |
import sys
|
| 79 |
import os
|
| 80 |
import tarfile
|
| 81 |
import requests
|
| 82 |
-
from webdav3.client import Client
|
| 83 |
import shutil
|
| 84 |
-
import
|
| 85 |
-
|
| 86 |
options = {
|
| 87 |
-
'webdav_hostname': '$
|
| 88 |
'webdav_login': '$WEBDAV_USERNAME',
|
| 89 |
'webdav_password': '$WEBDAV_PASSWORD'
|
| 90 |
}
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
try:
|
| 95 |
client = Client(options)
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
if not backups:
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
| 114 |
for chunk in r.iter_content(chunk_size=8192):
|
| 115 |
f.write(chunk)
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
if os.path.exists(
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
except Exception as e:
|
| 141 |
-
logging.error(f'解压备份文件失败: {e}')
|
| 142 |
-
sys.exit(1)
|
| 143 |
-
extracted_files_path = os.path.join(temp_extract_dir, local_target_name)
|
| 144 |
-
local_target_path = os.path.join(target_restore_dir, local_target_name)
|
| 145 |
-
if os.path.isdir(extracted_files_path):
|
| 146 |
-
logging.info(f'在备份中找到 \'{local_target_name}\' 目录。准备恢复...')
|
| 147 |
-
if os.path.exists(local_target_path):
|
| 148 |
-
logging.warning(f'本地已存在 \'{local_target_path}\' 目录,将删除并替换为备份内容。')
|
| 149 |
-
try:
|
| 150 |
-
# 对于目录,优先使用 shutil.rmtree
|
| 151 |
-
if os.path.isdir(local_target_path):
|
| 152 |
-
shutil.rmtree(local_target_path)
|
| 153 |
-
# 如果是文件或链接,用 os.remove 或 os.unlink
|
| 154 |
-
elif os.path.exists(local_target_path):
|
| 155 |
-
os.remove(local_target_path)
|
| 156 |
-
logging.info(f'成功删除旧的 \'{local_target_path}\'。')
|
| 157 |
-
except Exception as e:
|
| 158 |
-
logging.error(f'删除旧的 \'{local_target_path}\' 失败: {e}')
|
| 159 |
-
sys.exit(1)
|
| 160 |
-
|
| 161 |
-
try:
|
| 162 |
-
shutil.move(extracted_files_path, local_target_path)
|
| 163 |
-
logging.info(f'成功将备份恢复到 \'{local_target_path}\'。')
|
| 164 |
-
except Exception as e:
|
| 165 |
-
logging.error(f'移动解压后的 \'{local_target_name}\' 目录到 \'{local_target_path}\' 失败: {e}')
|
| 166 |
-
sys.exit(1)
|
| 167 |
else:
|
| 168 |
-
|
| 169 |
-
logging.info('开始清理临时文件...')
|
| 170 |
-
try:
|
| 171 |
-
os.remove(local_tmp_backup_path)
|
| 172 |
-
logging.info(f'已删除临时备份文件: {local_tmp_backup_path}')
|
| 173 |
-
except OSError as e:
|
| 174 |
-
logging.warning(f'删除临时备份文件 {local_tmp_backup_path} 时出错: {e}')
|
| 175 |
-
|
| 176 |
-
try:
|
| 177 |
-
shutil.rmtree(temp_extract_dir)
|
| 178 |
-
logging.info(f'已删除临时解压目录: {temp_extract_dir}')
|
| 179 |
-
except OSError as e:
|
| 180 |
-
logging.warning(f'删除临时解压目录 {temp_extract_dir} 时出错: {e}')
|
| 181 |
-
except Exception as e:
|
| 182 |
-
logging.error(f'恢复过程中发生意外错误: {e}')
|
| 183 |
-
sys.exit(1) # 恢复过程中的 Python 错误应该让脚本失败
|
| 184 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
"
|
| 186 |
# 检查 Python 脚本的退出码
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
-
#
|
| 193 |
-
if [ -
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
echo "成功设置 /home/user/data 目录权限。"
|
| 198 |
-
else
|
| 199 |
-
echo "警告:设置 /home/user/data 目录权限失败!"
|
| 200 |
-
fi
|
| 201 |
-
else
|
| 202 |
-
echo "未找到恢复后的 /home/user/data 目录或恢复未执行,跳过权限设置。"
|
| 203 |
fi
|
| 204 |
-
# --- 权限设置结束 ---
|
| 205 |
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
#
|
| 211 |
-
fi
|
| 212 |
-
}
|
| 213 |
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
#
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
else
|
| 220 |
-
echo "[Entrypoint] WebDAV 未启用或初始化失败,跳过首次恢复。"
|
| 221 |
-
# 如果没有恢复,检查本地是否存在 ./data,如果不存在可能需要创建空目录?
|
| 222 |
-
if [ ! -d "/home/user/data" ]; then
|
| 223 |
-
echo "[Entrypoint] 本地 /home/user/data 目录不存在,创建一个空的..."
|
| 224 |
-
mkdir /home/user/data
|
| 225 |
-
chmod u=rwX,g=rX,o=rX /home/user/data # 设置基本权限
|
| 226 |
-
fi
|
| 227 |
-
fi
|
| 228 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
|
| 230 |
-
# --- 定时同步函数 (备份 `/home/user/data` 文件夹) ---
|
| 231 |
-
sync_data() {
|
| 232 |
-
if [ "$WEBDAV_ENABLED" = false ]; then
|
| 233 |
-
echo "[Sync] WebDAV 未配置或初始化失败,跳过后台同步任务。"
|
| 234 |
-
return
|
| 235 |
-
fi
|
| 236 |
|
| 237 |
-
|
| 238 |
-
#
|
| 239 |
-
|
| 240 |
-
echo "[Sync] 下一次 `/home/user/data` 备份将在 ${SYNC_INTERVAL} 秒后进行..."
|
| 241 |
-
sleep $SYNC_INTERVAL
|
| 242 |
|
| 243 |
-
|
|
|
|
| 244 |
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
backup_file="files_backup_${timestamp}.tar.gz"
|
| 249 |
-
local_tmp_backup_path="/tmp/${backup_file}"
|
| 250 |
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
if [ $? -ne 0 ]; then
|
| 255 |
-
echo "[Sync] 错误:创建 tar 备份文件 ${backup_file} 失败!跳过此次同步。"
|
| 256 |
-
rm -f "${local_tmp_backup_path}" # 清理可能产生的坏文件
|
| 257 |
-
continue
|
| 258 |
-
fi
|
| 259 |
-
echo "[Sync] 成功创建本地备份文件: ${local_tmp_backup_path}"
|
| 260 |
-
|
| 261 |
-
# 上传新备份到WebDAV (使用正确的完整 URL)
|
| 262 |
-
webdav_upload_url="${FULL_WEBDAV_URL%/}/${backup_file}"
|
| 263 |
-
echo "[Sync] 开始上传 ${backup_file} 到 ${webdav_upload_url}"
|
| 264 |
-
curl --fail --silent --show-error -u "$WEBDAV_USERNAME:$WEBDAV_PASSWORD" -T "${local_tmp_backup_path}" "${webdav_upload_url}"
|
| 265 |
-
curl_exit_code=$? # 保存 curl 退出码
|
| 266 |
-
|
| 267 |
-
if [ $curl_exit_code -eq 0 ]; then
|
| 268 |
-
echo "[Sync] 成功将 ${backup_file} 上传至 WebDAV"
|
| 269 |
-
rm -f "${local_tmp_backup_path}"
|
| 270 |
-
echo "[Sync] 已清理本地临时备份文件: ${local_tmp_backup_path}"
|
| 271 |
-
|
| 272 |
-
# 清理 WebDAV 上的旧备份文件(保留最新的5个)
|
| 273 |
-
echo "[Sync] 开始清理 WebDAV 上的旧备份..."
|
| 274 |
-
python3 -c "
|
| 275 |
-
# ... (清理旧备份的 Python 代码基本不变���确认变量名正确) ...
|
| 276 |
import sys
|
| 277 |
-
import os
|
| 278 |
from webdav3.client import Client
|
| 279 |
-
|
| 280 |
-
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 281 |
options = {
|
| 282 |
-
'webdav_hostname': '$
|
| 283 |
'webdav_login': '$WEBDAV_USERNAME',
|
| 284 |
'webdav_password': '$WEBDAV_PASSWORD'
|
| 285 |
}
|
| 286 |
-
|
| 287 |
-
|
|
|
|
| 288 |
try:
|
| 289 |
client = Client(options)
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
backups.sort(key=lambda x: os.path.basename(x))
|
| 299 |
-
|
| 300 |
-
if len(backups) > keep_latest:
|
| 301 |
-
to_delete_count = len(backups) - keep_latest
|
| 302 |
-
logging.info(f'找到 {len(backups)} 个备份,超过限制 {keep_latest} 个,将删除最旧的 {to_delete_count} 个。')
|
| 303 |
-
for backup_to_delete_rel_path in backups[:to_delete_count]:
|
| 304 |
try:
|
| 305 |
-
#
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
|
|
|
|
|
|
| 310 |
else:
|
| 311 |
-
|
|
|
|
| 312 |
except Exception as e:
|
| 313 |
-
|
| 314 |
"
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
else
|
| 318 |
-
echo "[Sync] WebDAV 旧备份清理完成。"
|
| 319 |
-
fi
|
| 320 |
-
|
| 321 |
-
else
|
| 322 |
-
echo "[Sync] 错误:上传 ${backup_file} 至 WebDAV 失败!(curl 退出码: ${curl_exit_code}) 请检查网络或 WebDAV 服务器状态。"
|
| 323 |
-
rm -f "${local_tmp_backup_path}"
|
| 324 |
-
echo "[Sync] 已清理上传失败的本地临时备份文件: ${local_tmp_backup_path}"
|
| 325 |
fi
|
|
|
|
| 326 |
else
|
| 327 |
-
echo "
|
|
|
|
|
|
|
|
|
|
| 328 |
fi
|
| 329 |
-
|
| 330 |
-
echo "
|
|
|
|
| 331 |
done
|
| 332 |
}
|
| 333 |
|
| 334 |
-
# ---
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
else
|
| 340 |
-
echo "[Entrypoint] WebDAV 未启用或初始化失败,后台备份同步任务已禁用。"
|
| 341 |
-
fi
|
| 342 |
|
| 343 |
-
#
|
| 344 |
-
|
| 345 |
-
echo "
|
| 346 |
-
|
|
|
|
|
|
|
| 347 |
|
| 348 |
-
#
|
| 349 |
-
|
| 350 |
-
|
| 351 |
|
| 352 |
-
#
|
| 353 |
-
#
|
| 354 |
-
#
|
| 355 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
|
|
|
|
|
|
|
|
| 1 |
#!/bin/bash
|
| 2 |
|
| 3 |
+
# --- 配置检查 ---
|
| 4 |
+
# 检查 WebDAV 环境变量是否设置
|
| 5 |
if [[ -z "$WEBDAV_URL" ]] || [[ -z "$WEBDAV_USERNAME" ]] || [[ -z "$WEBDAV_PASSWORD" ]]; then
|
| 6 |
+
echo "错误:缺少必要的 WebDAV 环境变量 (WEBDAV_URL, WEBDAV_USERNAME, WEBDAV_PASSWORD)。"
|
| 7 |
+
echo "备份功能将不可用。"
|
| 8 |
+
# 根据你的需求,如果缺少环境变量就完全退出,或者只跳过备份相关功能
|
| 9 |
+
# 这里选择退出,因为脚本的主要目的是备份
|
| 10 |
+
exit 1
|
| 11 |
+
fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
# --- 路径和常量定义 ---
|
| 14 |
+
# 定义要备份的本地目录 (修改点 1:明确指定路径)
|
| 15 |
+
LOCAL_DATA_DIR="/home/user/data"
|
| 16 |
+
# 定义备份文件名前缀 (修改点 2:更改前缀)
|
| 17 |
+
BACKUP_PREFIX="data_backup_"
|
| 18 |
+
# 定义 WebDAV 上的可选子目录路径
|
| 19 |
+
WEBDAV_BACKUP_PATH=${WEBDAV_BACKUP_PATH:-""} # 默认为空,即 WebDAV 根目录
|
| 20 |
+
# 构造完整的 WebDAV URL
|
| 21 |
+
FULL_WEBDAV_URL="${WEBDAV_URL}"
|
| 22 |
+
if [ -n "$WEBDAV_BACKUP_PATH" ]; then
|
| 23 |
+
# 确保 URL 拼接时斜杠正确
|
| 24 |
+
if [[ "${WEBDAV_URL}" != */ ]]; then
|
| 25 |
+
WEBDAV_URL="${WEBDAV_URL}/"
|
| 26 |
fi
|
| 27 |
+
if [[ "${WEBDAV_BACKUP_PATH}" == /* ]]; then
|
| 28 |
+
WEBDAV_BACKUP_PATH="${WEBDAV_BACKUP_PATH:1}"
|
| 29 |
+
fi
|
| 30 |
+
FULL_WEBDAV_URL="${WEBDAV_URL}${WEBDAV_BACKUP_PATH}"
|
| 31 |
fi
|
| 32 |
+
# 定义 Python 虚拟环境路径 (如果需要,请修改)
|
| 33 |
+
VENV_PATH="$HOME/venv/bin/activate"
|
| 34 |
+
# 定义同步间隔(秒),默认为 600 秒 (10 分钟)
|
| 35 |
+
SYNC_INTERVAL=${SYNC_INTERVAL:-600}
|
| 36 |
+
# 定义保留的备份数量
|
| 37 |
+
KEEP_BACKUPS=5
|
| 38 |
|
| 39 |
+
# --- 虚拟环境激活 ---
|
| 40 |
+
# 检查并激活 Python 虚拟环境 (如果 Python 脚本需要)
|
| 41 |
+
if [ -f "$VENV_PATH" ]; then
|
| 42 |
+
echo "激活 Python 虚拟环境: $VENV_PATH"
|
| 43 |
+
source "$VENV_PATH"
|
| 44 |
+
else
|
| 45 |
+
echo "警告:未找到 Python 虚拟环境 $VENV_PATH。如果 Python 脚本需要特定库,可能会失败。"
|
| 46 |
+
fi
|
| 47 |
|
| 48 |
+
# --- 函数定义 ---
|
| 49 |
+
|
| 50 |
+
# 函数:从 WebDAV 恢复最新备份
|
| 51 |
+
restore_backup() {
|
| 52 |
+
echo "开始从 WebDAV 下载最新备份..."
|
| 53 |
python3 -c "
|
|
|
|
|
|
|
| 54 |
import sys
|
| 55 |
import os
|
| 56 |
import tarfile
|
| 57 |
import requests
|
|
|
|
| 58 |
import shutil
|
| 59 |
+
from webdav3.client import Client
|
| 60 |
+
|
| 61 |
options = {
|
| 62 |
+
'webdav_hostname': '$FULL_WEBDAV_URL',
|
| 63 |
'webdav_login': '$WEBDAV_USERNAME',
|
| 64 |
'webdav_password': '$WEBDAV_PASSWORD'
|
| 65 |
}
|
| 66 |
+
local_data_dir = '$LOCAL_DATA_DIR' # 使用变量
|
| 67 |
+
backup_prefix = '$BACKUP_PREFIX' # 使用变量
|
| 68 |
+
|
| 69 |
try:
|
| 70 |
client = Client(options)
|
| 71 |
+
# 过滤文件,使用正确的前缀 (修改点 2)
|
| 72 |
+
backups = [f for f in client.list() if f.endswith('.tar.gz') and f.startswith(backup_prefix)]
|
| 73 |
+
|
|
|
|
| 74 |
if not backups:
|
| 75 |
+
print(f'在 {options['webdav_hostname']} 没有找到前缀为 {backup_prefix} 的备份文件。')
|
| 76 |
+
# 如果没有备份,确保目标目录存在但为空可能是期望行为,或者直接退出
|
| 77 |
+
os.makedirs(local_data_dir, exist_ok=True)
|
| 78 |
+
print(f'确保目录 {local_data_dir} 存在。')
|
| 79 |
+
sys.exit(0) # 正常退出,因为没有备份可恢复
|
| 80 |
+
|
| 81 |
+
latest_backup = sorted(backups)[-1]
|
| 82 |
+
print(f'找到最新备份文件:{latest_backup}')
|
| 83 |
+
remote_file_url = f'{options['webdav_hostname']}/{latest_backup}' # 假设 URL 构造正确
|
| 84 |
+
local_temp_file = f'/tmp/{latest_backup}'
|
| 85 |
+
|
| 86 |
+
print(f'正在从 {remote_file_url} 下载...')
|
| 87 |
+
with requests.get(remote_file_url, auth=(options['webdav_login'], options['webdav_password']), stream=True) as r:
|
| 88 |
+
r.raise_for_status() # 检查 HTTP 错误
|
| 89 |
+
with open(local_temp_file, 'wb') as f:
|
| 90 |
for chunk in r.iter_content(chunk_size=8192):
|
| 91 |
f.write(chunk)
|
| 92 |
+
print(f'成功下载备份文件到 {local_temp_file}')
|
| 93 |
+
|
| 94 |
+
if os.path.exists(local_temp_file):
|
| 95 |
+
print(f'准备恢复到目录: {local_data_dir}')
|
| 96 |
+
# 如果目录已存在,先清空它 (或者删除重建,取决于需求)
|
| 97 |
+
if os.path.exists(local_data_dir):
|
| 98 |
+
print(f'目录 {local_data_dir} 已存在,正在清空...')
|
| 99 |
+
# shutil.rmtree(local_data_dir) # 删除整个目录树
|
| 100 |
+
# 或者清空目录内容
|
| 101 |
+
for item in os.listdir(local_data_dir):
|
| 102 |
+
item_path = os.path.join(local_data_dir, item)
|
| 103 |
+
if os.path.isfile(item_path) or os.path.islink(item_path):
|
| 104 |
+
os.unlink(item_path)
|
| 105 |
+
elif os.path.isdir(item_path):
|
| 106 |
+
shutil.rmtree(item_path)
|
| 107 |
+
else:
|
| 108 |
+
os.makedirs(local_data_dir, exist_ok=True)
|
| 109 |
+
|
| 110 |
+
print(f'正在解压 {local_temp_file} 到 {local_data_dir}...')
|
| 111 |
+
# 解压备份文件 (修改点 1: 确保解压到正确的目录)
|
| 112 |
+
with tarfile.open(local_temp_file, 'r:gz') as tar:
|
| 113 |
+
tar.extractall(path=local_data_dir)
|
| 114 |
+
print(f'成功从 {latest_backup} 恢复备份到 {local_data_dir}')
|
| 115 |
+
os.remove(local_temp_file) # 清理临时文件
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
else:
|
| 117 |
+
print(f'错误:下载的备份文件 {local_temp_file} 未找到。')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
+
except requests.exceptions.RequestException as e:
|
| 120 |
+
print(f'下载备份时发生网络错误: {e}')
|
| 121 |
+
sys.exit(1)
|
| 122 |
+
except Exception as e:
|
| 123 |
+
print(f'恢复备份时发生错误: {e}')
|
| 124 |
+
sys.exit(1)
|
| 125 |
"
|
| 126 |
# 检查 Python 脚本的退出码
|
| 127 |
+
if [ $? -ne 0 ]; then
|
| 128 |
+
echo "恢复备份过程中发生错误,脚本终止。"
|
| 129 |
+
exit 1
|
| 130 |
+
fi
|
| 131 |
+
}
|
| 132 |
|
| 133 |
+
# 函数:同步数据到 WebDAV
|
| 134 |
+
sync_data() {
|
| 135 |
+
while true; do
|
| 136 |
+
echo "-----------------------------------------------------"
|
| 137 |
+
echo "开始同步过程于: $(date)"
|
| 138 |
+
|
| 139 |
+
# 检查本地数据目录是否存在
|
| 140 |
+
if [ ! -d "$LOCAL_DATA_DIR" ]; then
|
| 141 |
+
echo "警告:本地数据目录 $LOCAL_DATA_DIR 不存在。跳过此次备份。"
|
| 142 |
+
sleep $SYNC_INTERVAL
|
| 143 |
+
continue # 继续下一次循环
|
| 144 |
+
fi
|
| 145 |
|
| 146 |
+
# 检查本地数据目录是否为空
|
| 147 |
+
if [ -z "$(ls -A $LOCAL_DATA_DIR)" ]; then
|
| 148 |
+
echo "信息:本地数据目录 $LOCAL_DATA_DIR 为空。跳过此次备份。"
|
| 149 |
+
sleep $SYNC_INTERVAL
|
| 150 |
+
continue # 继续下一次循环
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
fi
|
|
|
|
| 152 |
|
| 153 |
+
timestamp=$(date +%Y%m%d_%H%M%S)
|
| 154 |
+
# 使用新的前缀生成备份文件名 (修改点 2)
|
| 155 |
+
backup_file="${BACKUP_PREFIX}${timestamp}.tar.gz"
|
| 156 |
+
local_temp_backup_path="/tmp/${backup_file}"
|
| 157 |
+
remote_backup_path="${FULL_WEBDAV_URL}/${backup_file}" # 确保 URL 没有双斜杠
|
|
|
|
|
|
|
| 158 |
|
| 159 |
+
echo "正在压缩目录: $LOCAL_DATA_DIR 到 $local_temp_backup_path"
|
| 160 |
+
# 使用 tar 压缩指定目录的内容 (修改点 1: 使用正确的源目录)
|
| 161 |
+
# -C "$LOCAL_DATA_DIR" 表示先切换到该目录,然后压缩 '.' (当前目录的所有内容)
|
| 162 |
+
# --warning=no-file-changed 忽略文件在压缩过程中改变的警告
|
| 163 |
+
tar --warning=no-file-changed -czf "$local_temp_backup_path" -C "$LOCAL_DATA_DIR" .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
+
if [ $? -ne 0 ]; then
|
| 166 |
+
echo "错误:压缩目录 $LOCAL_DATA_DIR 失败。"
|
| 167 |
+
rm -f "$local_temp_backup_path" # 清理可能存在的坏文件
|
| 168 |
+
sleep $SYNC_INTERVAL
|
| 169 |
+
continue # 继续下一次循环
|
| 170 |
+
fi
|
| 171 |
+
|
| 172 |
+
# 检查压缩文件大小
|
| 173 |
+
if [ ! -s "$local_temp_backup_path" ]; then
|
| 174 |
+
echo "错误:压缩后的文件 $local_temp_backup_path 大小为 0 或不存在。跳过上传。"
|
| 175 |
+
rm -f "$local_temp_backup_path"
|
| 176 |
+
sleep $SYNC_INTERVAL
|
| 177 |
+
continue
|
| 178 |
+
fi
|
| 179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
+
echo "正在上传备份文件: $local_temp_backup_path 到 $remote_backup_path"
|
| 182 |
+
# 使用 curl 上传文件到 WebDAV
|
| 183 |
+
curl --fail -u "$WEBDAV_USERNAME:$WEBDAV_PASSWORD" -T "$local_temp_backup_path" "$remote_backup_path"
|
|
|
|
|
|
|
| 184 |
|
| 185 |
+
if [ $? -eq 0 ]; then
|
| 186 |
+
echo "成功上传 ${backup_file} 到 WebDAV"
|
| 187 |
|
| 188 |
+
# 上传成功后,清理本地临时文件
|
| 189 |
+
echo "清理本地临时文件: $local_temp_backup_path"
|
| 190 |
+
rm -f "$local_temp_backup_path"
|
|
|
|
|
|
|
| 191 |
|
| 192 |
+
# 清理 WebDAV 上的旧备份文件
|
| 193 |
+
echo "开始清理 WebDAV 上的旧备份..."
|
| 194 |
+
python3 -c "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
import sys
|
|
|
|
| 196 |
from webdav3.client import Client
|
| 197 |
+
|
|
|
|
| 198 |
options = {
|
| 199 |
+
'webdav_hostname': '$FULL_WEBDAV_URL',
|
| 200 |
'webdav_login': '$WEBDAV_USERNAME',
|
| 201 |
'webdav_password': '$WEBDAV_PASSWORD'
|
| 202 |
}
|
| 203 |
+
backup_prefix = '$BACKUP_PREFIX' # 使用变量
|
| 204 |
+
keep_backups = $KEEP_BACKUPS # 使用变量
|
| 205 |
+
|
| 206 |
try:
|
| 207 |
client = Client(options)
|
| 208 |
+
# 过滤文件,使用正确的前缀 (修改点 2)
|
| 209 |
+
backups = [f for f in client.list() if f.endswith('.tar.gz') and f.startswith(backup_prefix)]
|
| 210 |
+
backups.sort() # 按名称排序(时间戳)
|
| 211 |
+
|
| 212 |
+
if len(backups) > keep_backups:
|
| 213 |
+
to_delete_count = len(backups) - keep_backups
|
| 214 |
+
print(f'找到 {len(backups)} 个备份,超过了保留数量 {keep_backups},将删除最早的 {to_delete_count} 个。')
|
| 215 |
+
for file_to_delete in backups[:to_delete_count]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
try:
|
| 217 |
+
# 确保路径正确,对于根目录下的文件,不需要加斜杠
|
| 218 |
+
delete_path = file_to_delete
|
| 219 |
+
print(f'正在删除旧备份: {delete_path}')
|
| 220 |
+
client.clean(delete_path) # clean 方法通常用于删除文件或目录
|
| 221 |
+
print(f'成功删除 {delete_path}')
|
| 222 |
+
except Exception as delete_err:
|
| 223 |
+
print(f'删除文件 {delete_path} 时出错: {delete_err}')
|
| 224 |
else:
|
| 225 |
+
print(f'找到 {len(backups)} 个备份,未达到保留上限 {keep_backups},无需清理。')
|
| 226 |
+
|
| 227 |
except Exception as e:
|
| 228 |
+
print(f'清理旧备份时发生错误: {e}')
|
| 229 |
"
|
| 230 |
+
if [ $? -ne 0 ]; then
|
| 231 |
+
echo "警告:清理 WebDAV 旧备份时发生错误。"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
fi
|
| 233 |
+
|
| 234 |
else
|
| 235 |
+
echo "错误:上传 ${backup_file} 到 WebDAV 失败。返回码: $?"
|
| 236 |
+
# 上传失败,也清理本地临时文件
|
| 237 |
+
echo "清理本地临时文件: $local_temp_backup_path"
|
| 238 |
+
rm -f "$local_temp_backup_path"
|
| 239 |
fi
|
| 240 |
+
|
| 241 |
+
echo "下次同步将在 ${SYNC_INTERVAL} 秒后进行..."
|
| 242 |
+
sleep $SYNC_INTERVAL
|
| 243 |
done
|
| 244 |
}
|
| 245 |
|
| 246 |
+
# --- 主逻辑 ---
|
| 247 |
+
|
| 248 |
+
# 1. 首次启动时尝试恢复最新备份 (可选,根据你的需求决定是否需要)
|
| 249 |
+
echo "检查并恢复最新备份(如果存在)..."
|
| 250 |
+
restore_backup
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
+
# 2. 等待一段时间后启动应用程序 (如果需要)
|
| 253 |
+
# 如果你不需要这个脚本来启动其他应用,可以注释掉下面两行
|
| 254 |
+
# echo "等待 30 秒后启动应用程序..."
|
| 255 |
+
# sleep 30
|
| 256 |
+
# echo "启动应用程序..."
|
| 257 |
+
# ./app server & # 你的应用程序启动命令
|
| 258 |
|
| 259 |
+
# 3. 启动后台同步进程
|
| 260 |
+
echo "启动后台数据同步进程..."
|
| 261 |
+
sync_data &
|
| 262 |
|
| 263 |
+
# 让主脚本保持运行(如果需要,例如在 Docker 容器中)
|
| 264 |
+
# 如果上面的 `./app server &` 是前台运行的,或者你用其他方式保持脚本运行,就不需要这个
|
| 265 |
+
# 如果 `sync_data` 是唯一需要长时间运行的,它已经在后台了,主脚本可以退出
|
| 266 |
+
# 但如果这是容器的主进程,你可能需要一个前台等待
|
| 267 |
+
# 例如: wait $! # 等待最后一个后台进程 (sync_data)
|
| 268 |
+
# 或者,如果启动了 app server: wait $(pgrep -f app server) # 等待 app server 进程
|
| 269 |
+
# 或者简单地无限循环:
|
| 270 |
+
# tail -f /dev/null # 保持脚本在前台运行
|
| 271 |
|
| 272 |
+
echo "脚本初始化完成,同步进程已在后台运行。"
|
| 273 |
+
# 根据你的运行环境决定脚本最后如何结束或保持运行
|