66"""
77
88import asyncio
9+ import json
910import logging
1011import os
1112import shutil
13+ import sys
1214import tempfile
1315import zipfile
1416from pathlib import Path
1820
1921logger = logging .getLogger (__name__ )
2022
21- # 更新锁文件路径
23+ # 更新相关文件路径
2224UPDATE_LOCK_FILE = "/tmp/auto_bangumi_update.lock"
25+ UPDATE_FLAG_FILE = "/tmp/auto_bangumi_update_ready.flag"
2326
2427# 允许的下载域名白名单
2528ALLOWED_DOMAINS = ["github.com" , "codeload.github.com" ] # GitHub 的文件下载域名
@@ -33,8 +36,7 @@ class DockerUpdater:
3336
3437 def __init__ (self ):
3538 self .temp_dir : Path | None = None
36- self .backup_dir : Path = Path ("/app/backup" )
37- self .app_dir : Path = Path ("/app" )
39+ self .app_dir = Path ("/app" )
3840
3941 def _is_url_allowed (self , url : str ) -> bool :
4042 """检查 URL 是否在允许的白名单中
@@ -137,6 +139,29 @@ async def _download_file(self, url: str) -> Path:
137139 self ._cleanup_temp_files ()
138140 raise
139141
142+ def _create_update_flag (self , download_url : str , extract_path : Path ) -> None :
143+ """创建更新标志文件,供shell脚本检测
144+
145+ Args:
146+ download_url: 原始下载URL
147+ extract_path: 解压后的目录路径
148+ """
149+ try :
150+ flag_data = {
151+ "download_url" : download_url ,
152+ "extract_path" : str (extract_path ),
153+ "timestamp" : asyncio .get_event_loop ().time ()
154+ }
155+
156+ with open (UPDATE_FLAG_FILE , "w" ) as f :
157+ json .dump (flag_data , f , indent = 2 )
158+
159+ logger .info (f"[DockerUpdater] Created update flag file: { UPDATE_FLAG_FILE } " )
160+
161+ except Exception as e :
162+ logger .error (f"[DockerUpdater] Failed to create update flag: { e } " )
163+ raise
164+
140165 def _extract_zip (self , zip_path : Path ) -> Path :
141166 """解压 ZIP 文件
142167
@@ -172,102 +197,8 @@ def _extract_zip(self, zip_path: Path) -> Path:
172197 logger .error (f"[DockerUpdater] Extract failed: { e } " )
173198 raise
174199
175- def _backup_current_app (self ):
176- """备份当前应用的 src 和 dist 目录"""
177- try :
178- # 删除旧的备份
179- if self .backup_dir .exists ():
180- shutil .rmtree (self .backup_dir )
181-
182- # 创建备份目录
183- self .backup_dir .mkdir (exist_ok = True )
184200
185- # 只备份 src 和 dist 目录
186- src_dir = self .app_dir / "src"
187- dist_dir = self .app_dir / "dist"
188201
189- if src_dir .exists ():
190- shutil .copytree (src_dir , self .backup_dir / "src" )
191- logger .info (f"[DockerUpdater] Backed up src directory to { self .backup_dir / 'src' } " )
192-
193- if dist_dir .exists ():
194- shutil .copytree (dist_dir , self .backup_dir / "dist" )
195- logger .info (f"[DockerUpdater] Backed up dist directory to { self .backup_dir / 'dist' } " )
196-
197- except Exception as e :
198- logger .error (f"[DockerUpdater] Backup failed: { e } " )
199- raise
200-
201- def _install_new_app (self , source_dir : Path ):
202- """安装新应用的 src 和 dist 目录
203-
204- Args:
205- source_dir: 新应用源目录
206- """
207- try :
208- # 更新 src 目录
209- new_src = source_dir / "src"
210- if new_src .exists ():
211- target_src = self .app_dir / "src"
212- if target_src .exists ():
213- shutil .rmtree (target_src )
214- shutil .copytree (new_src , target_src )
215- logger .info (f"[DockerUpdater] Installed new src directory from { new_src } " )
216-
217- # 更新 dist 目录
218- new_dist = source_dir / "dist"
219- if new_dist .exists ():
220- target_dist = self .app_dir / "dist"
221- if target_dist .exists ():
222- shutil .rmtree (target_dist )
223- shutil .copytree (new_dist , target_dist )
224- logger .info (f"[DockerUpdater] Installed new dist directory from { new_dist } " )
225-
226- except Exception as e :
227- logger .error (f"[DockerUpdater] Installation failed: { e } " )
228- raise
229-
230-
231-
232- def _fix_permissions (self ):
233- """修复文件权限"""
234- try :
235- # 在 Docker 环境中,使用 chown 命令修复权限
236- os .system (f"chown -R ab:ab { self .app_dir } " )
237- logger .info ("[DockerUpdater] Fixed file permissions" )
238-
239- except Exception as e :
240- logger .error (f"[DockerUpdater] Failed to fix permissions: { e } " )
241-
242- def _rollback (self ):
243- """回滚到备份版本"""
244- try :
245- if not self .backup_dir .exists ():
246- logger .error ("[DockerUpdater] No backup found for rollback" )
247- return
248-
249- # 回滚 src 目录
250- backup_src = self .backup_dir / "src"
251- if backup_src .exists ():
252- target_src = self .app_dir / "src"
253- if target_src .exists ():
254- shutil .rmtree (target_src )
255- shutil .copytree (backup_src , target_src )
256- logger .info ("[DockerUpdater] Rolled back src directory" )
257-
258- # 回滚 dist 目录
259- backup_dist = self .backup_dir / "dist"
260- if backup_dist .exists ():
261- target_dist = self .app_dir / "dist"
262- if target_dist .exists ():
263- shutil .rmtree (target_dist )
264- shutil .copytree (backup_dist , target_dist )
265- logger .info ("[DockerUpdater] Rolled back dist directory" )
266-
267- logger .info ("[DockerUpdater] Rollback completed" )
268-
269- except Exception as e :
270- logger .error (f"[DockerUpdater] Rollback failed: { e } " )
271202
272203 def _cleanup_temp_files (self ):
273204 """清理临时文件"""
@@ -278,14 +209,14 @@ def _cleanup_temp_files(self):
278209 except Exception as e :
279210 logger .error (f"[DockerUpdater] Failed to cleanup temp files: { e } " )
280211
281- async def update (self , download_url : str ) -> dict :
282- """执行更新
212+ async def prepare_update (self , download_url : str ) -> dict :
213+ """准备更新:下载、解压、创建标志文件
283214
284215 Args:
285216 download_url: 更新包下载 URL
286217
287218 Returns:
288- dict: 更新结果
219+ dict: 准备结果
289220 """
290221 # 检查 URL
291222 if not self ._is_url_allowed (download_url ):
@@ -294,50 +225,37 @@ async def update(self, download_url: str) -> dict:
294225 # 创建更新锁
295226 if not self ._create_update_lock ():
296227 raise Exception ("Another update is already in progress" )
228+
297229 try :
298- logger .info ("[DockerUpdater] Starting Docker update process " )
230+ logger .info ("[DockerUpdater] Preparing update package " )
299231
300232 # 1. 下载更新包
301233 zip_path = await self ._download_file (download_url )
302234
303235 # 2. 解压更新包
304236 source_dir = self ._extract_zip (zip_path )
305237
306- # 3. 备份当前应用
307- self ._backup_current_app ()
308-
309- # 4. 安装新应用
310- self ._install_new_app (source_dir )
311-
312- # 6. 修复权限
313- self ._fix_permissions ()
238+ # 3. 验证解压结果
239+ if not (source_dir / "src" ).exists ():
240+ raise Exception ("Invalid update package: missing src directory" )
314241
315- logger .info ("[DockerUpdater] Update completed successfully" )
242+ # 4. 创建更新标志文件
243+ self ._create_update_flag (download_url , source_dir )
316244
317- return {"status" : "success" , "message" : "Update completed, container will restart" }
245+ logger .info ("[DockerUpdater] Update preparation completed" )
246+ return {"status" : "success" , "message" : "Update prepared, process will restart for file replacement" }
318247
319248 except Exception as e :
320- logger .error (f"[DockerUpdater] Update failed: { e } " )
321-
322- # 尝试回滚
323- try :
324- self ._rollback ()
325- except Exception as rollback_error :
326- logger .error (f"[DockerUpdater] Rollback also failed: { rollback_error } " )
327-
328- raise
329-
330- finally :
249+ logger .error (f"[DockerUpdater] Update preparation failed: { e } " )
331250 # 清理资源
332251 self ._cleanup_temp_files ()
333252 self ._remove_update_lock ()
253+ raise
334254
335- def force_restart (self ):
336- """强制重启容器(退出进程让 Docker 重启)"""
337- logger .info ("[DockerUpdater] Forcing container restart" )
338- # 在 Docker 环境中,.sh 有监控进程会自动重启
339- import sys
340-
255+ def trigger_graceful_restart (self ):
256+ """触发优雅重启:让 Python 进程退出,shell 脚本会检测更新标志并处理"""
257+ logger .info ("[DockerUpdater] Triggering graceful restart for update" )
258+ # 使用退出码 0 让 entrypoint.sh 知道这是正常的重启(热更新)
341259 sys .exit (0 )
342260
343261
0 commit comments