From 35eda7d1164f14e68a341a9b83b6a673801a9463 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 3 Jul 2024 08:49:59 +0800 Subject: [PATCH] fix filemanager --- app/modules/filemanager/__init__.py | 129 +++++++++++--------- app/modules/filemanager/storage/__init__.py | 9 +- app/modules/filemanager/storage/alipan.py | 41 +++++-- app/modules/filemanager/storage/local.py | 10 ++ app/modules/filemanager/storage/rclone.py | 45 +++++-- app/modules/filemanager/storage/u115.py | 43 +++++-- 6 files changed, 186 insertions(+), 91 deletions(-) diff --git a/app/modules/filemanager/__init__.py b/app/modules/filemanager/__init__.py index 86794f60..2d953d8b 100644 --- a/app/modules/filemanager/__init__.py +++ b/app/modules/filemanager/__init__.py @@ -809,52 +809,50 @@ class FileManagerModule(_ModuleBase): # 判断是否要覆盖 overflag = False - if target_storage == "local": - # 本地目标存储 - if new_file.exists() or new_file.is_symlink(): - # 本地目标文件已存在 - target_file = new_file - if new_file.is_symlink(): - target_file = new_file.readlink() - if not target_file.exists(): + # 目的操作对象 + target_oper: StorageBase = self.__get_storage_oper(target_storage) + target_item = target_oper.get_item(new_file) + if target_item: + # 目标文件已存在 + target_file = new_file + if target_storage == "local" and new_file.is_symlink(): + target_file = new_file.readlink() + if not target_file.exists(): + overflag = True + if not overflag: + # 目标文件已存在 + logger.info(f"目的文件系统中已经存在同名文件 {target_file},当前整理覆盖模式设置为 {overwrite_mode}") + match overwrite_mode: + case 'always': + # 总是覆盖同名文件 overflag = True - if not overflag: - # 目标文件已存在 - logger.info(f"文件本地系统中已经存在同名文件 {target_file},当前整理覆盖模式设置为 {overwrite_mode}") - match overwrite_mode: - case 'always': - # 总是覆盖同名文件 + case 'size': + # 存在时大覆盖小 + if target_item.size < fileitem.size: + logger.info(f"目标文件文件大小更小,将覆盖:{new_file}") overflag = True - case 'size': - # 存在时大覆盖小 - if target_file.stat().st_size < fileitem.size: - logger.info(f"目标文件文件大小更小,将覆盖:{new_file}") - overflag = True - else: - return TransferInfo(success=False, - message=f"本地媒体库存在同名文件,且质量更好", - fileitem=fileitem, - target_fileitem=__get_targetitem(target_file), - fail_list=[fileitem.path]) - case 'never': - # 存在不覆盖 + else: return TransferInfo(success=False, - message=f"本地媒体库存在同名文件,当前整理覆盖模式设置为不覆盖", + message=f"媒体库存在同名文件,且质量更好", fileitem=fileitem, target_fileitem=__get_targetitem(target_file), fail_list=[fileitem.path]) - case 'latest': - # 仅保留最新版本 - logger.info(f"当前整理覆盖模式设置为仅保留最新版本,将覆盖:{new_file}") - overflag = True - else: - if overwrite_mode == 'latest': - # 文件不存在,但仅保留最新版本 - logger.info(f"当前整理覆盖模式设置为 {overwrite_mode},仅保留最新版本") - self.__delete_local_version_files(new_file) + case 'never': + # 存在不覆盖 + return TransferInfo(success=False, + message=f"媒体库存在同名文件,当前覆盖模式为不覆盖", + fileitem=fileitem, + target_fileitem=__get_targetitem(target_file), + fail_list=[fileitem.path]) + case 'latest': + # 仅保留最新版本 + logger.info(f"当前整理覆盖模式设置为仅保留最新版本,将覆盖:{new_file}") + overflag = True else: - # TODO 支持网盘 - pass + if overwrite_mode == 'latest': + # 文件不存在,但仅保留最新版本 + logger.info(f"当前整理覆盖模式设置为 {overwrite_mode},仅保留最新版本,正在删除已有版本文件 ...") + self.__delete_version_files(target_storage, new_file) # 整理文件 new_item, err_msg = self.__transfer_file(fileitem=fileitem, target_storage=target_storage, @@ -978,32 +976,34 @@ class FileManagerModule(_ModuleBase): def media_exists(self, mediainfo: MediaInfo, **kwargs) -> Optional[ExistMediaInfo]: """ - TODO 支持网盘 - 判断媒体文件是否存在于本地文件系统,只支持标准媒体库结构 + 判断媒体文件是否存在于文件系统(网盘或本地文件),只支持标准媒体库结构 :param mediainfo: 识别的媒体信息 :return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}} """ # 检查本地媒体库 - dest_dirs = DirectoryHelper().get_local_library_dirs() + dest_dirs = DirectoryHelper().get_library_dirs() # 检查每一个媒体库目录 for dest_dir in dest_dirs: + # 存储 + storage_oper = self.__get_storage_oper(dest_dir.library_storage) + if not storage_oper: + continue # 媒体分类路径 - target_dir = self.__get_dest_dir(mediainfo=mediainfo, target_dir=dest_dir) - if not target_dir.exists(): + dir_path = self.__get_dest_dir(mediainfo=mediainfo, target_dir=dest_dir) + if not storage_oper.get_item(dir_path): continue # 重命名格式 rename_format = settings.TV_RENAME_FORMAT \ if mediainfo.type == MediaType.TV else settings.MOVIE_RENAME_FORMAT # 获取相对路径(重命名路径) - meta = MetaInfo(mediainfo.title) rel_path = self.get_rename_path( template_string=rename_format, - rename_dict=self.__get_naming_dict(meta=meta, + rename_dict=self.__get_naming_dict(meta=MetaInfo(mediainfo.title), mediainfo=mediainfo) ) # 取相对路径的第1层目录 if rel_path.parts: - media_path = target_dir / rel_path.parts[0] + media_path = dir_path / rel_path.parts[0] else: continue # 检查媒体文件夹是否存在 @@ -1035,32 +1035,45 @@ class FileManagerModule(_ModuleBase): # 不存在 return None - @staticmethod - def __delete_local_version_files(path: Path) -> bool: + def __delete_version_files(self, target_storage: str, path: Path) -> bool: """ - TODO 支持网盘 - 删除目录下的所有版本文件(仅本地) + 删除目录下的所有版本文件 + :param target_storage: 存储类型 :param path: 目录路径 """ + # 存储 + storage_oper = self.__get_storage_oper(target_storage) + if not storage_oper: + return False # 识别文件中的季集信息 meta = MetaInfoPath(path) season = meta.season episode = meta.episode - # 检索媒体文件 logger.warn(f"正在删除目标目录中其它版本的文件:{path.parent}") - media_files = SystemUtils.list_files(directory=path.parent, extensions=settings.RMT_MEDIAEXT) + # 获取父目录 + parent_item = storage_oper.get_item(path.parent) + if not parent_item: + logger.warn(f"目录 {path.parent} 不存在") + return False + # 检索媒体文件 + media_files = storage_oper.list(parent_item) if not media_files: - logger.info(f"目录中没有媒体文件:{path.parent}") + logger.info(f"目录 {path.parent} 中没有文件") return False # 删除文件 for media_file in media_files: - if str(media_file) == str(path): + media_path = Path(media_file.path) + if media_path == path: + continue + if media_file.type != "file": + continue + if f".{media_file.extension.lower()}" not in settings.RMT_MEDIAEXT: continue # 识别文件中的季集信息 - filemeta = MetaInfoPath(media_file) + filemeta = MetaInfoPath(media_path) # 相同季集的文件才删除 if filemeta.season != season or filemeta.episode != episode: continue - logger.info(f"正在删除文件:{media_file}") - media_file.unlink() + logger.info(f"正在删除文件:{media_file.name}") + storage_oper.delete(media_file) return True diff --git a/app/modules/filemanager/storage/__init__.py b/app/modules/filemanager/storage/__init__.py index ed0aeb6a..77fa40c8 100644 --- a/app/modules/filemanager/storage/__init__.py +++ b/app/modules/filemanager/storage/__init__.py @@ -64,7 +64,14 @@ class StorageBase(metaclass=ABCMeta): @abstractmethod def get_folder(self, path: Path) -> Optional[schemas.FileItem]: """ - 获取目录 + 获取目录,如目录不存在则创建 + """ + pass + + @abstractmethod + def get_item(self, path: Path) -> Optional[schemas.FileItem]: + """ + 获取文件或目录,不存在返回None """ pass diff --git a/app/modules/filemanager/storage/alipan.py b/app/modules/filemanager/storage/alipan.py index bcd448d0..d62802ea 100644 --- a/app/modules/filemanager/storage/alipan.py +++ b/app/modules/filemanager/storage/alipan.py @@ -463,12 +463,11 @@ class AliPan(StorageBase): 根据文件路程获取目录,不存在则创建 """ - def __find_dir_name(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]: + def __find_dir(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]: """ 查找下级目录中匹配名称的目录 """ - sub_files = self.list(_fileitem) - for sub_file in sub_files: + for sub_file in self.list(_fileitem): if sub_file.type != "dir": continue if sub_file.name == _name: @@ -480,16 +479,40 @@ class AliPan(StorageBase): for part in path.parts: if part == "/": continue - dir_file = __find_dir_name(fileitem, part) + dir_file = __find_dir(fileitem, part) if dir_file: - return dir_file + fileitem = dir_file else: dir_file = self.create_folder(dir_file, part) if not dir_file: - logger.warn(f"创建 aplipan 目录 {fileitem.path}{part} 失败!") + logger.warn(f"{self.schema.value}创建目录 {fileitem.path}{part} 失败!") return None fileitem = dir_file - return None + return fileitem + + def get_item(self, path: Path) -> Optional[schemas.FileItem]: + """ + 获取文件或目录,不存在返回None + """ + def __find_item(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]: + """ + 查找下级目录中匹配名称的目录或文件 + """ + for sub_file in self.list(_fileitem): + if sub_file.name == _name: + return sub_file + return None + + # 逐级查找和创建目录 + fileitem = schemas.FileItem(fileid="root") + for part in path.parts: + if part == "/": + continue + item = __find_item(fileitem, part) + if not item: + return None + fileitem = item + return fileitem def delete(self, fileitem: schemas.FileItem) -> bool: """ @@ -614,7 +637,7 @@ class AliPan(StorageBase): # 获取上传参数 result = res.json() if result.get("exist"): - logger.info(f"文件{result.get('file_name')}已存在,无需上传") + logger.info(f"文件 {result.get('file_name')} 已存在,无需上传") return schemas.FileItem( storage=self.schema.value, drive_id=result.get("drive_id"), @@ -660,7 +683,7 @@ class AliPan(StorageBase): path=f"{fileitem.path}{result.get('name')}", ) else: - logger.warn("上传文件失败:无法获取上传地址!") + logger.warn("阿里云盘上传文件失败:无法获取上传地址!") return None def move(self, fileitem: schemas.FileItem, target: schemas.FileItem) -> bool: diff --git a/app/modules/filemanager/storage/local.py b/app/modules/filemanager/storage/local.py index 4dce99ef..69f1f965 100644 --- a/app/modules/filemanager/storage/local.py +++ b/app/modules/filemanager/storage/local.py @@ -124,6 +124,16 @@ class LocalStorage(StorageBase): path.mkdir(parents=True, exist_ok=True) return self.__get_diritem(path) + def get_item(self, path: Path) -> Optional[schemas.FileItem]: + """ + 获取文件或目录,不存在返回None + """ + if not path.exists(): + return None + if path.is_file(): + return self.__get_fileitem(path) + return self.__get_diritem(path) + def detail(self, fileitm: schemas.FileItem) -> Optional[schemas.FileItem]: """ 获取文件详情 diff --git a/app/modules/filemanager/storage/rclone.py b/app/modules/filemanager/storage/rclone.py index 2b53067b..213e0ff8 100644 --- a/app/modules/filemanager/storage/rclone.py +++ b/app/modules/filemanager/storage/rclone.py @@ -98,7 +98,7 @@ class Rclone(StorageBase): items = json.loads(ret.stdout) return [self.__get_rcloneitem(item) for item in items] except Exception as err: - logger.error(f"浏览文件失败:{err}") + logger.error(f"rclone浏览文件失败:{err}") return None def create_folder(self, fileitm: schemas.FileItem, name: str) -> Optional[schemas.FileItem]: @@ -119,7 +119,7 @@ class Rclone(StorageBase): ret_fileitem.name = name return ret_fileitem except Exception as err: - logger.error(f"创建目录失败:{err}") + logger.error(f"rclone创建目录失败:{err}") return None def get_folder(self, path: Path) -> Optional[schemas.FileItem]: @@ -127,12 +127,11 @@ class Rclone(StorageBase): 根据文件路程获取目录,不存在则创建 """ - def __find_dir_name(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]: + def __find_dir(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]: """ 查找下级目录中匹配名称的目录 """ - sub_files = self.list(_fileitem) - for sub_file in sub_files: + for sub_file in self.list(_fileitem): if sub_file.type != "dir": continue if sub_file.name == _name: @@ -144,15 +143,35 @@ class Rclone(StorageBase): for part in path.parts: if part == "/": continue - dir_file = __find_dir_name(fileitem, part) + dir_file = __find_dir(fileitem, part) if dir_file: - return dir_file + fileitem = dir_file else: dir_file = self.create_folder(dir_file, part) if not dir_file: logger.warn(f"rclone创建目录 {fileitem.path}{part} 失败!") return None fileitem = dir_file + return fileitem + + def get_item(self, path: Path) -> Optional[schemas.FileItem]: + """ + 获取文件或目录,不存在返回None + """ + try: + ret = subprocess.run( + [ + 'rclone', 'lsjson', + f'MP:{path}' + ], + capture_output=True, + startupinfo=self.__get_hidden_shell() + ) + if ret.returncode == 0: + items = json.loads(ret.stdout) + return self.__get_rcloneitem(items[0]) + except Exception as err: + logger.error(f"rclone获取文件失败:{err}") return None def delete(self, fileitm: schemas.FileItem) -> bool: @@ -170,7 +189,7 @@ class Rclone(StorageBase): if retcode == 0: return True except Exception as err: - logger.error(f"删除文件失败:{err}") + logger.error(f"rclone删除文件失败:{err}") return False def rename(self, fileitm: schemas.FileItem, name: str) -> bool: @@ -189,7 +208,7 @@ class Rclone(StorageBase): if retcode == 0: return True except Exception as err: - logger.error(f"重命名文件失败:{err}") + logger.error(f"rclone重命名文件失败:{err}") return False def download(self, fileitm: schemas.FileItem, path: Path) -> bool: @@ -208,7 +227,7 @@ class Rclone(StorageBase): if retcode == 0: return True except Exception as err: - logger.error(f"复制文件失败:{err}") + logger.error(f"rclone复制文件失败:{err}") return False def upload(self, fileitm: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]: @@ -227,7 +246,7 @@ class Rclone(StorageBase): if retcode == 0: return self.__get_fileitem(path) except Exception as err: - logger.error(f"上传文件失败:{err}") + logger.error(f"rclone上传文件失败:{err}") return None def detail(self, fileitm: schemas.FileItem) -> Optional[schemas.FileItem]: @@ -247,7 +266,7 @@ class Rclone(StorageBase): items = json.loads(ret.stdout) return self.__get_rcloneitem(items[0]) except Exception as err: - logger.error(f"获取文件详情失败:{err}") + logger.error(f"rclone获取文件详情失败:{err}") return None def move(self, fileitm: schemas.FileItem, target: Path) -> bool: @@ -266,7 +285,7 @@ class Rclone(StorageBase): if retcode == 0: return True except Exception as err: - logger.error(f"移动文件失败:{err}") + logger.error(f"rclone移动文件失败:{err}") return False def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool: diff --git a/app/modules/filemanager/storage/u115.py b/app/modules/filemanager/storage/u115.py index b2b71532..38700955 100644 --- a/app/modules/filemanager/storage/u115.py +++ b/app/modules/filemanager/storage/u115.py @@ -88,7 +88,7 @@ class U115Pan(StorageBase, metaclass=Singleton): }, "" except Exception as e: logger.warn(f"115生成二维码失败:{str(e)}") - return {}, f"生成二维码失败:{str(e)}" + return {}, f"115生成二维码失败:{str(e)}" def check_login(self) -> Optional[Tuple[dict, str]]: """ @@ -143,7 +143,7 @@ class U115Pan(StorageBase, metaclass=Singleton): try: return self.cloud.storage().space() except Exception as e: - logger.error(f"获取115存储空间失败:{str(e)}") + logger.error(f"115获取存储空间失败:{str(e)}") return None def check(self) -> bool: @@ -175,7 +175,7 @@ class U115Pan(StorageBase, metaclass=Singleton): pickcode=item.pickcode ) for item in items] except Exception as e: - logger.error(f"浏览115文件失败:{str(e)}") + logger.error(f"115浏览文件失败:{str(e)}") return None def create_folder(self, fileitem: schemas.FileItem, name: str) -> Optional[schemas.FileItem]: @@ -197,7 +197,7 @@ class U115Pan(StorageBase, metaclass=Singleton): pickcode=result.pickcode ) except Exception as e: - logger.error(f"创建115目录失败:{str(e)}") + logger.error(f"115创建目录失败:{str(e)}") return None def get_folder(self, path: Path) -> Optional[schemas.FileItem]: @@ -205,12 +205,11 @@ class U115Pan(StorageBase, metaclass=Singleton): 根据文件路程获取目录,不存在则创建 """ - def __find_dir_name(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]: + def __find_dir(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]: """ 查找下级目录中匹配名称的目录 """ - sub_files = self.list(_fileitem) - for sub_file in sub_files: + for sub_file in self.list(_fileitem): if sub_file.type != "dir": continue if sub_file.name == _name: @@ -222,16 +221,40 @@ class U115Pan(StorageBase, metaclass=Singleton): for part in path.parts: if part == "/": continue - dir_file = __find_dir_name(fileitem, part) + dir_file = __find_dir(fileitem, part) if dir_file: - return dir_file + fileitem = dir_file else: dir_file = self.create_folder(dir_file, part) if not dir_file: logger.warn(f"115创建目录 {fileitem.path}{part} 失败!") return None fileitem = dir_file - return None + return fileitem + + def get_item(self, path: Path) -> Optional[schemas.FileItem]: + """ + 获取文件或目录,不存在返回None + """ + def __find_item(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]: + """ + 查找下级目录中匹配名称的目录或文件 + """ + for sub_file in self.list(_fileitem): + if sub_file.name == _name: + return sub_file + return None + + # 逐级查找和创建目录 + fileitem = schemas.FileItem(fileid="0") + for part in path.parts: + if part == "/": + continue + item = __find_item(fileitem, part) + if not item: + return None + fileitem = item + return fileitem def detail(self, fileitm: schemas.FileItem) -> Optional[schemas.FileItem]: """