From 02ad98c0243df4c904a80307184ab45115ce2bd9 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 30 Jun 2024 19:41:32 +0800 Subject: [PATCH] fix storage api --- app/api/apiv1.py | 5 +- app/api/endpoints/local.py | 273 ------------------ app/api/endpoints/storage.py | 164 +++++++++++ app/api/endpoints/u115.py | 212 -------------- app/chain/media.py | 70 +---- app/chain/storage.py | 59 ++++ .../{filetransfer => filemanager}/__init__.py | 65 +++++ .../storage/__init__.py | 0 .../storage/alipan.py | 2 +- .../storage/local.py | 2 +- .../storage/rclone.py | 10 +- .../storage/u115.py | 10 +- 12 files changed, 319 insertions(+), 553 deletions(-) delete mode 100644 app/api/endpoints/local.py create mode 100644 app/api/endpoints/storage.py delete mode 100644 app/api/endpoints/u115.py create mode 100644 app/chain/storage.py rename app/modules/{filetransfer => filemanager}/__init__.py (94%) rename app/modules/{filetransfer => filemanager}/storage/__init__.py (100%) rename app/modules/{filetransfer => filemanager}/storage/alipan.py (99%) rename app/modules/{filetransfer => filemanager}/storage/local.py (99%) rename app/modules/{filetransfer => filemanager}/storage/rclone.py (92%) rename app/modules/{filetransfer => filemanager}/storage/u115.py (97%) diff --git a/app/api/apiv1.py b/app/api/apiv1.py index 1dce06d1..df2e7711 100644 --- a/app/api/apiv1.py +++ b/app/api/apiv1.py @@ -2,7 +2,7 @@ from fastapi import APIRouter from app.api.endpoints import login, user, site, message, webhook, subscribe, \ media, douban, search, plugin, tmdb, history, system, download, dashboard, \ - local, transfer, mediaserver, bangumi, aliyun, u115 + transfer, mediaserver, bangumi, aliyun, storage api_router = APIRouter() api_router.include_router(login.router, prefix="/login", tags=["login"]) @@ -20,9 +20,8 @@ api_router.include_router(system.router, prefix="/system", tags=["system"]) api_router.include_router(plugin.router, prefix="/plugin", tags=["plugin"]) api_router.include_router(download.router, prefix="/download", tags=["download"]) api_router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"]) -api_router.include_router(local.router, prefix="/local", tags=["local"]) +api_router.include_router(storage.router, prefix="/local", tags=["storage"]) api_router.include_router(transfer.router, prefix="/transfer", tags=["transfer"]) api_router.include_router(mediaserver.router, prefix="/mediaserver", tags=["mediaserver"]) api_router.include_router(bangumi.router, prefix="/bangumi", tags=["bangumi"]) api_router.include_router(aliyun.router, prefix="/aliyun", tags=["aliyun"]) -api_router.include_router(u115.router, prefix="/u115", tags=["115"]) diff --git a/app/api/endpoints/local.py b/app/api/endpoints/local.py deleted file mode 100644 index 61de1326..00000000 --- a/app/api/endpoints/local.py +++ /dev/null @@ -1,273 +0,0 @@ -import shutil -from pathlib import Path -from typing import Any, List - -from fastapi import APIRouter, Depends, HTTPException -from starlette.responses import FileResponse, Response - -from app import schemas -from app.chain.transfer import TransferChain -from app.core.config import settings -from app.core.metainfo import MetaInfoPath -from app.core.security import verify_token, verify_uri_token -from app.helper.progress import ProgressHelper -from app.log import logger -from app.schemas.types import ProgressKey -from app.utils.system import SystemUtils - -router = APIRouter() - -IMAGE_TYPES = [".jpg", ".png", ".gif", ".bmp", ".jpeg", ".webp"] - - -@router.post("/list", summary="所有目录和文件(本地)", response_model=List[schemas.FileItem]) -def list_local(fileitem: schemas.FileItem, - sort: str = 'time', - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 查询当前目录下所有目录和文件 - :param fileitem: 文件项 - :param sort: 排序方式,name:按名称排序,time:按修改时间排序 - :param _: token - :return: 所有目录和文件 - """ - # 返回结果 - ret_items = [] - path = fileitem.path - if not fileitem.path or fileitem.path == "/": - if SystemUtils.is_windows(): - partitions = SystemUtils.get_windows_drives() or ["C:/"] - for partition in partitions: - ret_items.append(schemas.FileItem( - type="dir", - path=partition + "/", - name=partition, - basename=partition - )) - return ret_items - else: - path = "/" - else: - if SystemUtils.is_windows(): - path = path.lstrip("/") - elif not path.startswith("/"): - path = "/" + path - - # 遍历目录 - path_obj = Path(path) - if not path_obj.exists(): - logger.warn(f"目录不存在:{path}") - return [] - - # 如果是文件 - if path_obj.is_file(): - ret_items.append(schemas.FileItem( - type="file", - path=str(path_obj).replace("\\", "/"), - name=path_obj.name, - basename=path_obj.stem, - extension=path_obj.suffix[1:], - size=path_obj.stat().st_size, - modify_time=path_obj.stat().st_mtime, - )) - return ret_items - - # 扁历所有目录 - for item in SystemUtils.list_sub_directory(path_obj): - ret_items.append(schemas.FileItem( - type="dir", - path=str(item).replace("\\", "/") + "/", - name=item.name, - basename=item.stem, - modify_time=item.stat().st_mtime, - )) - - # 遍历所有文件,不含子目录 - for item in SystemUtils.list_sub_files(path_obj, - settings.RMT_MEDIAEXT - + settings.RMT_SUBEXT - + IMAGE_TYPES - + [".nfo"]): - ret_items.append(schemas.FileItem( - type="file", - path=str(item).replace("\\", "/"), - name=item.name, - basename=item.stem, - extension=item.suffix[1:], - size=item.stat().st_size, - modify_time=item.stat().st_mtime, - )) - # 排序 - if sort == 'time': - ret_items.sort(key=lambda x: x.modify_time, reverse=True) - else: - ret_items.sort(key=lambda x: x.name, reverse=False) - return ret_items - - -@router.get("/listdir", summary="所有目录(本地,不含文件)", response_model=List[schemas.FileItem]) -def list_local_dir(path: str, _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 查询当前目录下所有目录 - """ - # 返回结果 - ret_items = [] - if not path or path == "/": - if SystemUtils.is_windows(): - partitions = SystemUtils.get_windows_drives() or ["C:/"] - for partition in partitions: - ret_items.append(schemas.FileItem( - type="dir", - path=partition + "/", - name=partition, - children=[] - )) - return ret_items - else: - path = "/" - else: - if not SystemUtils.is_windows() and not path.startswith("/"): - path = "/" + path - - # 遍历目录 - path_obj = Path(path) - if not path_obj.exists(): - logger.warn(f"目录不存在:{path}") - return [] - - # 扁历所有目录 - for item in SystemUtils.list_sub_directory(path_obj): - ret_items.append(schemas.FileItem( - type="dir", - path=str(item).replace("\\", "/") + "/", - name=item.name, - children=[] - )) - return ret_items - - -@router.post("/mkdir", summary="创建目录(本地)", response_model=schemas.Response) -def mkdir_local(fileitem: schemas.FileItem, - name: str, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 创建目录 - """ - if not fileitem.path: - return schemas.Response(success=False) - path_obj = Path(fileitem.path) / name - if path_obj.exists(): - return schemas.Response(success=False) - path_obj.mkdir(parents=True, exist_ok=True) - return schemas.Response(success=True) - - -@router.post("/delete", summary="删除文件或目录(本地)", response_model=schemas.Response) -def delete_local(fileitem: schemas.FileItem, _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 删除文件或目录 - """ - if not fileitem.path: - return schemas.Response(success=False) - path_obj = Path(fileitem.path) - if not path_obj.exists(): - return schemas.Response(success=True) - if path_obj.is_file(): - path_obj.unlink() - else: - shutil.rmtree(path_obj, ignore_errors=True) - return schemas.Response(success=True) - - -@router.get("/download", summary="下载文件(本地)") -def download_local(path: str, _: schemas.TokenPayload = Depends(verify_uri_token)) -> Any: - """ - 下载文件或目录 - """ - if not path: - return schemas.Response(success=False) - path_obj = Path(path) - if not path_obj.exists(): - raise HTTPException(status_code=404, detail="文件不存在") - if path_obj.is_file(): - # 做为文件流式下载 - return FileResponse(path_obj) - else: - # 做为压缩包下载 - shutil.make_archive(base_name=path_obj.stem, format="zip", root_dir=path_obj) - reponse = Response(content=path_obj.read_bytes(), media_type="application/zip") - # 删除压缩包 - Path(f"{path_obj.stem}.zip").unlink() - return reponse - - -@router.post("/rename", summary="重命名文件或目录(本地)", response_model=schemas.Response) -def rename_local(fileitem: schemas.FileItem, - new_name: str, - recursive: bool = False, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 重命名文件或目录 - """ - if not fileitem.path or not new_name: - return schemas.Response(success=False) - path_obj = Path(fileitem.path) - if not path_obj.exists(): - return schemas.Response(success=False) - path_obj.rename(path_obj.parent / new_name) - if recursive: - transferchain = TransferChain() - media_exts = settings.RMT_MEDIAEXT + settings.RMT_SUBEXT + settings.RMT_AUDIO_TRACK_EXT - # 递归修改目录内文件(智能识别命名) - sub_files: List[schemas.FileItem] = list_local(fileitem=fileitem) - if sub_files: - # 开始进度 - progress = ProgressHelper() - progress.start(ProgressKey.BatchRename) - total = len(sub_files) - handled = 0 - for sub_file in sub_files: - handled += 1 - progress.update(value=handled / total * 100, - text=f"正在处理 {sub_file.name} ...", - key=ProgressKey.BatchRename) - if sub_file.type == "dir": - continue - if not sub_file.extension: - continue - if f".{sub_file.extension.lower()}" not in media_exts: - continue - sub_path = Path(sub_file.path) - meta = MetaInfoPath(sub_path) - mediainfo = transferchain.recognize_media(meta) - if not mediainfo: - progress.end(ProgressKey.BatchRename) - return schemas.Response(success=False, message=f"{sub_path.name} 未识别到媒体信息") - new_path = transferchain.recommend_name(meta=meta, mediainfo=mediainfo) - if not new_path: - progress.end(ProgressKey.BatchRename) - return schemas.Response(success=False, message=f"{sub_path.name} 未识别到新名称") - ret: schemas.Response = rename_local(fileitem, new_name=Path(new_path).name, recursive=False) - if not ret.success: - progress.end(ProgressKey.BatchRename) - return schemas.Response(success=False, message=f"{sub_path.name} 重命名失败!") - progress.end(ProgressKey.BatchRename) - return schemas.Response(success=True) - - -@router.get("/image", summary="读取图片(本地)") -def image_local(path: str, _: schemas.TokenPayload = Depends(verify_uri_token)) -> Any: - """ - 读取图片 - """ - if not path: - return None - path_obj = Path(path) - if not path_obj.exists(): - return None - if not path_obj.is_file(): - return None - # 判断是否图片文件 - if path_obj.suffix.lower() not in IMAGE_TYPES: - raise HTTPException(status_code=500, detail="图片读取出错") - return Response(content=path_obj.read_bytes(), media_type="image/jpeg") diff --git a/app/api/endpoints/storage.py b/app/api/endpoints/storage.py new file mode 100644 index 00000000..2bed0d48 --- /dev/null +++ b/app/api/endpoints/storage.py @@ -0,0 +1,164 @@ +from pathlib import Path +from typing import Any, List + +from fastapi import APIRouter, Depends +from starlette.responses import FileResponse + +from app import schemas +from app.chain.storage import StorageChain +from app.chain.transfer import TransferChain +from app.core.config import settings +from app.core.metainfo import MetaInfoPath +from app.core.security import verify_token, verify_uri_token +from app.helper.progress import ProgressHelper +from app.schemas.types import ProgressKey + +router = APIRouter() + + +@router.get("/qrcode", summary="生成二维码内容", response_model=schemas.Response) +def qrcode(_: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 生成二维码 + """ + qrcode_data, errmsg = StorageChain().generate_qrcode() + if qrcode_data: + return schemas.Response(success=True, data=qrcode_data, message=errmsg) + return schemas.Response(success=False) + + +@router.get("/check", summary="二维码登录确认", response_model=schemas.Response) +def check(_: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 二维码登录确认 + """ + data, errmsg = StorageChain().check_login() + if data: + return schemas.Response(success=True, data=data) + return schemas.Response(success=False, message=errmsg) + + +@router.post("/list", summary="所有目录和文件", response_model=List[schemas.FileItem]) +def list(fileitem: schemas.FileItem, + sort: str = 'updated_at', + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 查询当前目录下所有目录和文件 + :param fileitem: 文件项 + :param sort: 排序方式,name:按名称排序,time:按修改时间排序 + :param _: token + :return: 所有目录和文件 + """ + file_list = StorageChain().list_files(fileitem) + if sort == "name": + file_list.sort(key=lambda x: x.name) + else: + file_list.sort(key=lambda x: x.modify_time, reverse=True) + return file_list + + +@router.post("/mkdir", summary="创建目录", response_model=schemas.Response) +def mkdir(fileitem: schemas.FileItem, + name: str, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 创建目录 + :param fileitem: 文件项 + :param name: 目录名称 + :param _: token + """ + if not name: + return schemas.Response(success=False) + result = StorageChain().create_folder(fileitem, name) + if result: + return schemas.Response(success=True) + return schemas.Response(success=False) + + +@router.post("/delete", summary="删除文件或目录", response_model=schemas.Response) +def delete(fileitem: schemas.FileItem, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 删除文件或目录 + :param fileitem: 文件项 + :param _: token + """ + result = StorageChain().delete_file(fileitem) + if result: + return schemas.Response(success=True) + return schemas.Response(success=False) + + +@router.get("/download", summary="下载文件") +def download(fileitem: schemas.FileItem, + _: schemas.TokenPayload = Depends(verify_uri_token)) -> Any: + """ + 下载文件或目录 + :param fileitem: 文件项 + :param _: token + """ + # 临时目录 + tmp_file = settings.TEMP_PATH / fileitem.name + status = StorageChain().download_file(fileitem, tmp_file) + if status: + return FileResponse(path=tmp_file) + return schemas.Response(success=False) + + +@router.post("/rename", summary="重命名文件或目录", response_model=schemas.Response) +def rename(fileitem: schemas.FileItem, + new_name: str, + recursive: bool = False, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 重命名文件或目录 + :param fileitem: 文件项 + :param new_name: 新名称 + :param recursive: 是否递归修改 + :param _: token + """ + if not fileitem.fileid or not new_name: + return schemas.Response(success=False) + result = StorageChain().rename_file(fileitem, new_name) + if result: + if recursive: + transferchain = TransferChain() + media_exts = settings.RMT_MEDIAEXT + settings.RMT_SUBEXT + settings.RMT_AUDIO_TRACK_EXT + # 递归修改目录内文件(智能识别命名) + sub_files: List[schemas.FileItem] = StorageChain().list_files(fileitem) + if sub_files: + # 开始进度 + progress = ProgressHelper() + progress.start(ProgressKey.BatchRename) + total = len(sub_files) + handled = 0 + for sub_file in sub_files: + handled += 1 + progress.update(value=handled / total * 100, + text=f"正在处理 {sub_file.name} ...", + key=ProgressKey.BatchRename) + if sub_file.type == "dir": + continue + if not sub_file.extension: + continue + if f".{sub_file.extension.lower()}" not in media_exts: + continue + sub_path = Path(f"{fileitem.path}{sub_file.name}") + meta = MetaInfoPath(sub_path) + mediainfo = transferchain.recognize_media(meta) + if not mediainfo: + progress.end(ProgressKey.BatchRename) + return schemas.Response(success=False, message=f"{sub_path.name} 未识别到媒体信息") + new_path = transferchain.recommend_name(meta=meta, mediainfo=mediainfo) + if not new_path: + progress.end(ProgressKey.BatchRename) + return schemas.Response(success=False, message=f"{sub_path.name} 未识别到新名称") + ret: schemas.Response = rename(fileitem=sub_file, + new_name=Path(new_path).name, + recursive=False) + if not ret.success: + progress.end(ProgressKey.BatchRename) + return schemas.Response(success=False, message=f"{sub_path.name} 重命名失败!") + progress.end(ProgressKey.BatchRename) + return schemas.Response(success=True) + return schemas.Response(success=False) diff --git a/app/api/endpoints/u115.py b/app/api/endpoints/u115.py deleted file mode 100644 index 9a6ae198..00000000 --- a/app/api/endpoints/u115.py +++ /dev/null @@ -1,212 +0,0 @@ -from pathlib import Path -from typing import Any, List - -from fastapi import APIRouter, Depends, HTTPException -from starlette.responses import Response - -from app import schemas -from app.chain.transfer import TransferChain -from app.core.config import settings -from app.core.metainfo import MetaInfoPath -from app.core.security import verify_token, verify_uri_token -from app.helper.progress import ProgressHelper -from app.schemas.types import ProgressKey -from app.utils.http import RequestUtils - -router = APIRouter() - - -@router.get("/qrcode", summary="生成二维码内容", response_model=schemas.Response) -def qrcode(_: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 生成二维码 - """ - qrcode_data = U115Helper().generate_qrcode() - if qrcode_data: - return schemas.Response(success=True, data={ - 'codeContent': qrcode_data - }) - return schemas.Response(success=False) - - -@router.get("/check", summary="二维码登录确认", response_model=schemas.Response) -def check(_: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 二维码登录确认 - """ - data, errmsg = U115Helper().check_login() - if data: - return schemas.Response(success=True, data=data) - return schemas.Response(success=False, message=errmsg) - - -@router.get("/storage", summary="查询存储空间信息", response_model=schemas.Response) -def storage(_: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 查询存储空间信息 - """ - storage_info = U115Helper().storage() - if storage_info: - return schemas.Response(success=True, data={ - "total": storage_info[0], - "used": storage_info[1] - }) - return schemas.Response(success=False) - - -@router.post("/list", summary="所有目录和文件(115网盘)", response_model=List[schemas.FileItem]) -def list_115(fileitem: schemas.FileItem, - sort: str = 'updated_at', - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 查询当前目录下所有目录和文件 - :param fileitem: 文件项 - :param sort: 排序方式,name:按名称排序,time:按修改时间排序 - :param _: token - :return: 所有目录和文件 - """ - if not fileitem.fileid: - return [] - if not fileitem.path: - path = "/" - else: - path = fileitem.path - if fileitem.fileid == "root": - fileid = "0" - else: - fileid = fileitem.fileid - if fileitem.type == "file": - name = Path(path).name - suffix = Path(name).suffix[1:] - return [schemas.FileItem( - fileid=fileid, - type="file", - path=path.rstrip('/'), - name=name, - extension=suffix, - pickcode=fileitem.pickcode - )] - file_list = U115Helper().list(parent_file_id=fileid, path=path) - if sort == "name": - file_list.sort(key=lambda x: x.name) - else: - file_list.sort(key=lambda x: x.modify_time, reverse=True) - return file_list - - -@router.post("/mkdir", summary="创建目录(115网盘)", response_model=schemas.Response) -def mkdir_115(fileitem: schemas.FileItem, - name: str, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 创建目录 - """ - if not fileitem.fileid or not name: - return schemas.Response(success=False) - result = U115Helper().create_folder(parent_file_id=fileitem.fileid, name=name, path=fileitem.path) - if result: - return schemas.Response(success=True) - return schemas.Response(success=False) - - -@router.post("/delete", summary="删除文件或目录(115网盘)", response_model=schemas.Response) -def delete_115(fileitem: schemas.FileItem, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 删除文件或目录 - """ - if not fileitem.fileid: - return schemas.Response(success=False) - result = U115Helper().delete(fileitem.fileid) - if result: - return schemas.Response(success=True) - return schemas.Response(success=False) - - -@router.get("/download", summary="下载文件(115网盘)") -def download_115(pickcode: str, - _: schemas.TokenPayload = Depends(verify_uri_token)) -> Any: - """ - 下载文件或目录 - """ - if not pickcode: - return schemas.Response(success=False) - ticket = U115Helper().download(pickcode) - if ticket: - # 请求数据,并以文件流的方式返回 - res = RequestUtils(headers=ticket.headers).get_res(ticket.url) - if res: - return Response(content=res.content, media_type="application/octet-stream") - return schemas.Response(success=False) - - -@router.post("/rename", summary="重命名文件或目录(115网盘)", response_model=schemas.Response) -def rename_115(fileitem: schemas.FileItem, - new_name: str, - recursive: bool = False, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 重命名文件或目录 - """ - if not fileitem.fileid or not new_name: - return schemas.Response(success=False) - result = U115Helper().rename(fileitem.fileid, new_name) - if result: - if recursive: - transferchain = TransferChain() - media_exts = settings.RMT_MEDIAEXT + settings.RMT_SUBEXT + settings.RMT_AUDIO_TRACK_EXT - # 递归修改目录内文件(智能识别命名) - sub_files: List[schemas.FileItem] = list_115(fileitem) - if sub_files: - # 开始进度 - progress = ProgressHelper() - progress.start(ProgressKey.BatchRename) - total = len(sub_files) - handled = 0 - for sub_file in sub_files: - handled += 1 - progress.update(value=handled / total * 100, - text=f"正在处理 {sub_file.name} ...", - key=ProgressKey.BatchRename) - if sub_file.type == "dir": - continue - if not sub_file.extension: - continue - if f".{sub_file.extension.lower()}" not in media_exts: - continue - sub_path = Path(f"{fileitem.path}{sub_file.name}") - meta = MetaInfoPath(sub_path) - mediainfo = transferchain.recognize_media(meta) - if not mediainfo: - progress.end(ProgressKey.BatchRename) - return schemas.Response(success=False, message=f"{sub_path.name} 未识别到媒体信息") - new_path = transferchain.recommend_name(meta=meta, mediainfo=mediainfo) - if not new_path: - progress.end(ProgressKey.BatchRename) - return schemas.Response(success=False, message=f"{sub_path.name} 未识别到新名称") - ret: schemas.Response = rename_115(fileitem=sub_file, - new_name=Path(new_path).name, - recursive=False) - if not ret.success: - progress.end(ProgressKey.BatchRename) - return schemas.Response(success=False, message=f"{sub_path.name} 重命名失败!") - progress.end(ProgressKey.BatchRename) - return schemas.Response(success=True) - return schemas.Response(success=False) - - -@router.get("/image", summary="读取图片(115网盘)") -def image_115(pickcode: str, _: schemas.TokenPayload = Depends(verify_uri_token)) -> Any: - """ - 读取图片 - """ - if not pickcode: - return schemas.Response(success=False) - ticket = U115Helper().download(pickcode) - if ticket: - # 请求数据,获取内容编码为图片base64返回 - res = RequestUtils(headers=ticket.headers).get_res(ticket.url) - if res: - content_type = res.headers.get("Content-Type") - return Response(content=res.content, media_type=content_type) - raise HTTPException(status_code=500, detail="下载图片出错") diff --git a/app/chain/media.py b/app/chain/media.py index d2e18fdd..efe47155 100644 --- a/app/chain/media.py +++ b/app/chain/media.py @@ -6,19 +6,17 @@ from typing import Optional, List, Tuple, Union from app import schemas from app.chain import ChainBase +from app.chain.storage import StorageChain from app.core.config import settings from app.core.context import Context, MediaInfo from app.core.event import eventmanager, Event from app.core.meta import MetaBase from app.core.metainfo import MetaInfo, MetaInfoPath -from app.modules.filetransfer.storage.alipan import AliyunHelper -from app.modules.filetransfer.storage.u115 import U115Helper from app.log import logger from app.schemas.types import EventType, MediaType from app.utils.http import RequestUtils from app.utils.singleton import Singleton from app.utils.string import StringUtils -from app.utils.system import SystemUtils recognize_lock = Lock() @@ -339,46 +337,19 @@ class MediaChain(ChainBase, metaclass=Singleton): 手动刮削媒体信息 """ - def __list_files(_storage: str, _fileid: str, _path: str = None, _drive_id: str = None): + def __list_files(_fileitem: schemas.FileItem): """ 列出下级文件 """ - if _storage == "aliyun": - return AliyunHelper().list(drive_id=_drive_id, parent_file_id=_fileid, path=_path) - elif _storage == "u115": - return U115Helper().list(parent_file_id=_fileid, path=_path) - else: - items = SystemUtils.list_sub_all(Path(_path)) - return [schemas.FileItem( - type="file" if item.is_file() else "dir", - path=str(item), - name=item.name, - basename=item.stem, - extension=item.suffix[1:], - size=item.stat().st_size, - modify_time=item.stat().st_mtime - ) for item in items] + return StorageChain().list_files(fileitem=_fileitem) - def __save_file(_storage: str, _drive_id: str, _fileid: str, _path: Path, _content: Union[bytes, str]): + def __save_file(_fileitem: schemas.FileItem, _path: Path, _content: Union[bytes, str]): """ 保存或上传文件 """ - if _storage != "local": - # 写入到临时目录 - temp_path = settings.TEMP_PATH / _path.name - temp_path.write_bytes(_content) - # 上传文件 - logger.info(f"正在上传 {_path.name} ...") - if _storage == "aliyun": - AliyunHelper().upload(drive_id=_drive_id, parent_file_id=_fileid, file_path=temp_path) - elif _storage == "u115": - U115Helper().upload(parent_file_id=_fileid, file_path=temp_path) - logger.info(f"{_path.name} 上传完成") - else: - # 保存到本地 - logger.info(f"正在保存 {_path.name} ...") - _path.write_bytes(_content) - logger.info(f"{_path} 已保存") + tmp_file = settings.TEMP_PATH / _path.name + tmp_file.write_bytes(_content) + StorageChain().upload_file(fileitem=_fileitem, path=tmp_file) def __save_image(_url: str) -> Optional[bytes]: """ @@ -417,12 +388,10 @@ class MediaChain(ChainBase, metaclass=Singleton): logger.warn(f"{filepath.name} nfo文件生成失败!") return # 保存或上传nfo文件 - __save_file(_storage=storage, _drive_id=fileitem.drive_id, _fileid=fileitem.parent_fileid, - _path=filepath.with_suffix(".nfo"), _content=movie_nfo) + __save_file(_fileitem=fileitem, _path=filepath.with_suffix(".nfo"), _content=movie_nfo) else: # 电影目录 - files = __list_files(_storage=storage, _fileid=fileitem.fileid, - _drive_id=fileitem.drive_id, _path=fileitem.path) + files = __list_files(_fileitem=fileitem) for file in files: self.manual_scrape(storage=storage, fileitem=file, meta=meta, mediainfo=mediainfo, @@ -441,8 +410,7 @@ class MediaChain(ChainBase, metaclass=Singleton): # 下载图片 content = __save_image(_url=attr_value) # 写入nfo到根目录 - __save_file(_storage=storage, _drive_id=fileitem.drive_id, _fileid=fileitem.fileid, - _path=image_path, _content=content) + __save_file(_fileitem=fileitem, _path=image_path, _content=content) else: # 电视剧 if fileitem.type == "file": @@ -462,12 +430,10 @@ class MediaChain(ChainBase, metaclass=Singleton): logger.warn(f"{filepath.name} nfo生成失败!") return # 保存或上传nfo文件 - __save_file(_storage=storage, _drive_id=fileitem.drive_id, _fileid=fileitem.parent_fileid, - _path=filepath.with_suffix(".nfo"), _content=episode_nfo) + __save_file(_fileitem=fileitem, _path=filepath.with_suffix(".nfo"), _content=episode_nfo) else: # 当前为目录,处理目录内的文件 - files = __list_files(_storage=storage, _fileid=fileitem.fileid, - _drive_id=fileitem.drive_id, _path=fileitem.path) + files = __list_files(_fileitem=fileitem) for file in files: self.manual_scrape(storage=storage, fileitem=file, meta=meta, mediainfo=mediainfo, @@ -484,8 +450,7 @@ class MediaChain(ChainBase, metaclass=Singleton): return # 写入nfo到根目录 nfo_path = filepath / "season.nfo" - __save_file(_storage=storage, _drive_id=fileitem.drive_id, _fileid=fileitem.fileid, - _path=nfo_path, _content=season_nfo) + __save_file(_fileitem=fileitem, _path=nfo_path, _content=season_nfo) # TMDB季poster图片 image_dict = self.metadata_img(mediainfo=mediainfo, season=season_meta.begin_season) if image_dict: @@ -494,8 +459,7 @@ class MediaChain(ChainBase, metaclass=Singleton): # 下载图片 content = __save_image(image_url) # 保存图片文件到当前目录 - __save_file(_storage=storage, _drive_id=fileitem.drive_id, _fileid=fileitem.fileid, - _path=image_path, _content=content) + __save_file(_fileitem=fileitem, _path=image_path, _content=content) if season_meta.name: # 当前目录有名称,生成tvshow nfo 和 tv图片 tv_nfo = self.metadata_nfo(meta=meta, mediainfo=mediainfo) @@ -504,8 +468,7 @@ class MediaChain(ChainBase, metaclass=Singleton): return # 写入tvshow nfo到根目录 nfo_path = filepath / "tvshow.nfo" - __save_file(_storage=storage, _drive_id=fileitem.drive_id, _fileid=fileitem.fileid, - _path=nfo_path, _content=tv_nfo) + __save_file(_fileitem=fileitem, _path=nfo_path, _content=tv_nfo) # 生成目录图片 image_dict = self.metadata_img(mediainfo=mediainfo) if image_dict: @@ -514,7 +477,6 @@ class MediaChain(ChainBase, metaclass=Singleton): # 下载图片 content = __save_image(image_url) # 保存图片文件到当前目录 - __save_file(_storage=storage, _drive_id=fileitem.drive_id, _fileid=fileitem.fileid, - _path=image_path, _content=content) + __save_file(_fileitem=fileitem, _path=image_path, _content=content) logger.info(f"{filepath.name} 刮削完成") diff --git a/app/chain/storage.py b/app/chain/storage.py new file mode 100644 index 00000000..3887b634 --- /dev/null +++ b/app/chain/storage.py @@ -0,0 +1,59 @@ +from pathlib import Path +from typing import Optional, Tuple, List + +from app import schemas +from app.chain import ChainBase + + +class StorageChain(ChainBase): + """ + 存储处理链 + """ + + def generate_qrcode(self) -> Optional[Tuple[dict, str]]: + """ + 生成二维码 + """ + return self.run_module("generate_qrcode",) + + def check_login(self) -> Optional[Tuple[dict, str]]: + """ + 登录确认 + """ + return self.run_module("check_login",) + + def list_files(self, fileitem: schemas.FileItem) -> Optional[List[schemas.FileItem]]: + """ + 查询当前目录下所有目录和文件 + """ + return self.run_module("list_files", fileitem=fileitem) + + def create_folder(self, fileitem: schemas.FileItem, name: str) -> Optional[schemas.FileItem]: + """ + 创建目录 + """ + return self.run_module("create_folder", fileitem=fileitem, name=name) + + def download_file(self, fileitem: schemas.FileItem, path: str) -> Optional[bool]: + """ + 下载文件 + """ + return self.run_module("download_file", fileitem=fileitem, path=path) + + def upload_file(self, fileitem: schemas.FileItem, path: Path) -> Optional[bool]: + """ + 上传文件 + """ + return self.run_module("upload_file", fileitem=fileitem, path=path) + + def delete_file(self, fileitem: schemas.FileItem) -> Optional[bool]: + """ + 删除文件或目录 + """ + return self.run_module("delete_file", fileitem=fileitem) + + def rename_file(self, fileitem: schemas.FileItem, name: str) -> Optional[bool]: + """ + 重命名文件或目录 + """ + return self.run_module("rename_file", fileitem=fileitem, name=name) diff --git a/app/modules/filetransfer/__init__.py b/app/modules/filemanager/__init__.py similarity index 94% rename from app/modules/filetransfer/__init__.py rename to app/modules/filemanager/__init__.py index 347546aa..d43cab28 100644 --- a/app/modules/filetransfer/__init__.py +++ b/app/modules/filemanager/__init__.py @@ -107,6 +107,71 @@ class FileTransferModule(_ModuleBase): ) return str(path) + def list_files(self, fileitem: FileItem) -> Optional[List[FileItem]]: + """ + 浏览文件 + :param fileitem: 源文件 + :return: 文件列表 + """ + storage_oper = self.__get_storage_oper(fileitem.storage) + if not storage_oper: + logger.error(f"不支持 {fileitem.storage} 的文件浏览") + return None + return storage_oper.list(fileitem) + + def create_folder(self, fileitem: FileItem, name: str) -> Optional[FileItem]: + """ + 创建目录 + :param fileitem: 源文件 + :param name: 目录名 + :return: 创建的目录 + """ + storage_oper = self.__get_storage_oper(fileitem.storage) + if not storage_oper: + logger.error(f"不支持 {fileitem.storage} 的目录创建") + return None + return storage_oper.create_folder(fileitem, name) + + def delete_file(self, fileitem: FileItem) -> bool: + """ + 删除文件或目录 + """ + storage_oper = self.__get_storage_oper(fileitem.storage) + if not storage_oper: + logger.error(f"不支持 {fileitem.storage} 的删除处理") + return False + return storage_oper.delete(fileitem) + + def rename_file(self, fileitem: FileItem, name: str) -> bool: + """ + 重命名文件或目录 + """ + storage_oper = self.__get_storage_oper(fileitem.storage) + if not storage_oper: + logger.error(f"不支持 {fileitem.storage} 的重命名处理") + return False + return storage_oper.rename(fileitem, name) + + def download_file(self, fileitem: FileItem, path: Path) -> bool: + """ + 下载文件 + """ + storage_oper = self.__get_storage_oper(fileitem.storage) + if not storage_oper: + logger.error(f"不支持 {fileitem.storage} 的下载处理") + return False + return storage_oper.download(fileitem, path) + + def upload_file(self, fileitem: FileItem, path: Path) -> bool: + """ + 上传文件 + """ + storage_oper = self.__get_storage_oper(fileitem.storage) + if not storage_oper: + logger.error(f"不支持 {fileitem.storage} 的上传处理") + return False + return storage_oper.upload(fileitem, path) + def transfer(self, fileitem: FileItem, meta: MetaBase, mediainfo: MediaInfo, transfer_type: str, target_storage: str = None, target_path: Path = None, episodes_info: List[TmdbEpisode] = None, diff --git a/app/modules/filetransfer/storage/__init__.py b/app/modules/filemanager/storage/__init__.py similarity index 100% rename from app/modules/filetransfer/storage/__init__.py rename to app/modules/filemanager/storage/__init__.py diff --git a/app/modules/filetransfer/storage/alipan.py b/app/modules/filemanager/storage/alipan.py similarity index 99% rename from app/modules/filetransfer/storage/alipan.py rename to app/modules/filemanager/storage/alipan.py index f4989cfd..b13b2098 100644 --- a/app/modules/filetransfer/storage/alipan.py +++ b/app/modules/filemanager/storage/alipan.py @@ -11,7 +11,7 @@ from app import schemas from app.core.config import settings from app.db.systemconfig_oper import SystemConfigOper from app.log import logger -from app.modules.filetransfer.storage import StorageBase +from app.modules.filemanager.storage import StorageBase from app.schemas.types import SystemConfigKey, StorageSchema from app.utils.http import RequestUtils from app.utils.string import StringUtils diff --git a/app/modules/filetransfer/storage/local.py b/app/modules/filemanager/storage/local.py similarity index 99% rename from app/modules/filetransfer/storage/local.py rename to app/modules/filemanager/storage/local.py index 26ba64e5..5c377d60 100644 --- a/app/modules/filetransfer/storage/local.py +++ b/app/modules/filemanager/storage/local.py @@ -4,7 +4,7 @@ from typing import Optional, List from app import schemas from app.log import logger -from app.modules.filetransfer.storage import StorageBase +from app.modules.filemanager.storage import StorageBase from app.schemas.types import StorageSchema from app.utils.system import SystemUtils diff --git a/app/modules/filetransfer/storage/rclone.py b/app/modules/filemanager/storage/rclone.py similarity index 92% rename from app/modules/filetransfer/storage/rclone.py rename to app/modules/filemanager/storage/rclone.py index f419276d..c288d8a2 100644 --- a/app/modules/filetransfer/storage/rclone.py +++ b/app/modules/filemanager/storage/rclone.py @@ -4,7 +4,7 @@ from typing import Optional, List from app import schemas from app.log import logger -from app.modules.filetransfer.storage import StorageBase +from app.modules.filemanager.storage import StorageBase from app.schemas.types import StorageSchema from app.utils.system import SystemUtils @@ -64,14 +64,14 @@ class Rclone(StorageBase): def move(self, fileitm: schemas.FileItem, target_file: schemas.FileItem) -> bool: """ - 移动文件 + 移动文件,target_file格式:rclone:path """ try: retcode = subprocess.run( [ 'rclone', 'moveto', fileitm.path, - f'MP:{target_file}' + f'{target_file}' ], startupinfo=self.__get_hidden_shell() ).returncode @@ -83,14 +83,14 @@ class Rclone(StorageBase): def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool: """ - 复制文件 + 复制文件,target_file格式:rclone:path """ try: retcode = subprocess.run( [ 'rclone', 'copyto', fileitm.path, - f'MP:{target_file}' + f'{target_file}' ], startupinfo=self.__get_hidden_shell() ).returncode diff --git a/app/modules/filetransfer/storage/u115.py b/app/modules/filemanager/storage/u115.py similarity index 97% rename from app/modules/filetransfer/storage/u115.py rename to app/modules/filemanager/storage/u115.py index 071a4083..4abdb230 100644 --- a/app/modules/filetransfer/storage/u115.py +++ b/app/modules/filemanager/storage/u115.py @@ -10,7 +10,7 @@ from py115.types import LoginTarget, QrcodeSession, QrcodeStatus, Credential from app import schemas from app.db.systemconfig_oper import SystemConfigOper from app.log import logger -from app.modules.filetransfer.storage import StorageBase +from app.modules.filemanager.storage import StorageBase from app.schemas.types import SystemConfigKey, StorageSchema from app.utils.http import RequestUtils from app.utils.singleton import Singleton @@ -73,7 +73,7 @@ class U115Pan(StorageBase, metaclass=Singleton): """ self.systemconfig.delete(SystemConfigKey.User115Params) - def generate_qrcode(self) -> Optional[str]: + def generate_qrcode(self) -> Optional[Tuple[dict, str]]: """ 生成二维码 """ @@ -86,10 +86,12 @@ class U115Pan(StorageBase, metaclass=Singleton): return None # 转换为base64图片格式 image_base64 = base64.b64encode(image_bin).decode() - return f"data:image/png;base64,{image_base64}" + return { + "codeContent": f"data:image/jpeg;base64,{image_base64}" + }, "" except Exception as e: logger.warn(f"115生成二维码失败:{str(e)}") - return None + return {}, f"生成二维码失败:{str(e)}" def check_login(self) -> Optional[Tuple[dict, str]]: """