diff --git a/app/chain/transfer.py b/app/chain/transfer.py index 846e13ec..f59142ab 100644 --- a/app/chain/transfer.py +++ b/app/chain/transfer.py @@ -393,10 +393,14 @@ class TransferChain(ChainBase): if src_match: # 按源目录匹配,以便找到更合适的目录配置 target_directory = self.directoryhelper.get_dir(file_mediainfo, - storage=file_item.storage, src_path=file_path) + storage=file_item.storage, + src_path=file_path, + target_storage=target_storage) else: # 未指定目标路径,根据媒体信息获取目标目录 - target_directory = self.directoryhelper.get_dir(file_mediainfo) + target_directory = self.directoryhelper.get_dir(file_mediainfo, + storage=target_storage, + target_storage=target_storage) # 执行整理 transferinfo: TransferInfo = self.transfer(fileitem=file_item, diff --git a/app/helper/directory.py b/app/helper/directory.py index 1f9389a7..9b512ace 100644 --- a/app/helper/directory.py +++ b/app/helper/directory.py @@ -48,16 +48,18 @@ class DirectoryHelper: """ return [d for d in self.get_library_dirs() if d.library_storage == "local"] - def get_dir(self, media: MediaInfo, storage: str = "local", - src_path: Path = None, dest_path: Path = None, fileitem: schemas.FileItem = None + def get_dir(self, media: MediaInfo, + storage: str = "local", fileitem: schemas.FileItem = None, src_path: Path = None, + target_storage: str = "local", dest_path: Path = None ) -> Optional[schemas.TransferDirectoryConf]: """ 根据媒体信息获取下载目录、媒体库目录配置 :param media: 媒体信息 - :param storage: 存储类型 + :param storage: 源存储类型 + :param target_storage: 目标存储类型 + :param fileitem: 文件项,使用文件路径匹配 :param src_path: 源目录,有值时直接匹配 :param dest_path: 目标目录,有值时直接匹配 - :param fileitem: 文件项,使用文件路径匹配 """ # 处理类型 if not media: @@ -70,21 +72,23 @@ class DirectoryHelper: # 没有启用整理的目录 if not d.monitor_type: continue - # 存储类型不匹配 + # 源存储类型不匹配 if storage and d.storage != storage: continue - # 下载目录 - download_path = Path(d.download_path) - # 媒体库目录 - library_path = Path(d.library_path) - # 有源目录时,源目录不匹配下载目录 - if src_path and not src_path.is_relative_to(download_path): + # 目标存储类型不匹配 + if target_storage and d.library_storage != target_storage: + continue + # 有文件项时,源存储不匹配 + if fileitem and fileitem.storage != d.storage: continue # 有文件项时,文件项不匹配下载目录 - if fileitem and not Path(fileitem.path).is_relative_to(download_path): + if fileitem and not Path(fileitem.path).is_relative_to(d.download_path): + continue + # 有源目录时,源目录不匹配下载目录 + if src_path and not src_path.is_relative_to(d.download_path): continue # 有目标目录时,目标目录不匹配媒体库目录 - if dest_path and not dest_path.is_relative_to(library_path): + if dest_path and not dest_path.is_relative_to(d.library_path): continue # 目录类型为全部的,符合条件 if not d.media_type: diff --git a/app/modules/filemanager/__init__.py b/app/modules/filemanager/__init__.py index 5534aff3..221f8d2c 100644 --- a/app/modules/filemanager/__init__.py +++ b/app/modules/filemanager/__init__.py @@ -1,4 +1,3 @@ -import copy import re from pathlib import Path from threading import Lock @@ -463,9 +462,9 @@ class FileManagerModule(_ModuleBase): target_file.parent.mkdir(parents=True) # 本地到本地 if transfer_type == "copy": - state = source_oper.copy(fileitem, target_file) + state = source_oper.copy(fileitem, target_file.parent, target_file.name) elif transfer_type == "move": - state = source_oper.move(fileitem, target_file) + state = source_oper.move(fileitem, target_file.parent, target_file.name) elif transfer_type == "link": state = source_oper.link(fileitem, target_file) elif transfer_type == "softlink": @@ -493,7 +492,7 @@ class FileManagerModule(_ModuleBase): else: return None, f"{fileitem.path} 上传 {target_storage} 失败" else: - return None, f"{target_file.parent} {target_storage} 目录获取失败" + return None, f"【{target_storage}】{target_file.parent} 目录获取失败" elif transfer_type == "move": # 移动 # 根据目的路径获取文件夹 @@ -508,7 +507,7 @@ class FileManagerModule(_ModuleBase): else: return None, f"{fileitem.path} 上传 {target_storage} 失败" else: - return None, f"{target_file.parent} {target_storage} 目录获取失败" + return None, f"【{target_storage}】{target_file.parent} 目录获取失败" elif fileitem.storage != "local" and target_storage == "local": # 网盘到本地 if target_file.exists(): @@ -532,25 +531,20 @@ class FileManagerModule(_ModuleBase): return None, f"{fileitem.path} {fileitem.storage} 下载失败" elif fileitem.storage == target_storage: # 同一网盘 - # 根据目的路径获取文件夹 - target_diritem = target_oper.get_folder(target_file.parent) - if target_diritem: - # 重命名文件 - if target_oper.rename(fileitem, target_file.name): - # 移动文件到新目录 - if source_oper.move(fileitem, target_diritem): - ret_fileitem = copy.deepcopy(fileitem) - ret_fileitem.path = target_diritem.path + "/" + target_file.name - ret_fileitem.name = target_file.name - ret_fileitem.basename = target_file.stem - ret_fileitem.parent_fileid = target_diritem.fileid - return ret_fileitem, "" - else: - return None, f"{fileitem.path} {target_storage} 移动文件失败" + if transfer_type == "copy": + # 移动文件到新目录 + if source_oper.move(fileitem, target_file.parent, target_file.name): + return target_oper.get_item(target_file), "" else: - return None, f"{fileitem.path} {target_storage} 重命名文件失败" + return None, f"【{target_storage}】{fileitem.path} 移动文件失败" + elif transfer_type == "move": + # 移动文件到新目录 + if source_oper.move(fileitem, target_file.parent, target_file.name): + return target_oper.get_item(target_file), "" + else: + return None, f"【{target_storage}】{fileitem.path} 移动文件失败" else: - return None, f"{target_file.parent} {target_storage} 目录获取失败" + return None, f"不支持的整理方式:{transfer_type}" return None, "未知错误" @@ -815,7 +809,8 @@ class FileManagerModule(_ModuleBase): else: logger.info(f"正在删除已存在的文件:{target_file}") target_file.unlink() - logger.info(f"正在整理文件:【{fileitem.storage}】{fileitem.path} 到 【{target_storage}】{target_file}") + logger.info(f"正在整理文件:【{fileitem.storage}】{fileitem.path} 到 【{target_storage}】{target_file}," + f"操作类型:{transfer_type}") new_item, errmsg = self.__transfer_command(fileitem=fileitem, target_storage=target_storage, target_file=target_file, diff --git a/app/modules/filemanager/storages/__init__.py b/app/modules/filemanager/storages/__init__.py index 4528ff0e..635217e8 100644 --- a/app/modules/filemanager/storages/__init__.py +++ b/app/modules/filemanager/storages/__init__.py @@ -122,7 +122,6 @@ class StorageBase(metaclass=ABCMeta): 下载文件,保存到本地,返回本地临时文件地址 :param fileitem: 文件项 :param path: 文件保存路径 - """ pass @@ -144,16 +143,22 @@ class StorageBase(metaclass=ABCMeta): pass @abstractmethod - def copy(self, fileitem: schemas.FileItem, target: Union[schemas.FileItem, Path]) -> bool: + def copy(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool: """ 复制文件 + :param fileitem: 文件项 + :param path: 目标目录 + :param new_name: 新文件名 """ pass @abstractmethod - def move(self, fileitem: schemas.FileItem, target: Union[schemas.FileItem, Path]) -> bool: + def move(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool: """ 移动文件 + :param fileitem: 文件项 + :param path: 目标目录 + :param new_name: 新文件名 """ pass diff --git a/app/modules/filemanager/storages/alipan.py b/app/modules/filemanager/storages/alipan.py index 0d95cb50..2cf08c8b 100644 --- a/app/modules/filemanager/storages/alipan.py +++ b/app/modules/filemanager/storages/alipan.py @@ -61,6 +61,7 @@ class AliPan(StorageBase, metaclass=Singleton): """ 初始化 aligo """ + def show_qrcode(qr_link: str): """ 显示二维码 @@ -370,6 +371,9 @@ class AliPan(StorageBase, metaclass=Singleton): def upload(self, fileitem: schemas.FileItem, path: Path, new_name: str = None) -> Optional[schemas.FileItem]: """ 上传文件,并标记完成 + :param fileitem: 上传目录项 + :param path: 目标目录 + :param new_name: 新文件名 """ if not self.aligo: return None @@ -383,19 +387,41 @@ class AliPan(StorageBase, metaclass=Singleton): return self.__get_fileitem(item) return None - def move(self, fileitem: schemas.FileItem, target: schemas.FileItem) -> bool: + def move(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool: """ 移动文件 + :param fileitem: 文件项 + :param path: 目标目录 + :param new_name: 新文件名 """ if not self.aligo: return False + target = self.get_folder(path) + if not target: + return False if self.aligo.move_file(file_id=fileitem.fileid, drive_id=fileitem.drive_id, - to_parent_file_id=target.fileid, to_drive_id=target.drive_id): + to_parent_file_id=target.fileid, to_drive_id=target.drive_id, + new_name=new_name): return True return False - def copy(self, fileitem: schemas.FileItem, target: schemas.FileItem) -> bool: - pass + def copy(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool: + """ + 复制文件 + :param fileitem: 文件项 + :param path: 目标目录 + :param new_name: 新文件名 + """ + if not self.aligo: + return False + target = self.get_folder(path) + if not target: + return False + if self.aligo.copy_file(file_id=fileitem.fileid, drive_id=fileitem.drive_id, + to_parent_file_id=target.fileid, to_drive_id=target.drive_id, + new_name=new_name): + return True + return False def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool: """ diff --git a/app/modules/filemanager/storages/alist.py b/app/modules/filemanager/storages/alist.py index 8f355c1b..85d18ff7 100644 --- a/app/modules/filemanager/storages/alist.py +++ b/app/modules/filemanager/storages/alist.py @@ -2,10 +2,10 @@ import json import logging from datetime import datetime from pathlib import Path -from typing import Optional, Tuple, List, Dict, Union +from typing import Optional, List, Dict -from requests import Response from cachetools import cached, TTLCache +from requests import Response from app import schemas from app.core.config import settings @@ -13,10 +13,11 @@ from app.log import logger from app.modules.filemanager.storages import StorageBase from app.schemas.types import StorageSchema from app.utils.http import RequestUtils +from app.utils.singleton import Singleton from app.utils.url import UrlUtils -class Alist(StorageBase): +class Alist(StorageBase, metaclass=Singleton): """ Alist相关操作 api文档:https://alist.nn.ci/zh/guide/api @@ -348,7 +349,7 @@ class Alist(StorageBase): result = resp.json() if result["code"] != 200: - logging.warning(f'获取文件 {path} 失败,错误信息:{result["message"]}') + logging.debug(f'获取文件 {path} 失败,错误信息:{result["message"]}') return return schemas.FileItem( @@ -576,51 +577,21 @@ class Alist(StorageBase): """ return self.get_item(Path(fileitem.path)) - @staticmethod - def __get_copy_and_move_data( - fileitem: schemas.FileItem, target: Union[schemas.FileItem, Path] - ) -> Tuple[str, str, List[str], bool]: - """ - 获取复制或移动文件需要的数据 - - :param fileitem: 文件项 - :param target: 目标文件项或目标路径 - :return: 源目录,目标目录,文件名列表,是否有效 - """ - name = Path(target).name - if fileitem.name != name: - return "", "", [], False - - src_dir = Path(fileitem.path).parent.as_posix() - if isinstance(target, schemas.FileItem): - traget_dir = Path(target.path).parent.as_posix() - else: - traget_dir = target.parent.as_posix() - - return src_dir, traget_dir, [name], True - - def copy( - self, fileitem: schemas.FileItem, target: Union[schemas.FileItem, Path] - ) -> bool: + def copy(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool: """ 复制文件 - - 源文件名和目标文件名必须相同 + :param fileitem: 文件项 + :param path: 目标目录 + :param new_name: 新文件名 """ - src_dir, dst_dir, names, is_valid = self.__get_copy_and_move_data( - fileitem, target - ) - if not is_valid: - return False - resp: Response = RequestUtils( headers=self.__get_header_with_token() ).post_res( self.__get_api_url("/api/fs/copy"), json={ - "src_dir": src_dir, - "dst_dir": dst_dir, - "names": names, + "src_dir": Path(fileitem.path).parent.as_posix(), + "dst_dir": path.as_posix(), + "names": [fileitem.name], }, ) """ @@ -655,28 +626,31 @@ class Alist(StorageBase): f'复制文件 {fileitem.path} 失败,错误信息:{result["message"]}' ) return False + # 重命名 + if fileitem.name != new_name: + self.rename( + self.get_item(path / fileitem.name), new_name + ) return True - def move( - self, fileitem: schemas.FileItem, target: Union[schemas.FileItem, Path] - ) -> bool: + def move(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool: """ 移动文件 + :param fileitem: 文件项 + :param path: 目标目录 + :param new_name: 新文件名 """ - src_dir, dst_dir, names, is_valid = self.__get_copy_and_move_data( - fileitem, target - ) - if not is_valid: - return False - + # 先重命名 + if fileitem.name != new_name: + self.rename(fileitem, new_name) resp: Response = RequestUtils( headers=self.__get_header_with_token() ).post_res( self.__get_api_url("/api/fs/move"), json={ - "src_dir": src_dir, - "dst_dir": dst_dir, - "names": names, + "src_dir": Path(fileitem.path).parent.as_posix(), + "dst_dir": path.as_posix(), + "names": [new_name], }, ) """ @@ -757,15 +731,7 @@ class Alist(StorageBase): @staticmethod def __parse_timestamp(time_str: str) -> float: - # try: - # # 尝试解析带微秒的时间格式 - # dt = datetime.strptime(time_str[:26], '%Y-%m-%dT%H:%M:%S.%f') - # except ValueError: - # # 如果失败,尝试解析不带微秒的时间格式 - # dt = datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%SZ') - - # 直接使用 ISO 8601 格式解析时间 - dt = datetime.fromisoformat(time_str) - - # 返回时间戳 - return dt.timestamp() + """ + 直接使用 ISO 8601 格式解析时间 + """ + return datetime.fromisoformat(time_str).timestamp() diff --git a/app/modules/filemanager/storages/local.py b/app/modules/filemanager/storages/local.py index 2fa5f678..2dd8f799 100644 --- a/app/modules/filemanager/storages/local.py +++ b/app/modules/filemanager/storages/local.py @@ -37,7 +37,7 @@ class LocalStorage(StorageBase): """ return True - def __get_fileitem(self, path: Path): + def __get_fileitem(self, path: Path) -> schemas.FileItem: """ 获取文件项 """ @@ -52,7 +52,7 @@ class LocalStorage(StorageBase): modify_time=path.stat().st_mtime, ) - def __get_diritem(self, path: Path): + def __get_diritem(self, path: Path) -> schemas.FileItem: """ 获取目录项 """ @@ -201,17 +201,6 @@ class LocalStorage(StorageBase): return None return self.get_item(target_path) - def copy(self, fileitem: schemas.FileItem, target_file: Path) -> bool: - """ - 复制文件 - """ - file_path = Path(fileitem.path) - code, message = SystemUtils.copy(file_path, target_file) - if code != 0: - logger.error(f"复制文件失败:{message}") - return False - return True - def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool: """ 硬链接文件 @@ -234,12 +223,29 @@ class LocalStorage(StorageBase): return False return True - def move(self, fileitem: schemas.FileItem, target: Path) -> bool: + def copy(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool: """ - 移动文件 + 复制文件 + :param fileitem: 文件项 + :param path: 目标目录 + :param new_name: 新文件名 """ file_path = Path(fileitem.path) - code, message = SystemUtils.move(file_path, target) + code, message = SystemUtils.copy(file_path, path / new_name) + if code != 0: + logger.error(f"复制文件失败:{message}") + return False + return True + + def move(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool: + """ + 移动文件 + :param fileitem: 文件项 + :param path: 目标目录 + :param new_name: 新文件名 + """ + file_path = Path(fileitem.path) + code, message = SystemUtils.move(file_path, path / new_name) if code != 0: logger.error(f"移动文件失败:{message}") return False diff --git a/app/modules/filemanager/storages/rclone.py b/app/modules/filemanager/storages/rclone.py index 39fa664f..7999d9ba 100644 --- a/app/modules/filemanager/storages/rclone.py +++ b/app/modules/filemanager/storages/rclone.py @@ -306,16 +306,19 @@ class Rclone(StorageBase): logger.error(f"rclone获取文件详情失败:{err}") return None - def move(self, fileitem: schemas.FileItem, target: Path) -> bool: + def move(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool: """ - 移动文件,target_file格式:rclone:path + 移动文件 + :param fileitem: 文件项 + :param path: 目标目录 + :param new_name: 新文件名 """ try: retcode = subprocess.run( [ 'rclone', 'moveto', f'MP:{fileitem.path}', - f'MP:{target}' + f'MP:{path / new_name}' ], startupinfo=self.__get_hidden_shell() ).returncode @@ -325,8 +328,27 @@ class Rclone(StorageBase): logger.error(f"rclone移动文件失败:{err}") return False - def copy(self, fileitem: schemas.FileItem, target_file: Path) -> bool: - pass + def copy(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool: + """ + 复制文件 + :param fileitem: 文件项 + :param path: 目标目录 + :param new_name: 新文件名 + """ + try: + retcode = subprocess.run( + [ + 'rclone', 'copyto', + f'MP:{fileitem.path}', + f'MP:{path / new_name}' + ], + startupinfo=self.__get_hidden_shell() + ).returncode + if retcode == 0: + return True + except Exception as err: + logger.error(f"rclone复制文件失败:{err}") + return False def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool: pass diff --git a/app/modules/filemanager/storages/u115.py b/app/modules/filemanager/storages/u115.py index d2e1e1f9..1889fc91 100644 --- a/app/modules/filemanager/storages/u115.py +++ b/app/modules/filemanager/storages/u115.py @@ -358,32 +358,38 @@ class U115Pan(StorageBase, metaclass=Singleton): logger.error(f"115上传文件失败:{str(e)}") return None - def move(self, fileitem: schemas.FileItem, target: schemas.FileItem) -> bool: - """ - 移动文件 - """ - if not self.client: - return False - try: - self.client.fs.move(fileitem.path, target.path) - return True - except Exception as e: - logger.error(f"115移动文件失败:{str(e)}") - return False - - def copy(self, fileitem: schemas.FileItem, target_file: Path) -> bool: + def copy(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool: """ 复制文件 + :param fileitem: 文件项 + :param path: 目标目录 + :param new_name: 新文件名 """ if not self.client: return False try: - self.client.fs.copy(fileitem.path, target_file) + self.client.fs.copy(fileitem.path, path / new_name) return True except Exception as e: logger.error(f"115复制文件失败:{str(e)}") return False + def move(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool: + """ + 移动文件 + :param fileitem: 文件项 + :param path: 目标目录 + :param new_name: 新文件名 + """ + if not self.client: + return False + try: + self.client.fs.move(fileitem.path, path / new_name) + return True + except Exception as e: + logger.error(f"115移动文件失败:{str(e)}") + return False + def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool: pass