From 0da2bd64688cf9e75fa2f0277d8b8b99dcac865e Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 2 Jul 2024 20:32:32 +0800 Subject: [PATCH] fix rclone --- app/modules/filemanager/__init__.py | 25 +-- app/modules/filemanager/storage/__init__.py | 2 +- app/modules/filemanager/storage/rclone.py | 220 ++++++++++++++++++-- 3 files changed, 218 insertions(+), 29 deletions(-) diff --git a/app/modules/filemanager/__init__.py b/app/modules/filemanager/__init__.py index e957f280..ba0840c9 100644 --- a/app/modules/filemanager/__init__.py +++ b/app/modules/filemanager/__init__.py @@ -56,31 +56,32 @@ class FileManagerModule(_ModuleBase): if not dirs: return False, "未设置任何目录" for d in dirs: + # 下载目录 download_path = d.download_path if not download_path: return False, f"{d.name} 的下载目录未设置" if d.storage == "local" and not Path(download_path).exists(): return False, f"{d.name} 的下载目录 {download_path} 不存在" + # 媒体库目录 library_path = d.library_path if not library_path: return False, f"{d.name} 的媒体库目录未设置" if d.library_storage == "local" and not Path(library_path).exists(): return False, f"{d.name} 的媒体库目录 {library_path} 不存在" - # 检查软硬链接 - if d.transfer_type in ["link", "softlink"] \ - and (d.storage != "local" or d.library_storage != "local"): - return False, f"{d.name} 不是本地存储,不支持软硬链接" - # 检查硬链接 + # 硬链接 if d.transfer_type == "link" \ + and d.storage == "local" \ + and d.library_storage == "local" \ and not SystemUtils.is_same_disk(Path(download_path), Path(library_path)): return False, f"{d.name} 的下载目录 {download_path} 与媒体库目录 {library_path} 不在同一磁盘,无法硬链接" - # 检查网盘 - if d.storage != "local": - storage_oper = self.__get_storage_oper(d.storage) - if not storage_oper: - return False, f"{d.name} 的存储类型 {d.storage} 不支持" - if not storage_oper.check(): - return False, f"{d.name} 的存储测试不通过" + # 存储 + storage_oper = self.__get_storage_oper(d.storage) + if not storage_oper: + return False, f"{d.name} 的存储类型 {d.storage} 不支持" + if not storage_oper.check(): + return False, f"{d.name} 的存储测试不通过" + if d.transfer_type and d.transfer_type not in storage_oper.support_transtype(): + return False, f"{d.name} 的存储不支持 {d.transfer_type} 整理方式" return True, "" diff --git a/app/modules/filemanager/storage/__init__.py b/app/modules/filemanager/storage/__init__.py index c7430856..265aebac 100644 --- a/app/modules/filemanager/storage/__init__.py +++ b/app/modules/filemanager/storage/__init__.py @@ -83,7 +83,7 @@ class StorageBase(metaclass=ABCMeta): pass @abstractmethod - def download(self, fileitm: schemas.FileItem, path: Path): + def download(self, fileitm: schemas.FileItem, path: Path) -> bool: """ 下载文件,保存到本地 """ diff --git a/app/modules/filemanager/storage/rclone.py b/app/modules/filemanager/storage/rclone.py index 29b56919..af30a958 100644 --- a/app/modules/filemanager/storage/rclone.py +++ b/app/modules/filemanager/storage/rclone.py @@ -1,3 +1,5 @@ +import copy +import json import subprocess from pathlib import Path from typing import Optional, List @@ -6,6 +8,7 @@ from app import schemas from app.log import logger from app.modules.filemanager.storage import StorageBase from app.schemas.types import StorageSchema +from app.utils.string import StringUtils from app.utils.system import SystemUtils @@ -16,6 +19,7 @@ class Rclone(StorageBase): # 存储类型 schema = StorageSchema.Rclone + # 支持的整理方式 transtype = { "move": "移动", @@ -32,37 +36,221 @@ class Rclone(StorageBase): else: return None + def __get_fileitem(self, path: Path): + """ + 获取文件项 + """ + return schemas.FileItem( + storage=self.schema.value, + type="file", + path=str(path).replace("\\", "/"), + name=path.name, + basename=path.stem, + extension=path.suffix[1:], + size=path.stat().st_size, + modify_time=path.stat().st_mtime, + ) + + def __get_rcloneitem(self, item: dict): + """ + 获取rclone文件项 + """ + return schemas.FileItem( + storage=self.schema.value, + type="dir" if item.get("IsDir") else "file", + path=item.get("Path"), + name=item.get("Name"), + basename=Path(item.get("Name")).stem, + extension=Path(item.get("Name")).suffix[1:], + size=item.get("Size"), + modify_time=StringUtils.str_to_timestamp(item.get("ModTime")) + ) + def check(self) -> bool: - pass + """ + 检查存储是否可用 + """ + try: + retcode = subprocess.run( + ['rclone', 'lsf', 'MP:'], + startupinfo=self.__get_hidden_shell() + ).returncode + if retcode == 0: + return True + except Exception as err: + logger.error(f"rclone存储检查失败:{err}") + return False def list(self, fileitm: schemas.FileItem) -> Optional[List[schemas.FileItem]]: - pass + """ + 浏览文件 + """ + try: + ret = subprocess.run( + [ + 'rclone', 'lsjson', + f'MP:{fileitm.path}' + ], + capture_output=True, + startupinfo=self.__get_hidden_shell() + ) + if ret.returncode == 0: + items = json.loads(ret.stdout) + return [self.__get_rcloneitem(item) for item in items] + except Exception as err: + logger.error(f"浏览文件失败:{err}") + return None def create_folder(self, fileitm: schemas.FileItem, name: str) -> Optional[schemas.FileItem]: - pass + """ + 创建目录 + """ + try: + retcode = subprocess.run( + [ + 'rclone', 'mkdir', + f'MP:{fileitm.path}/{name}' + ], + startupinfo=self.__get_hidden_shell() + ).returncode + if retcode == 0: + ret_fileitem = copy.deepcopy(fileitm) + ret_fileitem.path = f"{fileitm.path}/{name}/" + ret_fileitem.name = name + return ret_fileitem + except Exception as err: + logger.error(f"创建目录失败:{err}") + return None def get_folder(self, path: Path) -> Optional[schemas.FileItem]: """ - 获取目录 + 根据文件路程获取目录,不存在则创建 """ - pass + + def __find_dir_name(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]: + """ + 查找下级目录中匹配名称的目录 + """ + sub_files = self.list(_fileitem) + for sub_file in sub_files: + if sub_file.type != "dir": + continue + if sub_file.name == _name: + return sub_file + return None + + # 逐级查找和创建目录 + fileitem = schemas.FileItem(path="/") + for part in path.parts: + if part == "/": + continue + dir_file = __find_dir_name(fileitem, part) + if dir_file: + return 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 None def delete(self, fileitm: schemas.FileItem) -> bool: - pass + """ + 删除文件 + """ + try: + retcode = subprocess.run( + [ + 'rclone', 'deletefile', + f'MP:{fileitm.path}' + ], + startupinfo=self.__get_hidden_shell() + ).returncode + if retcode == 0: + return True + except Exception as err: + logger.error(f"删除文件失败:{err}") + return False def rename(self, fileitm: schemas.FileItem, name: str) -> bool: - pass + """ + 重命名文件 + """ + try: + retcode = subprocess.run( + [ + 'rclone', 'moveto', + f'MP:{fileitm.path}', + f'MP:{Path(fileitm.path).parent}/{name}' + ], + startupinfo=self.__get_hidden_shell() + ).returncode + if retcode == 0: + return True + except Exception as err: + logger.error(f"重命名文件失败:{err}") + return False - def download(self, fileitm: schemas.FileItem, path: Path): - pass + def download(self, fileitm: schemas.FileItem, path: Path) -> bool: + """ + 下载文件 + """ + try: + retcode = subprocess.run( + [ + 'rclone', 'copyto', + f'MP:{fileitm.path}', + f'{path}' + ], + startupinfo=self.__get_hidden_shell() + ).returncode + if retcode == 0: + return True + except Exception as err: + logger.error(f"复制文件失败:{err}") + return False def upload(self, fileitm: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]: - pass + """ + 上传文件 + """ + try: + retcode = subprocess.run( + [ + 'rclone', 'copyto', + fileitm.path, + f'MP:{path}' + ], + startupinfo=self.__get_hidden_shell() + ).returncode + if retcode == 0: + return self.__get_fileitem(path) + except Exception as err: + logger.error(f"上传文件失败:{err}") + return None def detail(self, fileitm: schemas.FileItem) -> Optional[schemas.FileItem]: - pass + """ + 获取文件详情 + """ + try: + ret = subprocess.run( + [ + 'rclone', 'lsjson', + f'MP:{fileitm.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"获取文件详情失败:{err}") + return None - def move(self, fileitm: schemas.FileItem, target_file: schemas.FileItem) -> bool: + def move(self, fileitm: schemas.FileItem, target_file: Path) -> bool: """ 移动文件,target_file格式:rclone:path """ @@ -70,8 +258,8 @@ class Rclone(StorageBase): retcode = subprocess.run( [ 'rclone', 'moveto', - fileitm.path, - f'{target_file}' + f'MP:{fileitm.path}', + f'MP:{target_file}' ], startupinfo=self.__get_hidden_shell() ).returncode @@ -89,8 +277,8 @@ class Rclone(StorageBase): retcode = subprocess.run( [ 'rclone', 'copyto', - fileitm.path, - f'{target_file}' + f'MP:{fileitm.path}', + f'MP:{target_file}' ], startupinfo=self.__get_hidden_shell() ).returncode