moxiaoying commited on
Commit
c63c907
·
verified ·
1 Parent(s): 2f84652

Update sync_data.sh

Browse files
Files changed (1) hide show
  1. sync_data.sh +274 -83
sync_data.sh CHANGED
@@ -1,21 +1,56 @@
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
- exit 0
7
- fi
8
-
9
- # 设置备份路径
10
- WEBDAV_BACKUP_PATH=${WEBDAV_BACKUP_PATH:-""}
11
- FULL_WEBDAV_URL="${WEBDAV_URL}"
12
- if [ -n "$WEBDAV_BACKUP_PATH" ]; then
13
- FULL_WEBDAV_URL="${WEBDAV_URL}/${WEBDAV_BACKUP_PATH}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  fi
15
 
16
- # 下载最新备份并恢复
17
  restore_backup() {
18
- echo "开始从 WebDAV 下载最新备份..."
 
 
 
 
 
 
19
  python3 -c "
20
  import sys
21
  import os
@@ -23,109 +58,265 @@ import tarfile
23
  import requests
24
  from webdav3.client import Client
25
  import shutil
 
 
 
 
26
  options = {
27
- 'webdav_hostname': '$FULL_WEBDAV_URL',
28
  'webdav_login': '$WEBDAV_USERNAME',
29
  'webdav_password': '$WEBDAV_PASSWORD'
30
  }
31
- client = Client(options)
32
- backups = [file for file in client.list() if file.endswith('.tar.gz') and file.startswith('webui_backup_')]
33
- if not backups:
34
- print('没有找到备份文件')
35
- sys.exit()
36
- latest_backup = sorted(backups)[-1]
37
- print(f'最新备份文件:{latest_backup}')
38
- with requests.get(f'$FULL_WEBDAV_URL/{latest_backup}', auth=('$WEBDAV_USERNAME', '$WEBDAV_PASSWORD'), stream=True) as r:
39
- if r.status_code == 200:
40
- with open(f'/tmp/{latest_backup}', 'wb') as f:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  for chunk in r.iter_content(chunk_size=8192):
42
  f.write(chunk)
43
- print(f'成功下载备份文件到 /tmp/{latest_backup}')
44
- if os.path.exists(f'/tmp/{latest_backup}'):
45
- # 解压备份文件到临时目录
46
- temp_dir = '/tmp/restore'
47
- os.makedirs(temp_dir, exist_ok=True)
48
- tar = tarfile.open(f'/tmp/{latest_backup}', 'r:gz')
49
- tar.extractall(temp_dir)
50
- tar.close()
51
- # 查找并移动 webui.db 文件
52
- for root, dirs, files in os.walk(temp_dir):
53
- if 'webui.db' in files:
54
- db_path = os.path.join(root, 'webui.db')
55
- os.makedirs('./data', exist_ok=True)
56
- os.replace(db_path, './data/webui.db')
57
- print(f'成功从 {latest_backup} 恢复备份')
58
- break
59
- else:
60
- print('备份文件中未找到 webui.db')
61
- # 删除临时目录
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  try:
63
- shutil.rmtree(temp_dir)
 
64
  except Exception as e:
65
- print(f'删除临时目录时出错:{e}')
66
- os.remove(f'/tmp/{latest_backup}')
67
- else:
68
- print('下载的备份文件不存在')
 
 
 
 
 
 
69
  else:
70
- print(f'下载备份失败:{r.status_code}')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  "
 
 
 
 
 
 
 
72
  }
73
 
74
- # 首次启动时下载最新备份
75
- echo "正在从 WebDAV 下载最新备份..."
76
  restore_backup
77
 
78
- # 同步函数
79
  sync_data() {
 
 
 
 
 
80
  while true; do
81
- echo "在 $(date) 开始同步进程"
 
 
 
 
 
82
 
83
- if [ -f "./data/webui.db" ]; then
 
84
  timestamp=$(date +%Y%m%d_%H%M%S)
85
- backup_file="webui_backup_${timestamp}.tar.gz"
 
 
86
 
87
- # 打包数据库文件
88
- tar -czf "/tmp/${backup_file}" ./data/webui.db
 
 
 
 
 
 
 
 
 
89
 
90
  # 上传新备份到WebDAV
91
- curl -u "$WEBDAV_USERNAME:$WEBDAV_PASSWORD" -T "/tmp/${backup_file}" "$FULL_WEBDAV_URL/${backup_file}"
 
 
 
 
92
  if [ $? -eq 0 ]; then
93
  echo "成功将 ${backup_file} 上传至 WebDAV"
94
- else
95
- echo "上传 ${backup_file} 至 WebDAV 失败"
96
- fi
 
97
 
98
- # 清理旧备份文件
99
- python3 -c "
 
100
  import sys
 
101
  from webdav3.client import Client
 
 
 
 
102
  options = {
103
- 'webdav_hostname': '$FULL_WEBDAV_URL',
104
  'webdav_login': '$WEBDAV_USERNAME',
105
  'webdav_password': '$WEBDAV_PASSWORD'
106
  }
107
- client = Client(options)
108
- backups = [file for file in client.list() if file.endswith('.tar.gz') and file.startswith('webui_backup_')]
109
- backups.sort()
110
- if len(backups) > 5:
111
- to_delete = len(backups) - 5
112
- for file in backups[:to_delete]:
113
- client.clean(file)
114
- print(f'成功删除 {file}。')
115
- else:
116
- print('仅找到 {} 个备份,无需清理。'.format(len(backups)))
117
- " 2>&1
118
-
119
- rm -f "/tmp/${backup_file}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  else
121
- echo "数据库文件尚不存在,等待下次同步..."
122
  fi
123
-
124
- SYNC_INTERVAL=${SYNC_INTERVAL:-600}
125
- echo "下次同步将在 ${SYNC_INTERVAL} 秒后进行..."
126
- sleep $SYNC_INTERVAL
127
  done
128
  }
129
 
130
- # 后台启动同步进程
131
- sync_data &
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 环境变量,启动时将禁用 WebDAV 备份与恢复功能。"
6
+ # 如果没有凭证,后续的备份恢复逻辑就没意义了,可以选择退出或标记禁用
7
+ WEBDAV_ENABLED=false
8
+ else
9
+ WEBDAV_ENABLED=true
10
+ # 设置备份路径,逻辑不变
11
+ WEBDAV_BACKUP_PATH=${WEBDAV_BACKUP_PATH:-"files_backups"} # 给个默认子目录名,更清晰
12
+ FULL_WEBDAV_URL="${WEBDAV_URL%/}" # 确保基础 URL 末尾没有斜杠
13
+ if [ -n "$WEBDAV_BACKUP_PATH" ]; then
14
+ # 使用 python 来安全地拼接 URL 路径,避免双斜杠等问题
15
+ FULL_WEBDAV_URL=$(python3 -c "from urllib.parse import urljoin; print(urljoin('$FULL_WEBDAV_URL/', '$WEBDAV_BACKUP_PATH/'))")
16
+ # 确保 WebDAV 上的备份目录存在
17
+ echo "检查并创建 WebDAV 备份目录: $FULL_WEBDAV_URL"
18
+ python3 -c "
19
+ import os
20
+ from webdav3.client import Client
21
+ options = {
22
+ 'webdav_hostname': '$WEBDAV_URL', # 注意这里用基础 URL
23
+ 'webdav_login': '$WEBDAV_USERNAME',
24
+ 'webdav_password': '$WEBDAV_PASSWORD'
25
+ }
26
+ client = Client(options)
27
+ # WEBDAV_BACKUP_PATH 可能包含多级,需要逐级检查创建
28
+ path_parts = filter(None, '$WEBDAV_BACKUP_PATH'.split('/'))
29
+ current_path = ''
30
+ for part in path_parts:
31
+ current_path = os.path.join(current_path, part)
32
+ if not client.is_dir(current_path):
33
+ print(f'WebDAV 目录 {current_path} 不存在,尝试创建...')
34
+ try:
35
+ client.mkdir(current_path)
36
+ print(f'成功创建 WebDAV 目录: {current_path}')
37
+ except Exception as e:
38
+ print(f'创建 WebDAV 目录 {current_path} 失败: {e}')
39
+ # 如果目录创建失败,后续可能无法上传,可以考虑退出或标记错误
40
+ "
41
+ fi
42
+ echo "WebDAV 备份将存储在: $FULL_WEBDAV_URL"
43
  fi
44
 
45
+ # 下载最新备份并恢复 `./files` 文件夹
46
  restore_backup() {
47
+ if [ "$WEBDAV_ENABLED" = false ]; then
48
+ echo "WebDAV 未配置,跳过恢复。"
49
+ return
50
+ fi
51
+
52
+ echo "开始从 WebDAV 下载最新 `./files` 备份..."
53
+ # 这坨 Python 代码是核心,得大改特改
54
  python3 -c "
55
  import sys
56
  import os
 
58
  import requests
59
  from webdav3.client import Client
60
  import shutil
61
+ import logging
62
+
63
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
64
+
65
  options = {
66
+ 'webdav_hostname': '$WEBDAV_URL', # 基础 URL
67
  'webdav_login': '$WEBDAV_USERNAME',
68
  'webdav_password': '$WEBDAV_PASSWORD'
69
  }
70
+ backup_dir_path = '$WEBDAV_BACKUP_PATH' # WebDAV 上的备份子目录
71
+ target_restore_dir = './' # 恢复的目标父目录
72
+ local_target_name = 'files' # 要恢复的文件夹名称
73
+
74
+ try:
75
+ client = Client(options)
76
+ # 列出备份目录下的文件,注意路径
77
+ logging.info(f'正在列出 WebDAV 目录: {backup_dir_path}')
78
+ files_in_backup_dir = client.list(backup_dir_path)
79
+ # 过滤出咱们的备份文件
80
+ backups = [f for f in files_in_backup_dir if f.endswith('.tar.gz') and os.path.basename(f).startswith('files_backup_')]
81
+
82
+ if not backups:
83
+ logging.warning('在 WebDAV 上没有找到任何有效的 `./files` 备份文件。')
84
+ sys.exit(0) # 没有备份就正常退出,不报错
85
+
86
+ # 对完整路径进行排序找出最新的,注意 client.list 可能返回带路径的文件名
87
+ # 我们需要按文件名(包含时间戳)排序
88
+ backups.sort(key=lambda x: os.path.basename(x), reverse=True)
89
+ latest_backup_rel_path = backups[0] # 这是相对于 WebDAV 根目录的路径
90
+ latest_backup_filename = os.path.basename(latest_backup_rel_path)
91
+ latest_backup_full_url = '${FULL_WEBDAV_URL%/}/' + latest_backup_filename # 构造完整的下载 URL
92
+
93
+ logging.info(f'找到最新备份文件:{latest_backup_filename} (���径: {latest_backup_rel_path})')
94
+ local_tmp_backup_path = f'/tmp/{latest_backup_filename}'
95
+
96
+ # 下载备份文件
97
+ logging.info(f'开始下载备份文件从: {latest_backup_full_url}')
98
+ with requests.get(latest_backup_full_url, auth=('$WEBDAV_USERNAME', '$WEBDAV_PASSWORD'), stream=True) as r:
99
+ r.raise_for_status() # 检查 HTTP 错误
100
+ with open(local_tmp_backup_path, 'wb') as f:
101
  for chunk in r.iter_content(chunk_size=8192):
102
  f.write(chunk)
103
+ logging.info(f'成功下载备份文件到: {local_tmp_backup_path}')
104
+
105
+ # 解压备份文件到临时目录
106
+ temp_extract_dir = '/tmp/restore_files_temp'
107
+ if os.path.exists(temp_extract_dir):
108
+ shutil.rmtree(temp_extract_dir) # 清理旧的临时解压目录
109
+ os.makedirs(temp_extract_dir, exist_ok=True)
110
+
111
+ logging.info(f'开始解压备份文件到: {temp_extract_dir}')
112
+ try:
113
+ with tarfile.open(local_tmp_backup_path, 'r:gz') as tar:
114
+ # 安全解压,防止路径遍历漏洞
115
+ def is_within_directory(directory, target):
116
+ abs_directory = os.path.abspath(directory)
117
+ abs_target = os.path.abspath(target)
118
+ prefix = os.path.commonprefix([abs_directory, abs_target])
119
+ return prefix == abs_directory
120
+
121
+ for member in tar.getmembers():
122
+ member_path = os.path.join(temp_extract_dir, member.name)
123
+ if not is_within_directory(temp_extract_dir, member_path):
124
+ raise Exception(f'试图解压到危险路径: {member.name}')
125
+ tar.extractall(path=temp_extract_dir)
126
+ logging.info(f'成功解压备份文件。')
127
+ except Exception as e:
128
+ logging.error(f'解压备份文件失败: {e}')
129
+ sys.exit(1)
130
+
131
+ # 检查解压后的临时目录中是否存在 'files' 文件夹
132
+ extracted_files_path = os.path.join(temp_extract_dir, local_target_name)
133
+ local_target_path = os.path.join(target_restore_dir, local_target_name)
134
+
135
+ if os.path.isdir(extracted_files_path):
136
+ logging.info(f'在备份中找到 \'{local_target_name}\' 目录。准备恢复...')
137
+ # 安全地替换本地的 ./files 目录
138
+ # 1. 删除现有的 ./files 目录(如果存在)
139
+ if os.path.exists(local_target_path):
140
+ logging.warning(f'本地已存在 \'{local_target_path}\' 目录,将删除并替换为备份内容。')
141
  try:
142
+ shutil.rmtree(local_target_path)
143
+ logging.info(f'成功删除旧的 \'{local_target_path}\' 目录。')
144
  except Exception as e:
145
+ logging.error(f'删除旧的 \'{local_target_path}\' 目录失败: {e}')
146
+ sys.exit(1)
147
+
148
+ # 2. 将解压出来的 files 目录移动到目标位置
149
+ try:
150
+ shutil.move(extracted_files_path, local_target_path)
151
+ logging.info(f'成功将备份恢复到 \'{local_target_path}\'。')
152
+ except Exception as e:
153
+ logging.error(f'移动解压后的 \'{local_target_name}\' 目录到 \'{local_target_path}\' 失败: {e}')
154
+ sys.exit(1)
155
  else:
156
+ logging.warning(f'解压后的备份文件中未找到 \'{local_target_name}\' 目录。无法执行恢复。')
157
+
158
+ # 清理临时文件和目录
159
+ logging.info('开始清理临时文件...')
160
+ try:
161
+ os.remove(local_tmp_backup_path)
162
+ logging.info(f'已删除临时备份文件: {local_tmp_backup_path}')
163
+ except OSError as e:
164
+ logging.warning(f'删除临时备份文件 {local_tmp_backup_path} 时出错: {e}')
165
+
166
+ try:
167
+ shutil.rmtree(temp_extract_dir)
168
+ logging.info(f'已删除临时解压目录: {temp_extract_dir}')
169
+ except OSError as e:
170
+ logging.warning(f'删除临时解压目录 {temp_extract_dir} ��出错: {e}')
171
+
172
+ except Exception as e:
173
+ logging.error(f'恢复过程中发生意外错误: {e}')
174
+ # 这里可以决定是否因为恢复失败而退出整个脚本
175
+ # sys.exit(1)
176
  "
177
+ # 检查 Python 脚本的退出码
178
+ if [ $? -ne 0 ]; then
179
+ echo "恢复备份过程中发生错误,请检查日志。"
180
+ # 这里可以根据需要决定是否中止后续操作
181
+ else
182
+ echo "恢复过程完成。"
183
+ fi
184
  }
185
 
186
+ # 首次启动时尝试恢复最新备份
187
+ echo "首次启动,尝试从 WebDAV 恢复最新 `./files` 备份..."
188
  restore_backup
189
 
190
+ # 定时同步函数,现在备份 `./files` 文件夹
191
  sync_data() {
192
+ if [ "$WEBDAV_ENABLED" = false ]; then
193
+ echo "WebDAV 未配置,跳过后台同步任务。"
194
+ return
195
+ fi
196
+
197
  while true; do
198
+ # 获取同步间隔,默认 10 分钟 (600 秒)
199
+ SYNC_INTERVAL=${SYNC_INTERVAL:-600}
200
+ echo "下一次 `./files` 备份将在 ${SYNC_INTERVAL} 秒后进行..."
201
+ sleep $SYNC_INTERVAL
202
+
203
+ echo "[$(date)] 开始执行 `./files` 备份和同步..."
204
 
205
+ # 检查 `./files` 目录是否存在,存在才备份
206
+ if [ -d "./files" ]; then
207
  timestamp=$(date +%Y%m%d_%H%M%S)
208
+ # 新的文件名格式
209
+ backup_file="files_backup_${timestamp}.tar.gz"
210
+ local_tmp_backup_path="/tmp/${backup_file}"
211
 
212
+ echo "找到 `./files` 目录,开始创建备份文件: ${backup_file}"
213
+ # 打包 `./files` 目录,注意 -C 参数可以避免包含 ./ 路径,或者直接打包相对路径
214
+ # 使用 tar -czf archive.tar.gz -C ./ files 会打包 files 目录本身
215
+ # 使用 tar -czf archive.tar.gz ./files 会打包 ./files 目录
216
+ # 我们希望解压后直接得到 files 目录,所以打包 ./files 比较合适
217
+ tar -czf "${local_tmp_backup_path}" ./files
218
+ if [ $? -ne 0 ]; then
219
+ echo "创建 tar 备份文件 ${backup_file} 失败!跳过此次同步。"
220
+ continue # 跳到下一次循环
221
+ fi
222
+ echo "成功创建本地备份文件: ${local_tmp_backup_path}"
223
 
224
  # 上传新备份到WebDAV
225
+ webdav_upload_url="${FULL_WEBDAV_URL%/}/${backup_file}" # 确保 URL 正确
226
+ echo "开始上传 ${backup_file} 到 ${webdav_upload_url}"
227
+ # 使用 curl 上传,增加 --fail 参数让非 2xx 响应码导致失败退出
228
+ curl --fail -u "$WEBDAV_USERNAME:$WEBDAV_PASSWORD" -T "${local_tmp_backup_path}" "${webdav_upload_url}"
229
+
230
  if [ $? -eq 0 ]; then
231
  echo "成功将 ${backup_file} 上传至 WebDAV"
232
+
233
+ # 上传成功后,清理本地临时文件
234
+ rm -f "${local_tmp_backup_path}"
235
+ echo "已清理本地临时备份文件: ${local_tmp_backup_path}"
236
 
237
+ # 清理 WebDAV 上的旧备份文件(保留最新的5个)
238
+ echo "开始清理 WebDAV 上的旧备份..."
239
+ python3 -c "
240
  import sys
241
+ import os
242
  from webdav3.client import Client
243
+ import logging
244
+
245
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
246
+
247
  options = {
248
+ 'webdav_hostname': '$WEBDAV_URL', # 基础 URL
249
  'webdav_login': '$WEBDAV_USERNAME',
250
  'webdav_password': '$WEBDAV_PASSWORD'
251
  }
252
+ backup_dir_path = '$WEBDAV_BACKUP_PATH' # WebDAV 上的备份子目录
253
+ keep_latest = 5 # 保留最新的备份数量
254
+
255
+ try:
256
+ client = Client(options)
257
+ logging.info(f'正在列出 WebDAV 目录: {backup_dir_path} 以进行清理')
258
+ files_in_backup_dir = client.list(backup_dir_path)
259
+ # 过滤出咱们的备份文件
260
+ backups = [f for f in files_in_backup_dir if f.endswith('.tar.gz') and os.path.basename(f).startswith('files_backup_')]
261
+
262
+ # 按文件名(包含时间戳)排序,旧的在前
263
+ backups.sort(key=lambda x: os.path.basename(x))
264
+
265
+ if len(backups) > keep_latest:
266
+ to_delete_count = len(backups) - keep_latest
267
+ logging.info(f'找到 {len(backups)} 个备份,超过限制 {keep_latest} 个,将删除最旧的 {to_delete_count} 个。')
268
+ # 删除最旧的几个备份,注意 client.clean 需要的是相对于 WebDAV 根的路径
269
+ for backup_to_delete_rel_path in backups[:to_delete_count]:
270
+ try:
271
+ # 使用 clean 方法删除,它需要的是相对路径
272
+ client.clean(backup_to_delete_rel_path)
273
+ logging.info(f'成功删除旧备份: {backup_to_delete_rel_path}')
274
+ except Exception as e:
275
+ logging.error(f'删除旧备份 {backup_to_delete_rel_path} 失败: {e}')
276
+ else:
277
+ logging.info(f'找到 {len(backups)} 个备份,未超过限制 {keep_latest} 个,无需清理。')
278
+
279
+ except Exception as e:
280
+ logging.error(f'清理旧备份时发生错误: {e}')
281
+ "
282
+ if [ $? -ne 0 ]; then
283
+ echo "清理 WebDAV 旧备份时发生错误,请检查日志。"
284
+ else
285
+ echo "WebDAV 旧备份清理完成。"
286
+ fi
287
+
288
+ else
289
+ echo "上传 ${backup_file} 至 WebDAV 失败!请检查网络或 WebDAV 服务器状态。"
290
+ # 上传失败,也清理本地临时文件避免堆积
291
+ rm -f "${local_tmp_backup_path}"
292
+ echo "已清理上传失败的本地临时备份文件: ${local_tmp_backup_path}"
293
+ fi
294
  else
295
+ echo "目标目录 `./files` 不存在或不是一个目录,跳过此次备份。"
296
  fi
297
+
298
+ echo "[$(date)] 同步进程结束。"
 
 
299
  done
300
  }
301
 
302
+ # 只有在 WebDAV 配置有效时才启动后台同步进程
303
+ if [ "$WEBDAV_ENABLED" = true ]; then
304
+ echo "启动后台 `./files` 备份同步进程..."
305
+ sync_data &
306
+ else
307
+ echo "WebDAV 未配置,后台备份同步任务已禁用。"
308
+ fi
309
+
310
+ # 你可以在这里添加启动你的主应用程序的命令
311
+ # 例如: exec python main.py
312
+ # 或者让脚本执行完毕,如果它只是一个 entrypoint 的一部分
313
+
314
+ echo "脚本主要逻辑执行完毕。后台同步任务(如果启用)正在运行。"
315
+
316
+ # 如果这是一个需要在前台保持运行的脚本(例如 Docker entrypoint),
317
+ # 你可能需要一个 `wait` 命令或者让 `sync_data` 在前台运行(去掉 &)
318
+ # 或者用其他方式保持容器运行。如果 sync_data 是后台任务,
319
+ # 这个脚本会执行完然后退出,可能导致容器停止。
320
+ # 如果需要保持运行,可以考虑将 sync_data() & 移到脚本最后,
321
+ # 或者添加一个 tail -f /dev/null 之类的命令保持前台。
322
+ # 但通常更好的做法是让主应用在前台运行,备份脚本作为后台进程。