moxiaoying commited on
Commit
de9ddf2
·
verified ·
1 Parent(s): 201f710

Update sync_data.sh

Browse files
Files changed (1) hide show
  1. 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 "错误:缺少 WEBDAV_URL、WEBDAV_USERNAME 或 WEBDAV_PASSWORD 环境变量。"
6
- echo "WebDAV 备份与恢复功能将被禁用。"
7
- WEBDAV_ENABLED=false
8
- else
9
- WEBDAV_ENABLED=true
10
- WEBDAV_BACKUP_PATH=${WEBDAV_BACKUP_PATH:-"files_backups"} # 默认备份子目录
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
- # 检查 Python 脚本的退出码
54
- if [ $? -ne 0 ]; then
55
- echo "错误:初始化 WebDAV 备份目录失败。请检查日志。WebDAV 功能将禁用。"
56
- WEBDAV_ENABLED=false
57
- else
58
- # 如果 Python 成功设置完整的备份 URL (在 Python 内部拼接更安全,但这里简化处理)
59
- if [ -n "$WEBDAV_BACKUP_PATH" ]; then
60
- FULL_WEBDAV_URL=$(python3 -c "from urllib.parse import urljoin; print(urljoin('$FULL_WEBDAV_URL/', '$WEBDAV_BACKUP_PATH/'))")
61
- fi
62
- echo "WebDAV 备份将存储在: $FULL_WEBDAV_URL"
 
 
63
  fi
 
 
 
 
64
  fi
 
 
 
 
 
 
65
 
66
- # --- 下载最新备份并恢复 `./data` 文件夹 ---
67
- restore_backup() {
68
- if [ "$WEBDAV_ENABLED" = false ]; then
69
- echo "WebDAV 未配置或初始化失败,跳过恢复。"
70
- return
71
- fi
 
 
72
 
73
- echo "开始从 WebDAV 下载最新 `/home/user/data` 备份..."
74
- # 这部分 Python 代码用于下载、解压,暂时保持不变,因为错误不在于此
 
 
 
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 logging
85
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
86
  options = {
87
- 'webdav_hostname': '$WEBDAV_URL', # 基础 URL
88
  'webdav_login': '$WEBDAV_USERNAME',
89
  'webdav_password': '$WEBDAV_PASSWORD'
90
  }
91
- backup_dir_path = '$WEBDAV_BACKUP_PATH' # WebDAV 上的备份子目录
92
- target_restore_dir = '/home/user' # 恢复的目标父目录
93
- local_target_name = 'data' # 要恢复的文件夹名称
94
  try:
95
  client = Client(options)
96
- logging.info(f'在列出 WebDAV 目录: {backup_dir_path}')
97
- files_in_backup_dir = client.list(backup_dir_path)
98
- backups = [f for f in files_in_backup_dir if f.endswith('.tar.gz') and os.path.basename(f).startswith('files_backup_')]
99
-
100
  if not backups:
101
- logging.warning('在 WebDAV 没有找到任何有效的 `/home/user/data` 备份文件。')
102
- sys.exit(0)
103
- backups.sort(key=lambda x: os.path.basename(x), reverse=True)
104
- latest_backup_rel_path = backups[0]
105
- latest_backup_filename = os.path.basename(latest_backup_rel_path)
106
- # 使用正确的 FULL_WEBDAV_URL (已包含备份子路径)
107
- latest_backup_full_url = '${FULL_WEBDAV_URL%/}/' + latest_backup_filename
108
- logging.info(f'找到最新备份文件:{latest_backup_filename} (路径: {latest_backup_rel_path})')
109
- local_tmp_backup_path = f'/tmp/{latest_backup_filename}'
110
- logging.info(f'开始下载备份文件从: {latest_backup_full_url}')
111
- with requests.get(latest_backup_full_url, auth=('$WEBDAV_USERNAME', '$WEBDAV_PASSWORD'), stream=True) as r:
112
- r.raise_for_status()
113
- with open(local_tmp_backup_path, 'wb') as f:
 
 
114
  for chunk in r.iter_content(chunk_size=8192):
115
  f.write(chunk)
116
- logging.info(f'成功下载备份文件到: {local_tmp_backup_path}')
117
- temp_extract_dir = '/tmp/restore_files_temp'
118
- if os.path.exists(temp_extract_dir):
119
- shutil.rmtree(temp_extract_dir)
120
- os.makedirs(temp_extract_dir, exist_ok=True)
121
- logging.info(f'开始解压备份文件到: {temp_extract_dir}')
122
- try:
123
- with tarfile.open(local_tmp_backup_path, 'r:gz') as tar:
124
- def is_within_directory(directory, target):
125
- abs_directory = os.path.abspath(directory)
126
- abs_target = os.path.abspath(target)
127
- prefix = os.path.commonprefix([abs_directory, abs_target])
128
- return prefix == abs_directory
129
-
130
- import io # 需要导入 io
131
- def safe_extract(tar, path=".", members=None, *, numeric_owner=False):
132
- for member in tar.getmembers():
133
- member_path = os.path.join(path, member.name)
134
- if not is_within_directory(path, member_path):
135
- raise Exception("Attempted Path Traversal in Tar File")
136
- tar.extractall(path, members, numeric_owner=numeric_owner)
137
-
138
- safe_extract(tar, path=temp_extract_dir) # 使用安全解压
139
- logging.info(f'成功解压备份文件。')
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
- logging.warning(f'解压后的备份文件中未找到 \'{local_target_name}\' 目录无法执行恢复。')
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
- python_exit_code=$?
 
 
 
 
188
 
189
- if [ $python_exit_code -eq 0 ]; then
190
- echo "恢复过程 Python 部分成功执行。"
 
 
 
 
 
 
 
 
 
 
191
 
192
- # --- 新增权限设置 ---
193
- if [ -d "/home/user/data" ]; then
194
- echo "尝试为恢复后的 /home/user/data 目录设置权限 (u=rwX,g=rX,o=rX)..."
195
- chmod -R u=rwX,g=rX,o=rX /home/user/data
196
- if [ $? -eq 0 ]; then
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
- echo "恢复过程完成。"
207
- else
208
- echo "错误:恢复备份过程中发生错误,请检查 Python 日志。"
209
- # 恢复失败通常是比较严重的问题,可以选择退出
210
- # exit 1
211
- fi
212
- }
213
 
214
- # --- 首次启动时尝试恢复最新备份 ---
215
- echo "[Entrypoint] 首次启动,尝试从 WebDAV 恢复最新 `./data` 备份..."
216
- # 只有 WebDAV 启用且目录检查成功才执行恢复
217
- if [ "$WEBDAV_ENABLED" = true ]; then
218
- restore_backup
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
- while true; do
238
- # 获取同步间隔,默认 10 分钟 (600 秒)
239
- SYNC_INTERVAL=${SYNC_INTERVAL:-600}
240
- echo "[Sync] 下一次 `/home/user/data` 备份将在 ${SYNC_INTERVAL} 秒后进行..."
241
- sleep $SYNC_INTERVAL
242
 
243
- echo "[Sync][$(date)] 开始执行 `/home/user/data` 备份和同步..."
 
244
 
245
- # 检查 `/home/user/data` 目录是否存在
246
- if [ -d "/home/user/data" ]; then
247
- timestamp=$(date +%Y%m%d_%H%M%S)
248
- backup_file="files_backup_${timestamp}.tar.gz"
249
- local_tmp_backup_path="/tmp/${backup_file}"
250
 
251
- echo "[Sync] 找到 `/home/user/data` 目录,开始创建备份文件: ${backup_file}"
252
- # 打包 /home/user/data 目录
253
- tar -czf "${local_tmp_backup_path}" /home/user/data
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
- import logging
280
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
281
  options = {
282
- 'webdav_hostname': '$WEBDAV_URL', # 基础 URL
283
  'webdav_login': '$WEBDAV_USERNAME',
284
  'webdav_password': '$WEBDAV_PASSWORD'
285
  }
286
- backup_dir_path = '$WEBDAV_BACKUP_PATH' # WebDAV 上的备份子目录
287
- keep_latest = 5 # 保留最新的备份数
 
288
  try:
289
  client = Client(options)
290
- logging.info(f'在列出 WebDAV 目录: {backup_dir_path} 以进行清理')
291
- # 使用 remote_path 参数指定要列出的目录
292
- files_in_backup_dir_info = client.list(remote_path=backup_dir_path, get_info=True)
293
- # 过滤出咱们的备份文件,现在 list 返回的是字典列表,需要取 'path' 或 'name'
294
- # 'path' 通常是相对于 hostname 的完整路径
295
- backups = [f['path'] for f in files_in_backup_dir_info if f['path'].endswith('.tar.gz') and os.path.basename(f['path']).startswith('files_backup_')]
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
- # clean 需要的是相对路径
306
- client.clean(backup_to_delete_rel_path)
307
- logging.info(f'成功删除旧备份: {backup_to_delete_rel_path}')
308
- except Exception as e:
309
- logging.error(f'删除旧备份 {backup_to_delete_rel_path} 失败: {e}')
 
 
310
  else:
311
- logging.info(f'找到 {len(backups)} 个备份,未超过 {keep_latest},无需清理。')
 
312
  except Exception as e:
313
- logging.error(f'清理旧备份时发生错误: {e}')
314
  "
315
- if [ $? -ne 0 ]; then
316
- echo "[Sync] 清理 WebDAV 旧备份时发生错误,请检查日志。"
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 "[Sync] 目标目录 `/home/user/data` 不存在或不是一个目录,跳过此次备份。"
 
 
 
328
  fi
329
-
330
- echo "[Sync][$(date)] 同步进程结束。"
 
331
  done
332
  }
333
 
334
- # --- 启动后台同步进程 ---
335
- # 只有在 WebDAV 启用且目录检查成功后才启动后台同步
336
- if [ "$WEBDAV_ENABLED" = true ]; then
337
- echo "[Entrypoint] 启动后台 `/home/user/data` 备份同步进程 (sync_data.sh)..."
338
- sync_data &
339
- else
340
- echo "[Entrypoint] WebDAV 未启用或初始化失败,后台备份同步任务已禁用。"
341
- fi
342
 
343
- # --- 主逻辑完成,保持运行 (根据你的应用需求决定) ---
344
- echo "[Entrypoint] 脚本主要逻辑执行完毕。后台同步任务(如果启用)正在运。"
345
- echo "[Entrypoint] 你可以在这里启动你的主应用程序,例如 'exec python main.py'"
346
- echo "[Entrypoint] 或者,如果此脚本是容器的唯一进程,使用 'wait' 或其他方法保持容器运行。"
 
 
347
 
348
- # 如果需要这个脚本保持前台运行(例如作为 Docker CMD/ENTRYPOINT 且没有其他前台进程
349
- # 可以取消下面这行的注释,或者更好地让你的主应用跑在前
350
- # wait $! # 等待最后一个后台进程 (sync_data) 结束,如果 sync_data 意外退出,容器也会退出
351
 
352
- # 如果只是需要保持容器运行, sync_data 是无限循环的后台任务
353
- # 可以用 tail -f /dev/null sleep infinity
354
- # tail -f /dev/null
355
- exec "$@" # 如果这个脚本 entrypoint,允许执行 CMD 传入命令
 
 
 
 
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
+ # 根据你的运行环境决定脚本最后如何结束或保持运行