Skip to content

Commit

Permalink
feat(plugin): add force install option and backup/restore on failure
Browse files Browse the repository at this point in the history
  • Loading branch information
InfinityPacer committed Oct 17, 2024
1 parent 346c6dd commit e233bc6
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 8 deletions.
2 changes: 1 addition & 1 deletion app/core/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ def sync(self) -> List[str]:

def install_plugin(plugin):
start_time = time.time()
state, msg = self.pluginhelper.install(pid=plugin.id, repo_url=plugin.repo_url)
state, msg = self.pluginhelper.install(pid=plugin.id, repo_url=plugin.repo_url, force_install=True)
elapsed_time = time.time() - start_time
if state:
logger.info(
Expand Down
68 changes: 61 additions & 7 deletions app/helper/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,18 +148,20 @@ def install_report(self) -> bool:
json={"plugins": [{"plugin_id": plugin} for plugin in plugins]})
return True if res else False

def install(self, pid: str, repo_url: str, package_version: str = None) -> Tuple[bool, str]:
def install(self, pid: str, repo_url: str, package_version: str = None, force_install: bool = False) \
-> Tuple[bool, str]:
"""
安装插件,包括依赖安装和文件下载,相关资源支持自动降级策略
1. 检查并获取插件的指定版本,确认版本兼容性
2. 从 GitHub 获取文件列表(包括 requirements.txt)
3. 删除旧的插件目录
3. 删除旧的插件目录(如非强制安装则进行备份)
4. 下载并预安装 requirements.txt 中的依赖(如果存在)
5. 下载并安装插件的其他文件
6. 再次尝试安装依赖(确保安装完整)
:param pid: 插件 ID
:param repo_url: 插件仓库地址
:param package_version: 首选插件版本 (如 "v2", "v3"),如不指定则默认使用系统配置的版本
:param force_install: 是否强制安装插件,默认不启用,启用时不进行备份和恢复操作
:return: (是否成功, 错误信息)
"""
if SystemUtils.is_frozen():
Expand Down Expand Up @@ -197,7 +199,11 @@ def install(self, pid: str, repo_url: str, package_version: str = None) -> Tuple
if not file_list:
return False, msg

# 3. 删除旧的插件目录
# 3. 删除旧的插件目录,如果不强制安装则备份
backup_dir = None
if not force_install:
backup_dir = self.__backup_plugin(pid.lower())

self.__remove_old_plugin(pid.lower())

# 4. 查找并安装 requirements.txt 中的依赖,确保插件环境的依赖尽可能完整。依赖安装可能失败且不影响插件安装,目前只记录日志
Expand All @@ -215,9 +221,14 @@ def install(self, pid: str, repo_url: str, package_version: str = None) -> Tuple
logger.info(f"{pid} 准备开始下载插件文件")
success, message = self.__download_files(pid.lower(), file_list, user_repo, package_version, True)
if not success:
self.__remove_old_plugin(pid.lower())
logger.error(f"{pid} 下载插件文件失败:{message}")
logger.warning(f"{pid} 已清理对应插件目录,请尝试重新安装")
if backup_dir:
self.__restore_plugin(pid.lower(), backup_dir)
logger.warning(f"{pid} 插件安装失败,已还原备份插件")
else:
self.__remove_old_plugin(pid.lower())
logger.warning(f"{pid} 已清理对应插件目录,请尝试重新安装")

return False, message
else:
logger.info(f"{pid} 下载插件文件成功")
Expand All @@ -226,9 +237,13 @@ def install(self, pid: str, repo_url: str, package_version: str = None) -> Tuple
dependencies_exist, success, message = self.__install_dependencies_if_required(pid)
if dependencies_exist:
if not success:
self.__remove_old_plugin(pid.lower())
logger.error(f"{pid} 依赖安装失败:{message}")
logger.warning(f"{pid} 已清理对应插件目录,请尝试重新安装")
if backup_dir:
self.__restore_plugin(pid.lower(), backup_dir)
logger.warning(f"{pid} 插件安装失败,已还原备份插件")
else:
self.__remove_old_plugin(pid.lower())
logger.warning(f"{pid} 已清理对应插件目录,请尝试重新安装")
else:
logger.info(f"{pid} 依赖安装成功")

Expand Down Expand Up @@ -375,6 +390,45 @@ def __install_dependencies_if_required(self, pid: str) -> Tuple[bool, bool, str]

return False, False, "不存在依赖"

@staticmethod
def __backup_plugin(pid: str) -> str:
"""
备份旧插件目录
:param pid: 插件 ID
:return: 备份目录路径
"""
plugin_dir = Path(settings.ROOT_PATH) / "app" / "plugins" / pid
backup_dir = Path(settings.TEMP_PATH) / "plugins_backup" / pid

if plugin_dir.exists():
# 备份时清理已有的备份目录,防止残留文件影响
if backup_dir.exists():
shutil.rmtree(backup_dir, ignore_errors=True)
logger.debug(f"{pid} 旧的备份目录已清理 {backup_dir}")

shutil.copytree(plugin_dir, backup_dir, dirs_exist_ok=True)
logger.debug(f"{pid} 插件已备份到 {backup_dir}")

return str(backup_dir) if backup_dir.exists() else None

@staticmethod
def __restore_plugin(pid: str, backup_dir: str):
"""
还原旧插件目录
:param pid: 插件 ID
:param backup_dir: 备份目录路径
"""
plugin_dir = Path(settings.ROOT_PATH) / "app" / "plugins" / pid
if plugin_dir.exists():
shutil.rmtree(plugin_dir, ignore_errors=True)
logger.debug(f"{pid} 已清理插件目录 {plugin_dir}")

if Path(backup_dir).exists():
shutil.copytree(backup_dir, plugin_dir, dirs_exist_ok=True)
logger.debug(f"{pid} 已还原插件目录 {plugin_dir}")
shutil.rmtree(backup_dir, ignore_errors=True)
logger.debug(f"{pid} 已删除备份目录 {backup_dir}")

@staticmethod
def __remove_old_plugin(pid: str):
"""
Expand Down

0 comments on commit e233bc6

Please sign in to comment.