from pathlib import Path from typing import Any, Optional, List, Dict from app.schemas.workflow import FileItem as _SchemaFileItem from app.chain import ChainBase from app.application.directory import DirectoryHelper from app.runtime.log import logger class StorageChain(ChainBase): """ 存储处理链 """ def manage_storage(self, storage: str, action: str, **params: Any) -> Dict[str, Any]: """ 对指定网盘存储执行管理动作 不包含任何存储特定逻辑:存储标识、动作语义与参数解释全部封闭在 实现 storage_manage 契约的模块内部 :param storage: 存储类型标识,用于模块路由 :param action: 通用管理动作标识,具体语义由存储实现解释 :param params: 表单与动作参数,原样透传给模块 :return: 统一结构 {"success": bool, "message": ..., "data": ...} """ result = self.run_module("storage_manage", storage=storage, action=action, **params) return result or {"success": False, "message": "该存储类型未启用或不支持此管理动作"} def list_files(self, fileitem: _SchemaFileItem, recursion: bool = False) -> Optional[List[_SchemaFileItem]]: """ 查询当前目录下所有目录和文件 """ return self.run_module("list_files", fileitem=fileitem, recursion=recursion) def any_files(self, fileitem: _SchemaFileItem, extensions: list = None) -> Optional[bool]: """ 查询当前目录下是否存在指定扩展名任意文件 """ return self.run_module("any_files", fileitem=fileitem, extensions=extensions) def create_folder(self, fileitem: _SchemaFileItem, name: str) -> Optional[_SchemaFileItem]: """ 创建目录 """ return self.run_module("create_folder", fileitem=fileitem, name=name) def get_folder(self, storage: str, path: Path) -> Optional[_SchemaFileItem]: """ 获取目录,不存在则递归创建 """ return self.run_module("get_folder", storage=storage, path=path) def download_file(self, fileitem: _SchemaFileItem, path: Path = None) -> Optional[Path]: """ 下载文件 :param fileitem: 文件项 :param path: 本地保存路径 """ return self.run_module("download_file", fileitem=fileitem, path=path) def upload_file(self, fileitem: _SchemaFileItem, path: Path, new_name: Optional[str] = None) -> Optional[_SchemaFileItem]: """ 上传文件 :param fileitem: 保存目录项 :param path: 本地文件路径 :param new_name: 新文件名 """ return self.run_module("upload_file", fileitem=fileitem, path=path, new_name=new_name) def delete_file(self, fileitem: _SchemaFileItem) -> Optional[bool]: """ 删除文件或目录 """ return self.run_module("delete_file", fileitem=fileitem) def rename_file(self, fileitem: _SchemaFileItem, name: str) -> Optional[bool]: """ 重命名文件或目录 """ return self.run_module("rename_file", fileitem=fileitem, name=name) def exists(self, fileitem: _SchemaFileItem) -> Optional[bool]: """ 判断文件或目录是否存在 """ return True if self.get_item(fileitem) else False def get_item(self, fileitem: _SchemaFileItem) -> Optional[_SchemaFileItem]: """ 查询目录或文件 """ return self.get_file_item(storage=fileitem.storage, path=Path(fileitem.path)) def get_file_item(self, storage: str, path: Path) -> Optional[_SchemaFileItem]: """ 根据路径获取文件项 """ return self.run_module("get_file_item", storage=storage, path=path) def get_parent_item(self, fileitem: _SchemaFileItem) -> Optional[_SchemaFileItem]: """ 获取上级目录项 """ return self.run_module("get_parent_item", fileitem=fileitem) def snapshot_storage(self, storage: str, path: Path, last_snapshot_time: float = None, max_depth: int = 5, previous_snapshot: Optional[Dict[str, Dict]] = None) -> Optional[Dict[str, Dict]]: """ 快照存储 :param storage: 存储类型 :param path: 路径 :param last_snapshot_time: 上次快照时间,用于增量快照 :param max_depth: 最大递归深度,避免过深遍历 :param previous_snapshot: 上次完整快照,用于增量对账 """ return self.run_module("snapshot_storage", storage=storage, path=path, last_snapshot_time=last_snapshot_time, max_depth=max_depth, previous_snapshot=previous_snapshot) def is_bluray_folder(self, fileitem: Optional[_SchemaFileItem]) -> bool: """ 检查是否蓝光目录 """ if not fileitem or fileitem.type != "dir": return False if self.get_file_item(storage=fileitem.storage, path=Path(fileitem.path) / "BDMV"): return True if self.get_file_item(storage=fileitem.storage, path=Path(fileitem.path) / "CERTIFICATE"): return True return False @staticmethod def contains_bluray_subdirectories(fileitems: Optional[List[_SchemaFileItem]]) -> bool: """ 判断是否包含蓝光必备的文件夹 """ required_files = {"BDMV", "CERTIFICATE"} return any( item.type == "dir" and item.name in required_files for item in fileitems or [] ) def delete_media_file(self, fileitem: _SchemaFileItem, delete_self: bool = True) -> bool: """ 删除媒体文件,以及不含媒体文件的目录 """ media_exts = self.runtime_config.media_extensions fileitem_path = Path(fileitem.path) if fileitem.path else Path("") if len(fileitem_path.parts) <= 2: logger.warn(f"【{fileitem.storage}】{fileitem.path} 根目录或一级目录不允许删除") return False if fileitem.type == "dir": # 本身是目录 if self.is_bluray_folder(fileitem): logger.warn(f"正在删除蓝光原盘目录:【{fileitem.storage}】{fileitem.path}") if not self.delete_file(fileitem): logger.warn(f"【{fileitem.storage}】{fileitem.path} 删除失败") return False elif delete_self: # 本身是文件,需要删除文件 logger.warn(f"正在删除文件【{fileitem.storage}】{fileitem.path}") if not self.delete_file(fileitem): logger.warn(f"【{fileitem.storage}】{fileitem.path} 删除失败") return False # 检查和删除上级空目录 dir_item = fileitem if fileitem.type == "dir" else self.get_parent_item(fileitem) if not dir_item: logger.warn(f"【{fileitem.storage}】{fileitem.path} 上级目录不存在") return True # 查找操作文件项匹配的配置目录(资源目录、媒体库目录) associated_dir = max( ( Path(p) for d in DirectoryHelper().get_dirs() for p in (d.download_path, d.library_path) if p and fileitem_path.is_relative_to(p) ), key=lambda path: len(path.parts), default=None, ) while dir_item and len(Path(dir_item.path).parts) > 2: # 目录是资源目录、媒体库目录的上级,则不处理 if associated_dir and associated_dir.is_relative_to(Path(dir_item.path)): logger.debug(f"【{dir_item.storage}】{dir_item.path} 位于资源或媒体库目录结构中,不删除") break elif not associated_dir and self.list_files(dir_item, recursion=False): logger.debug(f"【{dir_item.storage}】{dir_item.path} 不是空目录,不删除") break if self.any_files(dir_item, extensions=media_exts) is not False: logger.debug(f"【{dir_item.storage}】{dir_item.path} 存在媒体文件,不删除") break # 删除空目录并继续处理父目录 logger.warn(f"【{dir_item.storage}】{dir_item.path} 不存在其它媒体文件,正在删除空目录") if not self.delete_file(dir_item): logger.warn(f"【{dir_item.storage}】{dir_item.path} 删除失败") return False dir_item = self.get_parent_item(dir_item) return True