mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
refactor: 推进后端分层架构治理
This commit is contained in:
@@ -4,7 +4,9 @@ from typing import Optional, List, Dict, Tuple, Callable, Union
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from app import schemas
|
||||
from app.schemas.file import StorageUsage as _SchemaStorageUsage
|
||||
from app.schemas.system import StorageConf as _SchemaStorageConf
|
||||
from app.schemas.workflow import FileItem as _SchemaFileItem
|
||||
from app.runtime.progress import ProgressHelper
|
||||
from app.application.storage import StorageHelper
|
||||
from app.runtime.log import logger
|
||||
@@ -69,7 +71,7 @@ class StorageBase(metaclass=ABCMeta):
|
||||
"""检查存储登录状态"""
|
||||
pass
|
||||
|
||||
def get_config(self) -> Optional[schemas.StorageConf]:
|
||||
def get_config(self) -> Optional[_SchemaStorageConf]:
|
||||
"""
|
||||
获取配置
|
||||
"""
|
||||
@@ -122,7 +124,7 @@ class StorageBase(metaclass=ABCMeta):
|
||||
return safe_name
|
||||
|
||||
def _build_download_path(
|
||||
self, fileitem: schemas.FileItem, path: Path
|
||||
self, fileitem: _SchemaFileItem, path: Path
|
||||
) -> Optional[Path]:
|
||||
"""
|
||||
构造本地下载路径,避免远端文件名携带目录片段时越过目标目录。
|
||||
@@ -148,14 +150,14 @@ class StorageBase(metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def list(self, fileitem: schemas.FileItem) -> List[schemas.FileItem]:
|
||||
def list(self, fileitem: _SchemaFileItem) -> List[_SchemaFileItem]:
|
||||
"""
|
||||
浏览文件
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create_folder(self, fileitem: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
|
||||
def create_folder(self, fileitem: _SchemaFileItem, name: str) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
创建目录
|
||||
:param fileitem: 父目录
|
||||
@@ -164,20 +166,20 @@ class StorageBase(metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_folder(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取目录,如目录不存在则创建
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_item(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_item(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件或目录,不存在返回None
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_item_strict(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_item_strict(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件或目录,确认不存在返回None;无法确认状态时抛出 StorageQueryError。
|
||||
|
||||
@@ -188,28 +190,28 @@ class StorageBase(metaclass=ABCMeta):
|
||||
"""
|
||||
raise StorageQueryError(f"存储 {self.schema} 未实现严格查询,无法确认目标状态: {path}")
|
||||
|
||||
def get_parent(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||
def get_parent(self, fileitem: _SchemaFileItem) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取父目录
|
||||
"""
|
||||
return self.get_item(Path(fileitem.path).parent)
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, fileitem: schemas.FileItem) -> bool:
|
||||
def delete(self, fileitem: _SchemaFileItem) -> bool:
|
||||
"""
|
||||
删除文件
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def rename(self, fileitem: schemas.FileItem, name: str) -> bool:
|
||||
def rename(self, fileitem: _SchemaFileItem, name: str) -> bool:
|
||||
"""
|
||||
重命名文件
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Path:
|
||||
def download(self, fileitem: _SchemaFileItem, path: Path = None) -> Path:
|
||||
"""
|
||||
下载文件,保存到本地,返回本地临时文件地址
|
||||
:param fileitem: 文件项
|
||||
@@ -218,8 +220,8 @@ class StorageBase(metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def upload(self, fileitem: schemas.FileItem, path: Path,
|
||||
new_name: Optional[str] = None) -> Optional[schemas.FileItem]:
|
||||
def upload(self, fileitem: _SchemaFileItem, path: Path,
|
||||
new_name: Optional[str] = None) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
上传文件
|
||||
:param fileitem: 上传目录项
|
||||
@@ -229,14 +231,14 @@ class StorageBase(metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||
def detail(self, fileitem: _SchemaFileItem) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件详情
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def copy(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
|
||||
def copy(self, fileitem: _SchemaFileItem, path: Path, new_name: str) -> bool:
|
||||
"""
|
||||
复制文件
|
||||
:param fileitem: 文件项
|
||||
@@ -246,7 +248,7 @@ class StorageBase(metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def move(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
|
||||
def move(self, fileitem: _SchemaFileItem, path: Path, new_name: str) -> bool:
|
||||
"""
|
||||
移动文件
|
||||
:param fileitem: 文件项
|
||||
@@ -256,21 +258,21 @@ class StorageBase(metaclass=ABCMeta):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def link(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
"""
|
||||
硬链接文件
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def softlink(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
"""
|
||||
软链接文件
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def usage(self) -> Optional[schemas.StorageUsage]:
|
||||
def usage(self) -> Optional[_SchemaStorageUsage]:
|
||||
"""
|
||||
存储使用情况
|
||||
"""
|
||||
@@ -292,8 +294,8 @@ class StorageBase(metaclass=ABCMeta):
|
||||
if PurePosixPath(file_path).is_relative_to(root_path)
|
||||
}
|
||||
|
||||
def __remove_deleted_children(_fileitm: schemas.FileItem,
|
||||
sub_files: List[schemas.FileItem]) -> None:
|
||||
def __remove_deleted_children(_fileitm: _SchemaFileItem,
|
||||
sub_files: List[_SchemaFileItem]) -> None:
|
||||
"""
|
||||
清理已确认遍历目录中不再存在的直接子项。
|
||||
未变化的子目录仍保留旧基线,避免增量遍历将其误删。
|
||||
@@ -311,7 +313,7 @@ class StorageBase(metaclass=ABCMeta):
|
||||
if direct_child_path not in child_paths:
|
||||
files_info.pop(old_file_path, None)
|
||||
|
||||
def __snapshot_file(_fileitm: schemas.FileItem, current_depth: int = 0):
|
||||
def __snapshot_file(_fileitm: _SchemaFileItem, current_depth: int = 0):
|
||||
"""
|
||||
递归获取文件信息
|
||||
"""
|
||||
|
||||
@@ -8,7 +8,8 @@ from typing import List, Optional, Tuple, Union
|
||||
|
||||
import requests
|
||||
|
||||
from app import schemas
|
||||
from app.schemas.file import StorageUsage as _SchemaStorageUsage
|
||||
from app.schemas.workflow import FileItem as _SchemaFileItem
|
||||
from app.runtime.config import settings, global_vars
|
||||
from app.runtime.log import logger
|
||||
from app.modules.filemanager import StorageBase
|
||||
@@ -287,16 +288,16 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
return ret_data.get(result_key)
|
||||
return ret_data
|
||||
|
||||
def __get_fileitem(self, fileinfo: dict, parent: str = "/") -> schemas.FileItem:
|
||||
def __get_fileitem(self, fileinfo: dict, parent: str = "/") -> _SchemaFileItem:
|
||||
"""
|
||||
获取文件信息
|
||||
"""
|
||||
if not fileinfo:
|
||||
return schemas.FileItem()
|
||||
return _SchemaFileItem()
|
||||
if not parent.endswith("/"):
|
||||
parent += "/"
|
||||
if fileinfo.get("type") == "folder":
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
fileid=fileinfo.get("file_id"),
|
||||
parent_fileid=fileinfo.get("parent_file_id"),
|
||||
@@ -309,7 +310,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
drive_id=fileinfo.get("drive_id"),
|
||||
)
|
||||
else:
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
fileid=fileinfo.get("file_id"),
|
||||
parent_fileid=fileinfo.get("parent_file_id"),
|
||||
@@ -343,7 +344,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
def init_storage(self):
|
||||
pass
|
||||
|
||||
def list(self, fileitem: schemas.FileItem) -> List[schemas.FileItem]:
|
||||
def list(self, fileitem: _SchemaFileItem) -> List[_SchemaFileItem]:
|
||||
"""
|
||||
目录遍历实现
|
||||
"""
|
||||
@@ -386,7 +387,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
break
|
||||
return items
|
||||
|
||||
def _delay_get_item(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def _delay_get_item(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
自动延迟重试 get_item 模块
|
||||
"""
|
||||
@@ -398,8 +399,8 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
return None
|
||||
|
||||
def create_folder(
|
||||
self, parent_item: schemas.FileItem, name: str
|
||||
) -> Optional[schemas.FileItem]:
|
||||
self, parent_item: _SchemaFileItem, name: str
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
创建目录
|
||||
"""
|
||||
@@ -588,10 +589,10 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
|
||||
def upload(
|
||||
self,
|
||||
target_dir: schemas.FileItem,
|
||||
target_dir: _SchemaFileItem,
|
||||
local_path: Path,
|
||||
new_name: Optional[str] = None,
|
||||
) -> Optional[schemas.FileItem]:
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
文件上传:分片、支持秒传
|
||||
"""
|
||||
@@ -721,7 +722,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
)
|
||||
return self.__get_fileitem(result, parent=target_dir.path)
|
||||
|
||||
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]:
|
||||
def download(self, fileitem: _SchemaFileItem, path: Path = None) -> Optional[Path]:
|
||||
"""
|
||||
带实时进度显示的下载
|
||||
"""
|
||||
@@ -801,7 +802,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
def check(self) -> bool:
|
||||
return self.access_token is not None
|
||||
|
||||
def delete(self, fileitem: schemas.FileItem) -> bool:
|
||||
def delete(self, fileitem: _SchemaFileItem) -> bool:
|
||||
"""
|
||||
删除文件/目录
|
||||
"""
|
||||
@@ -815,7 +816,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
except requests.exceptions.HTTPError:
|
||||
return False
|
||||
|
||||
def rename(self, fileitem: schemas.FileItem, name: str) -> bool:
|
||||
def rename(self, fileitem: _SchemaFileItem, name: str) -> bool:
|
||||
"""
|
||||
重命名文件/目录
|
||||
"""
|
||||
@@ -835,7 +836,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __get_by_path_item(self, path: Path, drive_id: str = None) -> Optional[schemas.FileItem]:
|
||||
def __get_by_path_item(self, path: Path, drive_id: str = None) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
按路径查询文件/目录项,无法确认状态时抛出 StorageQueryError。
|
||||
NotFound 系列错误码表示确认不存在,其余错误(网络失败、限流、
|
||||
@@ -861,7 +862,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
f"【阿里云盘】查询文件信息出错: {path} - {code} {resp.get('message')}")
|
||||
return self.__get_fileitem(resp, parent=str(path.parent))
|
||||
|
||||
def get_item(self, path: Path, drive_id: str = None) -> Optional[schemas.FileItem]:
|
||||
def get_item(self, path: Path, drive_id: str = None) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取指定路径的文件/目录项
|
||||
"""
|
||||
@@ -871,7 +872,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
logger.debug(f"【阿里云盘】获取文件信息失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_item_strict(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_item_strict(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取指定路径的文件/目录项,无法确认状态时抛出 StorageQueryError。
|
||||
"""
|
||||
@@ -882,14 +883,14 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
except Exception as e:
|
||||
raise StorageQueryError(f"【阿里云盘】查询文件信息失败: {path} - {e}") from e
|
||||
|
||||
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_folder(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取指定路径的文件夹,如不存在则创建
|
||||
"""
|
||||
|
||||
def __find_dir(
|
||||
_fileitem: schemas.FileItem, _name: str
|
||||
) -> Optional[schemas.FileItem]:
|
||||
_fileitem: _SchemaFileItem, _name: str
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
查找下级目录中匹配名称的目录
|
||||
"""
|
||||
@@ -905,7 +906,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
if folder:
|
||||
return folder
|
||||
# 逐级查找和创建目录
|
||||
fileitem = schemas.FileItem(
|
||||
fileitem = _SchemaFileItem(
|
||||
storage=self.schema.value, path="/", drive_id=self._default_drive_id
|
||||
)
|
||||
for part in path.parts[1:]:
|
||||
@@ -920,13 +921,13 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
fileitem = dir_file
|
||||
return fileitem
|
||||
|
||||
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||
def detail(self, fileitem: _SchemaFileItem) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件/目录详细信息
|
||||
"""
|
||||
return self.get_item(Path(fileitem.path))
|
||||
|
||||
def copy(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
|
||||
def copy(self, fileitem: _SchemaFileItem, path: Path, new_name: str) -> bool:
|
||||
"""
|
||||
复制文件到指定路径
|
||||
:param fileitem: 要复制的文件项
|
||||
@@ -958,7 +959,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
self.rename(new_file, new_name)
|
||||
return True
|
||||
|
||||
def move(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
|
||||
def move(self, fileitem: _SchemaFileItem, path: Path, new_name: str) -> bool:
|
||||
"""
|
||||
移动文件到指定路径
|
||||
:param fileitem: 要移动的文件项
|
||||
@@ -988,13 +989,13 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
return False
|
||||
return True
|
||||
|
||||
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def link(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
pass
|
||||
|
||||
def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def softlink(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
pass
|
||||
|
||||
def usage(self) -> Optional[schemas.StorageUsage]:
|
||||
def usage(self) -> Optional[_SchemaStorageUsage]:
|
||||
"""
|
||||
获取带有企业级配额信息的存储使用情况
|
||||
"""
|
||||
@@ -1005,7 +1006,7 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
space = resp.get("personal_space_info") or {}
|
||||
total_size = space.get("total_size") or 0
|
||||
used_size = space.get("used_size") or 0
|
||||
return schemas.StorageUsage(
|
||||
return _SchemaStorageUsage(
|
||||
total=total_size, available=total_size - used_size
|
||||
)
|
||||
except NoCheckInException:
|
||||
|
||||
@@ -5,7 +5,8 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
|
||||
from app import schemas
|
||||
from app.schemas.file import StorageUsage as _SchemaStorageUsage
|
||||
from app.schemas.workflow import FileItem as _SchemaFileItem
|
||||
from app.runtime.cache import cached
|
||||
from app.runtime.config import settings, global_vars
|
||||
from app.runtime.log import logger
|
||||
@@ -54,7 +55,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
|
||||
def _delay_get_item(
|
||||
self, path: Path, /, refresh: bool = False
|
||||
) -> Optional[schemas.FileItem]:
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
自动延迟重试 get_item 模块
|
||||
|
||||
@@ -70,8 +71,8 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
return None
|
||||
|
||||
def __build_transfer_item(
|
||||
self, source_item: schemas.FileItem, target_path: Path
|
||||
) -> schemas.FileItem:
|
||||
self, source_item: _SchemaFileItem, target_path: Path
|
||||
) -> _SchemaFileItem:
|
||||
"""
|
||||
根据目标路径构造文件项,用于 OpenList 操作成功但元数据短时间不可见的场景。
|
||||
目录项路径需要遵循 FileItem 以斜杠结尾的约定。
|
||||
@@ -80,7 +81,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
if source_item.type == "dir" and not target_path_str.endswith("/"):
|
||||
target_path_str = f"{target_path_str}/"
|
||||
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type=source_item.type,
|
||||
path=target_path_str,
|
||||
@@ -197,12 +198,12 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
|
||||
def list(
|
||||
self,
|
||||
fileitem: schemas.FileItem,
|
||||
fileitem: _SchemaFileItem,
|
||||
password: Optional[str] = "",
|
||||
page: int = 1,
|
||||
per_page: int = 0,
|
||||
refresh: bool = False,
|
||||
) -> List[schemas.FileItem]:
|
||||
) -> List[_SchemaFileItem]:
|
||||
"""
|
||||
浏览文件
|
||||
:param fileitem: 文件项
|
||||
@@ -291,7 +292,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
page_content = page_data.get("content") or []
|
||||
items.extend(
|
||||
[
|
||||
schemas.FileItem(
|
||||
_SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir" if item["is_dir"] else "file",
|
||||
path=(Path(fileitem.path) / item["name"]).as_posix()
|
||||
@@ -321,8 +322,8 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
current_page += 1
|
||||
|
||||
def create_folder(
|
||||
self, fileitem: schemas.FileItem, name: str
|
||||
) -> Optional[schemas.FileItem]:
|
||||
self, fileitem: _SchemaFileItem, name: str
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
创建目录
|
||||
:param fileitem: 父目录
|
||||
@@ -364,7 +365,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
return self._delay_get_item(
|
||||
path, refresh=True
|
||||
) or self.__build_transfer_item(
|
||||
schemas.FileItem(
|
||||
_SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir",
|
||||
path=fileitem.path,
|
||||
@@ -374,7 +375,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
path,
|
||||
)
|
||||
|
||||
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_folder(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取目录,如目录不存在则创建
|
||||
|
||||
@@ -386,7 +387,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
return folder
|
||||
if not folder:
|
||||
folder = self.create_folder(
|
||||
schemas.FileItem(
|
||||
_SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir",
|
||||
path=path.parent.as_posix(),
|
||||
@@ -404,7 +405,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
page: int = 1,
|
||||
per_page: int = 0,
|
||||
refresh: bool = False,
|
||||
) -> Optional[schemas.FileItem]:
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件或目录,不存在返回None
|
||||
:param path: 文件路径
|
||||
@@ -473,14 +474,14 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
|
||||
return self.__build_fileitem(path, result["data"])
|
||||
|
||||
def __build_fileitem(self, path: Path, data: dict) -> schemas.FileItem:
|
||||
def __build_fileitem(self, path: Path, data: dict) -> _SchemaFileItem:
|
||||
"""
|
||||
根据接口返回数据构建文件项。
|
||||
:param path: 文件路径
|
||||
:param data: 接口返回的 data 字段
|
||||
:return: 文件项
|
||||
"""
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir" if data["is_dir"] else "file",
|
||||
path=path.as_posix() + ("/" if data["is_dir"] else ""),
|
||||
@@ -492,7 +493,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
thumbnail=data["thumb"],
|
||||
)
|
||||
|
||||
def get_item_strict(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_item_strict(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件或目录,确认不存在返回None;无法确认状态时抛出 StorageQueryError。
|
||||
只有接口明确回报「对象不存在」才是确定结果,连接失败、HTTP 异常与其他
|
||||
@@ -523,7 +524,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
raise StorageQueryError(f"【OpenList】查询文件 {path} 失败:{message}")
|
||||
return self.__build_fileitem(path, result["data"])
|
||||
|
||||
def get_parent(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||
def get_parent(self, fileitem: _SchemaFileItem) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取父目录
|
||||
|
||||
@@ -532,7 +533,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
"""
|
||||
return self.get_folder(Path(fileitem.path).parent)
|
||||
|
||||
def delete(self, fileitem: schemas.FileItem) -> bool:
|
||||
def delete(self, fileitem: _SchemaFileItem) -> bool:
|
||||
"""
|
||||
删除文件或目录
|
||||
|
||||
@@ -570,7 +571,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
return False
|
||||
return True
|
||||
|
||||
def rename(self, fileitem: schemas.FileItem, name: str) -> bool:
|
||||
def rename(self, fileitem: _SchemaFileItem, name: str) -> bool:
|
||||
"""
|
||||
重命名文件
|
||||
|
||||
@@ -619,7 +620,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
|
||||
def download(
|
||||
self,
|
||||
fileitem: schemas.FileItem,
|
||||
fileitem: _SchemaFileItem,
|
||||
path: Path = None,
|
||||
password: Optional[str] = "",
|
||||
) -> Optional[Path]:
|
||||
@@ -712,11 +713,11 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
|
||||
def upload(
|
||||
self,
|
||||
fileitem: schemas.FileItem,
|
||||
fileitem: _SchemaFileItem,
|
||||
path: Path,
|
||||
new_name: Optional[str] = None,
|
||||
task: bool = False,
|
||||
) -> Optional[schemas.FileItem]:
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
上传文件(带进度)
|
||||
:param fileitem: 上传目录项
|
||||
@@ -830,13 +831,13 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
"X-File-Sha256": sha256_hash.hexdigest(),
|
||||
}
|
||||
|
||||
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||
def detail(self, fileitem: _SchemaFileItem) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件详情
|
||||
"""
|
||||
return self.get_item(Path(fileitem.path))
|
||||
|
||||
def copy(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
|
||||
def copy(self, fileitem: _SchemaFileItem, path: Path, new_name: str) -> bool:
|
||||
"""
|
||||
复制文件
|
||||
:param fileitem: 文件项
|
||||
@@ -892,8 +893,8 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
return True
|
||||
|
||||
def copy_item(
|
||||
self, fileitem: schemas.FileItem, path: Path, new_name: str
|
||||
) -> Optional[schemas.FileItem]:
|
||||
self, fileitem: _SchemaFileItem, path: Path, new_name: str
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
复制文件并返回目标文件项,兼容 OpenList 成功响应不携带目标对象的格式。
|
||||
"""
|
||||
@@ -913,7 +914,7 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
) or self.__build_transfer_item(fileitem, target_path)
|
||||
return None
|
||||
|
||||
def move(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
|
||||
def move(self, fileitem: _SchemaFileItem, path: Path, new_name: str) -> bool:
|
||||
"""
|
||||
移动文件
|
||||
:param fileitem: 文件项
|
||||
@@ -967,8 +968,8 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
return True
|
||||
|
||||
def move_item(
|
||||
self, fileitem: schemas.FileItem, path: Path, new_name: str
|
||||
) -> Optional[schemas.FileItem]:
|
||||
self, fileitem: _SchemaFileItem, path: Path, new_name: str
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
移动文件并返回目标文件项,兼容 OpenList 成功响应不携带目标对象的格式。
|
||||
"""
|
||||
@@ -979,19 +980,19 @@ class Alist(StorageBase, metaclass=WeakSingleton):
|
||||
fileitem, target_path
|
||||
)
|
||||
|
||||
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def link(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
"""
|
||||
硬链接文件
|
||||
"""
|
||||
pass
|
||||
|
||||
def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def softlink(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
"""
|
||||
软链接文件
|
||||
"""
|
||||
pass
|
||||
|
||||
def usage(self) -> Optional[schemas.StorageUsage]:
|
||||
def usage(self) -> Optional[_SchemaStorageUsage]:
|
||||
"""
|
||||
存储使用情况
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,8 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
|
||||
from app import schemas
|
||||
from app.schemas.file import StorageUsage as _SchemaStorageUsage
|
||||
from app.schemas.workflow import FileItem as _SchemaFileItem
|
||||
from app.runtime.config import global_vars, settings
|
||||
from app.application.directory import DirectoryHelper
|
||||
from app.runtime.log import logger
|
||||
@@ -45,7 +46,7 @@ class LocalStorage(StorageBase):
|
||||
"""
|
||||
return True
|
||||
|
||||
def __get_fileitem(self, path: Path) -> schemas.FileItem:
|
||||
def __get_fileitem(self, path: Path) -> _SchemaFileItem:
|
||||
"""
|
||||
获取文件项
|
||||
"""
|
||||
@@ -53,7 +54,7 @@ class LocalStorage(StorageBase):
|
||||
# 顺带只 stat 一次——原先 size 与 modify_time 各 stat 一次,在网络挂载上
|
||||
# 等于把这个热点路径的开销翻倍
|
||||
info = fsproxy.stat(path)
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="file",
|
||||
path=path.as_posix(),
|
||||
@@ -64,11 +65,11 @@ class LocalStorage(StorageBase):
|
||||
modify_time=info["mtime"],
|
||||
)
|
||||
|
||||
def __get_diritem(self, path: Path) -> schemas.FileItem:
|
||||
def __get_diritem(self, path: Path) -> _SchemaFileItem:
|
||||
"""
|
||||
获取目录项
|
||||
"""
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir",
|
||||
path=path.as_posix() + "/",
|
||||
@@ -77,7 +78,7 @@ class LocalStorage(StorageBase):
|
||||
modify_time=fsproxy.stat(path)["mtime"],
|
||||
)
|
||||
|
||||
def list(self, fileitem: schemas.FileItem) -> List[schemas.FileItem]:
|
||||
def list(self, fileitem: _SchemaFileItem) -> List[_SchemaFileItem]:
|
||||
"""
|
||||
浏览文件
|
||||
"""
|
||||
@@ -88,7 +89,7 @@ class LocalStorage(StorageBase):
|
||||
if SystemUtils.is_windows():
|
||||
partitions = SystemUtils.get_windows_drives() or ["C:/"]
|
||||
for partition in partitions:
|
||||
ret_items.append(schemas.FileItem(
|
||||
ret_items.append(_SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir",
|
||||
path=partition + "/",
|
||||
@@ -126,7 +127,7 @@ class LocalStorage(StorageBase):
|
||||
ret_items.append(self.__get_fileitem(item))
|
||||
return ret_items
|
||||
|
||||
def create_folder(self, fileitem: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
|
||||
def create_folder(self, fileitem: _SchemaFileItem, name: str) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
创建目录
|
||||
:param fileitem: 父目录
|
||||
@@ -139,7 +140,7 @@ class LocalStorage(StorageBase):
|
||||
path_obj.mkdir(parents=True, exist_ok=True)
|
||||
return self.__get_diritem(path_obj)
|
||||
|
||||
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_folder(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取目录
|
||||
"""
|
||||
@@ -147,7 +148,7 @@ class LocalStorage(StorageBase):
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return self.__get_diritem(path)
|
||||
|
||||
def get_item(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_item(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件或目录,不存在返回None
|
||||
"""
|
||||
@@ -159,7 +160,7 @@ class LocalStorage(StorageBase):
|
||||
return self.__get_fileitem(path)
|
||||
return self.__get_diritem(path)
|
||||
|
||||
def get_item_strict(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_item_strict(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件或目录,无法确认状态时抛出 StorageQueryError。
|
||||
Path.exists() 会把部分 errno(如 EBADF/ELOOP)归入「不存在」,
|
||||
@@ -178,7 +179,7 @@ class LocalStorage(StorageBase):
|
||||
except OSError as e:
|
||||
raise StorageQueryError(f"【本地】读取文件信息失败: {path} - {e}") from e
|
||||
|
||||
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||
def detail(self, fileitem: _SchemaFileItem) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件详情
|
||||
"""
|
||||
@@ -187,7 +188,7 @@ class LocalStorage(StorageBase):
|
||||
return None
|
||||
return self.__get_fileitem(path_obj)
|
||||
|
||||
def delete(self, fileitem: schemas.FileItem) -> bool:
|
||||
def delete(self, fileitem: _SchemaFileItem) -> bool:
|
||||
"""
|
||||
删除文件
|
||||
"""
|
||||
@@ -211,7 +212,7 @@ class LocalStorage(StorageBase):
|
||||
return False
|
||||
return True
|
||||
|
||||
def rename(self, fileitem: schemas.FileItem, name: str) -> bool:
|
||||
def rename(self, fileitem: _SchemaFileItem, name: str) -> bool:
|
||||
"""
|
||||
重命名文件
|
||||
"""
|
||||
@@ -225,7 +226,7 @@ class LocalStorage(StorageBase):
|
||||
return False
|
||||
return True
|
||||
|
||||
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]:
|
||||
def download(self, fileitem: _SchemaFileItem, path: Path = None) -> Optional[Path]:
|
||||
"""
|
||||
下载文件
|
||||
"""
|
||||
@@ -370,10 +371,10 @@ class LocalStorage(StorageBase):
|
||||
|
||||
def upload(
|
||||
self,
|
||||
fileitem: schemas.FileItem,
|
||||
fileitem: _SchemaFileItem,
|
||||
path: Path,
|
||||
new_name: Optional[str] = None
|
||||
) -> Optional[schemas.FileItem]:
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
上传文件(带进度)
|
||||
"""
|
||||
@@ -402,7 +403,7 @@ class LocalStorage(StorageBase):
|
||||
|
||||
def copy(
|
||||
self,
|
||||
fileitem: schemas.FileItem,
|
||||
fileitem: _SchemaFileItem,
|
||||
path: Path,
|
||||
new_name: str
|
||||
) -> bool:
|
||||
@@ -413,7 +414,7 @@ class LocalStorage(StorageBase):
|
||||
|
||||
def move(
|
||||
self,
|
||||
fileitem: schemas.FileItem,
|
||||
fileitem: _SchemaFileItem,
|
||||
path: Path,
|
||||
new_name: str
|
||||
) -> bool:
|
||||
@@ -443,7 +444,7 @@ class LocalStorage(StorageBase):
|
||||
logger.warn(f"【本地】移动已完成但删除源文件失败:{src} - {err}")
|
||||
return True
|
||||
|
||||
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def link(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
"""
|
||||
硬链接文件
|
||||
"""
|
||||
@@ -454,7 +455,7 @@ class LocalStorage(StorageBase):
|
||||
return False
|
||||
return True
|
||||
|
||||
def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def softlink(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
"""
|
||||
软链接文件
|
||||
"""
|
||||
@@ -465,7 +466,7 @@ class LocalStorage(StorageBase):
|
||||
return False
|
||||
return True
|
||||
|
||||
def usage(self) -> Optional[schemas.StorageUsage]:
|
||||
def usage(self) -> Optional[_SchemaStorageUsage]:
|
||||
"""
|
||||
存储使用情况
|
||||
"""
|
||||
@@ -475,7 +476,7 @@ class LocalStorage(StorageBase):
|
||||
[Path(d.library_path) for d in directory_helper.get_local_library_dirs() if d.library_path],
|
||||
btrfs_fsid_dedup=settings.BTRFS_FSID_DEDUP,
|
||||
)
|
||||
return schemas.StorageUsage(
|
||||
return _SchemaStorageUsage(
|
||||
total=total_storage,
|
||||
available=free_storage
|
||||
)
|
||||
|
||||
@@ -6,7 +6,8 @@ from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Union
|
||||
|
||||
from app import schemas
|
||||
from app.schemas.file import StorageUsage as _SchemaStorageUsage
|
||||
from app.schemas.workflow import FileItem as _SchemaFileItem
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.log import logger
|
||||
from app.modules.filemanager.storages import StorageBase, transfer_process
|
||||
@@ -114,14 +115,14 @@ class Rclone(StorageBase):
|
||||
|
||||
return None
|
||||
|
||||
def __get_rcloneitem(self, item: dict, parent: Optional[str] = "/") -> schemas.FileItem:
|
||||
def __get_rcloneitem(self, item: dict, parent: Optional[str] = "/") -> _SchemaFileItem:
|
||||
"""
|
||||
获取rclone文件项
|
||||
"""
|
||||
if not item:
|
||||
return schemas.FileItem()
|
||||
return _SchemaFileItem()
|
||||
if item.get("IsDir"):
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir",
|
||||
path=f"{parent}{item.get('Name')}" + "/",
|
||||
@@ -130,7 +131,7 @@ class Rclone(StorageBase):
|
||||
modify_time=time_tools.parse_timestamp(item.get("ModTime"))
|
||||
)
|
||||
else:
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="file",
|
||||
path=f"{parent}{item.get('Name')}",
|
||||
@@ -171,7 +172,7 @@ class Rclone(StorageBase):
|
||||
|
||||
def __wait_for_item(
|
||||
self, path: Path, retries: int = 3, delay: float = 0.2
|
||||
) -> Optional[schemas.FileItem]:
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
等待目录或文件在远端可见,兼容云盘最终一致性延迟。
|
||||
"""
|
||||
@@ -198,7 +199,7 @@ class Rclone(StorageBase):
|
||||
logger.error(f"【rclone】存储检查失败:{err}")
|
||||
return False
|
||||
|
||||
def list(self, fileitem: schemas.FileItem) -> List[schemas.FileItem]:
|
||||
def list(self, fileitem: _SchemaFileItem) -> List[_SchemaFileItem]:
|
||||
"""
|
||||
浏览文件
|
||||
"""
|
||||
@@ -220,7 +221,7 @@ class Rclone(StorageBase):
|
||||
logger.error(f"【rclone】浏览文件失败:{err}")
|
||||
return []
|
||||
|
||||
def create_folder(self, fileitem: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
|
||||
def create_folder(self, fileitem: _SchemaFileItem, name: str) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
创建目录
|
||||
:param fileitem: 父目录
|
||||
@@ -253,7 +254,7 @@ class Rclone(StorageBase):
|
||||
return folder
|
||||
return None
|
||||
|
||||
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_folder(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
根据文件路程获取目录,不存在则创建
|
||||
"""
|
||||
@@ -264,7 +265,7 @@ class Rclone(StorageBase):
|
||||
if folder:
|
||||
return folder
|
||||
# 逐级查找和创建目录
|
||||
fileitem = schemas.FileItem(storage=self.schema.value, type="dir", path="/")
|
||||
fileitem = _SchemaFileItem(storage=self.schema.value, type="dir", path="/")
|
||||
for part in normalized.parts[1:]:
|
||||
current_path = Path(self.__normalize_remote_path(Path(fileitem.path) / part))
|
||||
with self.__get_path_lock(current_path):
|
||||
@@ -277,7 +278,7 @@ class Rclone(StorageBase):
|
||||
fileitem = dir_file
|
||||
return fileitem
|
||||
|
||||
def get_item(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_item(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件或目录,不存在返回None
|
||||
"""
|
||||
@@ -300,7 +301,7 @@ class Rclone(StorageBase):
|
||||
logger.debug(f"【rclone】获取文件项失败:{err}")
|
||||
return None
|
||||
|
||||
def get_item_strict(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_item_strict(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件或目录,确认不存在返回None;无法确认状态时抛出 StorageQueryError。
|
||||
rclone 用退出码 3/4 表示目录/文件不存在,其余非零退出无法区分
|
||||
@@ -332,7 +333,7 @@ class Rclone(StorageBase):
|
||||
return self.__get_rcloneitem(item, parent=str(path.parent) + "/")
|
||||
return None
|
||||
|
||||
def delete(self, fileitem: schemas.FileItem) -> bool:
|
||||
def delete(self, fileitem: _SchemaFileItem) -> bool:
|
||||
"""
|
||||
删除文件
|
||||
"""
|
||||
@@ -350,7 +351,7 @@ class Rclone(StorageBase):
|
||||
logger.error(f"【rclone】删除文件失败:{err}")
|
||||
return False
|
||||
|
||||
def rename(self, fileitem: schemas.FileItem, name: str) -> bool:
|
||||
def rename(self, fileitem: _SchemaFileItem, name: str) -> bool:
|
||||
"""
|
||||
重命名文件
|
||||
"""
|
||||
@@ -369,7 +370,7 @@ class Rclone(StorageBase):
|
||||
logger.error(f"【rclone】重命名文件失败:{err}")
|
||||
return False
|
||||
|
||||
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]:
|
||||
def download(self, fileitem: _SchemaFileItem, path: Path = None) -> Optional[Path]:
|
||||
"""
|
||||
带实时进度显示的下载
|
||||
"""
|
||||
@@ -426,8 +427,8 @@ class Rclone(StorageBase):
|
||||
local_path.unlink()
|
||||
return None
|
||||
|
||||
def upload(self, fileitem: schemas.FileItem, path: Path,
|
||||
new_name: Optional[str] = None) -> Optional[schemas.FileItem]:
|
||||
def upload(self, fileitem: _SchemaFileItem, path: Path,
|
||||
new_name: Optional[str] = None) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
带实时进度显示的上传
|
||||
:param fileitem: 上传目录项
|
||||
@@ -483,7 +484,7 @@ class Rclone(StorageBase):
|
||||
logger.error(f"【rclone】上传失败: {target_name} - {err}")
|
||||
return None
|
||||
|
||||
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||
def detail(self, fileitem: _SchemaFileItem) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件详情
|
||||
"""
|
||||
@@ -503,7 +504,7 @@ class Rclone(StorageBase):
|
||||
logger.error(f"【rclone】获取文件详情失败:{err}")
|
||||
return None
|
||||
|
||||
def move(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
|
||||
def move(self, fileitem: _SchemaFileItem, path: Path, new_name: str) -> bool:
|
||||
"""
|
||||
移动文件
|
||||
:param fileitem: 文件项
|
||||
@@ -558,7 +559,7 @@ class Rclone(StorageBase):
|
||||
logger.error(f"【rclone】移动失败: {fileitem.name} - {err}")
|
||||
return False
|
||||
|
||||
def copy(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
|
||||
def copy(self, fileitem: _SchemaFileItem, path: Path, new_name: str) -> bool:
|
||||
"""
|
||||
复制文件
|
||||
:param fileitem: 文件项
|
||||
@@ -613,13 +614,13 @@ class Rclone(StorageBase):
|
||||
logger.error(f"【rclone】复制失败: {fileitem.name} - {err}")
|
||||
return False
|
||||
|
||||
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def link(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
pass
|
||||
|
||||
def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def softlink(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
pass
|
||||
|
||||
def usage(self) -> Optional[schemas.StorageUsage]:
|
||||
def usage(self) -> Optional[_SchemaStorageUsage]:
|
||||
"""
|
||||
存储使用情况
|
||||
"""
|
||||
@@ -647,7 +648,7 @@ class Rclone(StorageBase):
|
||||
)
|
||||
if ret.returncode == 0:
|
||||
items = json.loads(ret.stdout)
|
||||
return schemas.StorageUsage(
|
||||
return _SchemaStorageUsage(
|
||||
total=items.get("total"),
|
||||
available=items.get("free")
|
||||
)
|
||||
|
||||
@@ -12,7 +12,8 @@ from smbprotocol.exceptions import (
|
||||
SMBAuthenticationError,
|
||||
)
|
||||
|
||||
from app import schemas
|
||||
from app.schemas.file import StorageUsage as _SchemaStorageUsage
|
||||
from app.schemas.workflow import FileItem as _SchemaFileItem
|
||||
from app.runtime.config import settings, global_vars
|
||||
from app.runtime.log import logger
|
||||
from app.modules.filemanager import StorageBase
|
||||
@@ -172,7 +173,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
|
||||
def _create_fileitem(
|
||||
self, stat_result, file_path: str, name: str
|
||||
) -> schemas.FileItem:
|
||||
) -> _SchemaFileItem:
|
||||
"""
|
||||
创建文件项
|
||||
"""
|
||||
@@ -195,7 +196,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
modify_time = int(time.time())
|
||||
|
||||
if is_directory:
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir",
|
||||
path=relative_path,
|
||||
@@ -204,7 +205,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
modify_time=modify_time,
|
||||
)
|
||||
else:
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="file",
|
||||
path=relative_path,
|
||||
@@ -217,7 +218,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
except Exception as e:
|
||||
logger.error(f"【SMB】创建文件项失败:{e}")
|
||||
# 返回基本的文件项信息
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="file",
|
||||
path=file_path.replace(self._server_path, "").replace("\\", "/"),
|
||||
@@ -249,7 +250,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
self._connected = False
|
||||
return False
|
||||
|
||||
def list(self, fileitem: schemas.FileItem) -> List[schemas.FileItem]:
|
||||
def list(self, fileitem: _SchemaFileItem) -> List[_SchemaFileItem]:
|
||||
"""
|
||||
浏览文件
|
||||
"""
|
||||
@@ -295,8 +296,8 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
return []
|
||||
|
||||
def create_folder(
|
||||
self, fileitem: schemas.FileItem, name: str
|
||||
) -> Optional[schemas.FileItem]:
|
||||
self, fileitem: _SchemaFileItem, name: str
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
创建目录
|
||||
"""
|
||||
@@ -310,7 +311,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
smbclient.mkdir(new_path)
|
||||
|
||||
# 返回创建的目录信息
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir",
|
||||
path=f"{fileitem.path.rstrip('/')}/{name}/",
|
||||
@@ -322,7 +323,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
logger.error(f"【SMB】创建目录失败: {e}")
|
||||
return None
|
||||
|
||||
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_folder(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取目录,如目录不存在则创建
|
||||
"""
|
||||
@@ -349,7 +350,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
|
||||
return folder
|
||||
|
||||
def get_item(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_item(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件或目录,不存在返回None
|
||||
"""
|
||||
@@ -358,7 +359,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
|
||||
# 处理根目录
|
||||
if str(path) == "/":
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir",
|
||||
path="/",
|
||||
@@ -382,7 +383,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
logger.debug(f"【SMB】获取文件项失败: {e}")
|
||||
return None
|
||||
|
||||
def get_item_strict(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_item_strict(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件或目录,确认不存在返回None;无法确认状态时抛出 StorageQueryError。
|
||||
只有 ENOENT/ENOTDIR 才是「确认不存在」,连接中断、认证失败等都无法确认
|
||||
@@ -393,7 +394,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
|
||||
# 处理根目录
|
||||
if str(path) == "/":
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
type="dir",
|
||||
path="/",
|
||||
@@ -415,13 +416,13 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
except Exception as e:
|
||||
raise StorageQueryError(f"【SMB】查询文件项失败: {path} - {e}") from e
|
||||
|
||||
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||
def detail(self, fileitem: _SchemaFileItem) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件详情
|
||||
"""
|
||||
return self.get_item(Path(fileitem.path))
|
||||
|
||||
def delete(self, fileitem: schemas.FileItem) -> bool:
|
||||
def delete(self, fileitem: _SchemaFileItem) -> bool:
|
||||
"""
|
||||
删除文件或目录
|
||||
"""
|
||||
@@ -523,7 +524,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
logger.error(f"【SMB】递归删除失败: {smb_path} - {e}")
|
||||
raise SMBConnectionError(f"递归删除失败 {smb_path}: {e}")
|
||||
|
||||
def rename(self, fileitem: schemas.FileItem, name: str) -> bool:
|
||||
def rename(self, fileitem: _SchemaFileItem, name: str) -> bool:
|
||||
"""
|
||||
重命名文件
|
||||
"""
|
||||
@@ -543,7 +544,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
logger.error(f"【SMB】重命名失败: {e}")
|
||||
return False
|
||||
|
||||
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]:
|
||||
def download(self, fileitem: _SchemaFileItem, path: Path = None) -> Optional[Path]:
|
||||
"""
|
||||
带实时进度显示的下载
|
||||
"""
|
||||
@@ -595,8 +596,8 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
return None
|
||||
|
||||
def upload(
|
||||
self, fileitem: schemas.FileItem, path: Path, new_name: Optional[str] = None
|
||||
) -> Optional[schemas.FileItem]:
|
||||
self, fileitem: _SchemaFileItem, path: Path, new_name: Optional[str] = None
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
带实时进度显示的上传
|
||||
"""
|
||||
@@ -643,7 +644,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
logger.error(f"【SMB】上传失败: {target_name} - {e}")
|
||||
return None
|
||||
|
||||
def copy(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
|
||||
def copy(self, fileitem: _SchemaFileItem, path: Path, new_name: str) -> bool:
|
||||
"""
|
||||
复制文件
|
||||
"""
|
||||
@@ -670,7 +671,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
logger.error(f"【SMB】复制失败: {e}")
|
||||
return False
|
||||
|
||||
def move(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
|
||||
def move(self, fileitem: _SchemaFileItem, path: Path, new_name: str) -> bool:
|
||||
"""
|
||||
移动文件
|
||||
"""
|
||||
@@ -689,7 +690,7 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
logger.error(f"【SMB】移动失败: {e}")
|
||||
return False
|
||||
|
||||
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def link(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
"""
|
||||
硬链接文件
|
||||
Samba服务器需要开启 unix extensions 支持
|
||||
@@ -722,17 +723,17 @@ class SMB(StorageBase, metaclass=WeakSingleton):
|
||||
logger.error(f"【SMB】创建硬链接失败: {e}")
|
||||
return False
|
||||
|
||||
def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def softlink(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
pass
|
||||
|
||||
def usage(self) -> Optional[schemas.StorageUsage]:
|
||||
def usage(self) -> Optional[_SchemaStorageUsage]:
|
||||
"""
|
||||
存储使用情况
|
||||
"""
|
||||
try:
|
||||
self._check_connection()
|
||||
volume_stat = smbclient.stat_volume(self._server_path)
|
||||
return schemas.StorageUsage(
|
||||
return _SchemaStorageUsage(
|
||||
total=volume_stat.total_size,
|
||||
available=volume_stat.caller_available_size,
|
||||
)
|
||||
|
||||
@@ -12,7 +12,8 @@ from oss2 import SizedFileAdapter, determine_part_size
|
||||
from oss2.models import PartInfo
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
|
||||
from app import schemas
|
||||
from app.schemas.file import StorageUsage as _SchemaStorageUsage
|
||||
from app.schemas.workflow import FileItem as _SchemaFileItem
|
||||
from app.runtime.config import settings, global_vars
|
||||
from app.runtime.log import logger
|
||||
from app.modules.filemanager import StorageBase
|
||||
@@ -474,7 +475,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
def init_storage(self):
|
||||
pass
|
||||
|
||||
def list(self, fileitem: schemas.FileItem) -> List[schemas.FileItem]:
|
||||
def list(self, fileitem: _SchemaFileItem) -> List[_SchemaFileItem]:
|
||||
"""
|
||||
目录遍历实现
|
||||
"""
|
||||
@@ -520,7 +521,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
item_name = item["fn"]
|
||||
full_path = parent_path / item_name
|
||||
items.append(
|
||||
schemas.FileItem(
|
||||
_SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
fileid=str(item["fid"]),
|
||||
parent_fileid=cid,
|
||||
@@ -542,8 +543,8 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
return items
|
||||
|
||||
def create_folder(
|
||||
self, parent_item: schemas.FileItem, name: str
|
||||
) -> Optional[schemas.FileItem]:
|
||||
self, parent_item: _SchemaFileItem, name: str
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
创建目录
|
||||
"""
|
||||
@@ -564,7 +565,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
return self.get_item(new_path)
|
||||
logger.warn(f"【115】创建目录失败: {resp.get('error')}")
|
||||
return None
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
fileid=str(resp["data"]["file_id"]),
|
||||
path=new_path.as_posix() + "/",
|
||||
@@ -576,10 +577,10 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
|
||||
def upload(
|
||||
self,
|
||||
target_dir: schemas.FileItem,
|
||||
target_dir: _SchemaFileItem,
|
||||
local_path: Path,
|
||||
new_name: Optional[str] = None,
|
||||
) -> Optional[schemas.FileItem]:
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
实现带秒传、断点续传和二次认证的文件上传
|
||||
"""
|
||||
@@ -678,7 +679,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
params={"file_id": int(file_id)},
|
||||
)
|
||||
if info_resp:
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
fileid=str(info_resp["file_id"]),
|
||||
path=target_path.as_posix()
|
||||
@@ -872,11 +873,11 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
|
||||
def __build_uploaded_fileitem(
|
||||
self, target_path: Path, local_path: Path, file_size: int
|
||||
) -> schemas.FileItem:
|
||||
) -> _SchemaFileItem:
|
||||
"""
|
||||
构造已上传文件项,用于兼容 115 上传成功后目录索引延迟刷新。
|
||||
"""
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
path=target_path.as_posix(),
|
||||
type="file",
|
||||
@@ -887,7 +888,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
modify_time=local_path.stat().st_mtime if local_path.exists() else None,
|
||||
)
|
||||
|
||||
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]:
|
||||
def download(self, fileitem: _SchemaFileItem, path: Path = None) -> Optional[Path]:
|
||||
"""
|
||||
带实时进度显示的下载
|
||||
"""
|
||||
@@ -957,7 +958,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
def check(self) -> bool:
|
||||
return self.access_token is not None
|
||||
|
||||
def delete(self, fileitem: schemas.FileItem) -> bool:
|
||||
def delete(self, fileitem: _SchemaFileItem) -> bool:
|
||||
"""
|
||||
删除文件/目录
|
||||
"""
|
||||
@@ -969,7 +970,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
except httpx.HTTPError:
|
||||
return False
|
||||
|
||||
def rename(self, fileitem: schemas.FileItem, name: str) -> bool:
|
||||
def rename(self, fileitem: _SchemaFileItem, name: str) -> bool:
|
||||
"""
|
||||
重命名文件/目录
|
||||
"""
|
||||
@@ -984,7 +985,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
return True
|
||||
return False
|
||||
|
||||
def __get_info_item(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def __get_info_item(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
查询指定路径的文件/目录项,无法确认状态时抛出 StorageQueryError。
|
||||
接口业务码 20004(记录不存在)、430004(路径不存在)与 0 一样
|
||||
@@ -1004,7 +1005,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
if not data or not data.get("file_id"):
|
||||
# 115 对记录不存在和路径不存在返回不同业务码,两者都可确认目标不存在
|
||||
return None
|
||||
return schemas.FileItem(
|
||||
return _SchemaFileItem(
|
||||
storage=self.schema.value,
|
||||
fileid=str(data["file_id"]),
|
||||
path=path.as_posix() + ("/" if data["file_category"] == "0" else ""),
|
||||
@@ -1019,7 +1020,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
modify_time=data["utime"],
|
||||
)
|
||||
|
||||
def get_item(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_item(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取指定路径的文件/目录项
|
||||
"""
|
||||
@@ -1029,7 +1030,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
logger.debug(f"【115】获取文件信息失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_item_strict(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_item_strict(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取指定路径的文件/目录项,无法确认状态时抛出 StorageQueryError。
|
||||
"""
|
||||
@@ -1040,14 +1041,14 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
except Exception as e:
|
||||
raise StorageQueryError(f"【115】查询文件信息失败: {path} - {e}") from e
|
||||
|
||||
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
def get_folder(self, path: Path) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取指定路径的文件夹,如不存在则创建
|
||||
"""
|
||||
|
||||
def __find_dir(
|
||||
_fileitem: schemas.FileItem, _name: str
|
||||
) -> Optional[schemas.FileItem]:
|
||||
_fileitem: _SchemaFileItem, _name: str
|
||||
) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
查找下级目录中匹配名称的目录
|
||||
"""
|
||||
@@ -1063,7 +1064,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
if folder:
|
||||
return folder
|
||||
# 逐级查找和创建目录
|
||||
fileitem = schemas.FileItem(storage=self.schema.value, path="/")
|
||||
fileitem = _SchemaFileItem(storage=self.schema.value, path="/")
|
||||
for part in path.parts[1:]:
|
||||
dir_file = __find_dir(fileitem, part)
|
||||
if dir_file:
|
||||
@@ -1076,13 +1077,13 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
fileitem = dir_file
|
||||
return fileitem
|
||||
|
||||
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||
def detail(self, fileitem: _SchemaFileItem) -> Optional[_SchemaFileItem]:
|
||||
"""
|
||||
获取文件/目录详细信息
|
||||
"""
|
||||
return self.get_item(Path(fileitem.path))
|
||||
|
||||
def copy(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
|
||||
def copy(self, fileitem: _SchemaFileItem, path: Path, new_name: str) -> bool:
|
||||
"""
|
||||
复制
|
||||
"""
|
||||
@@ -1115,7 +1116,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
return True
|
||||
return False
|
||||
|
||||
def move(self, fileitem: schemas.FileItem, path: Path, new_name: str) -> bool:
|
||||
def move(self, fileitem: _SchemaFileItem, path: Path, new_name: str) -> bool:
|
||||
"""
|
||||
移动
|
||||
"""
|
||||
@@ -1147,13 +1148,13 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
return True
|
||||
return False
|
||||
|
||||
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def link(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
pass
|
||||
|
||||
def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
|
||||
def softlink(self, fileitem: _SchemaFileItem, target_file: Path) -> bool:
|
||||
pass
|
||||
|
||||
def usage(self) -> Optional[schemas.StorageUsage]:
|
||||
def usage(self) -> Optional[_SchemaStorageUsage]:
|
||||
"""
|
||||
存储使用情况
|
||||
"""
|
||||
@@ -1162,7 +1163,7 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
if not resp:
|
||||
return None
|
||||
space = resp["rt_space_info"]
|
||||
return schemas.StorageUsage(
|
||||
return _SchemaStorageUsage(
|
||||
total=space["all_total"]["size"], available=space["all_remain"]["size"]
|
||||
)
|
||||
except NoCheckInException:
|
||||
|
||||
Reference in New Issue
Block a user