mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-19 05:03:57 +08:00
fix(monitor): 目录监控自愈、快照语义修正与覆盖保护闭环 (#6210)
This commit is contained in:
@@ -174,6 +174,13 @@ class StorageBase(metaclass=ABCMeta):
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_item_strict(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
获取文件或目录,确认不存在返回None;无法确认状态时抛出 StorageQueryError。
|
||||
默认实现不区分「不存在」与「查询失败」,由具体存储按需覆写。
|
||||
"""
|
||||
return self.get_item(path)
|
||||
|
||||
def get_parent(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
获取父目录
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.core.config import settings, global_vars
|
||||
from app.log import logger
|
||||
from app.modules.filemanager import StorageBase
|
||||
from app.modules.filemanager.storages import transfer_process
|
||||
from app.schemas.exception import StorageQueryError
|
||||
from app.schemas.types import StorageSchema
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.singleton import WeakSingleton
|
||||
@@ -834,30 +835,53 @@ class AliPan(StorageBase, metaclass=WeakSingleton):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __get_by_path_item(self, path: Path, drive_id: str = None) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
按路径查询文件/目录项,无法确认状态时抛出 StorageQueryError。
|
||||
NotFound 系列错误码表示确认不存在,其余错误(网络失败、限流、
|
||||
权限或未知业务错误)均无法确认目标状态。
|
||||
"""
|
||||
resp = self._request_api(
|
||||
"POST",
|
||||
"/adrive/v1.0/openFile/get_by_path",
|
||||
json={
|
||||
"drive_id": drive_id or self._default_drive_id,
|
||||
"file_path": path.as_posix(),
|
||||
},
|
||||
no_error_log=True,
|
||||
)
|
||||
if resp is None:
|
||||
raise StorageQueryError(f"【阿里云盘】无法确认文件状态(请求失败): {path}")
|
||||
code = resp.get("code")
|
||||
if code:
|
||||
if "NotFound" in str(code):
|
||||
# 明确的不存在错误码,确认目标不存在
|
||||
return None
|
||||
raise StorageQueryError(
|
||||
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]:
|
||||
"""
|
||||
获取指定路径的文件/目录项
|
||||
"""
|
||||
try:
|
||||
resp = self._request_api(
|
||||
"POST",
|
||||
"/adrive/v1.0/openFile/get_by_path",
|
||||
json={
|
||||
"drive_id": drive_id or self._default_drive_id,
|
||||
"file_path": path.as_posix(),
|
||||
},
|
||||
no_error_log=True,
|
||||
)
|
||||
if not resp:
|
||||
return None
|
||||
if resp.get("code"):
|
||||
logger.debug(f"【阿里云盘】获取文件信息失败: {resp.get('message')}")
|
||||
return None
|
||||
return self.__get_fileitem(resp, parent=str(path.parent))
|
||||
return self.__get_by_path_item(path, drive_id=drive_id)
|
||||
except Exception as e:
|
||||
logger.debug(f"【阿里云盘】获取文件信息失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_item_strict(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
获取指定路径的文件/目录项,无法确认状态时抛出 StorageQueryError。
|
||||
"""
|
||||
try:
|
||||
return self.__get_by_path_item(path)
|
||||
except StorageQueryError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise StorageQueryError(f"【阿里云盘】查询文件信息失败: {path} - {e}") from e
|
||||
|
||||
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
获取指定路径的文件夹,如不存在则创建
|
||||
|
||||
@@ -8,6 +8,7 @@ from app.core.config import global_vars, settings
|
||||
from app.helper.directory import DirectoryHelper
|
||||
from app.log import logger
|
||||
from app.modules.filemanager.storages import StorageBase, transfer_process
|
||||
from app.schemas.exception import StorageQueryError
|
||||
from app.schemas.types import StorageSchema
|
||||
from app.utils.system import SystemUtils
|
||||
|
||||
@@ -148,6 +149,23 @@ class LocalStorage(StorageBase):
|
||||
return self.__get_fileitem(path)
|
||||
return self.__get_diritem(path)
|
||||
|
||||
def get_item_strict(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
获取文件或目录,无法确认状态时抛出 StorageQueryError。
|
||||
Path.exists() 会把部分 errno(如 EBADF/ELOOP)归入「不存在」,
|
||||
网络/FUSE 挂载抖动时会误判,这里用 stat 显式区分。
|
||||
"""
|
||||
try:
|
||||
path.stat()
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
return None
|
||||
except OSError as e:
|
||||
raise StorageQueryError(f"【本地】读取文件状态失败: {path} - {e}") from e
|
||||
try:
|
||||
return self.get_item(path)
|
||||
except OSError as e:
|
||||
raise StorageQueryError(f"【本地】读取文件信息失败: {path} - {e}") from e
|
||||
|
||||
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
获取文件详情
|
||||
|
||||
@@ -17,6 +17,7 @@ from app.core.config import settings, global_vars
|
||||
from app.log import logger
|
||||
from app.modules.filemanager import StorageBase
|
||||
from app.modules.filemanager.storages import transfer_process
|
||||
from app.schemas.exception import StorageQueryError
|
||||
from app.schemas.types import StorageSchema
|
||||
from app.utils.singleton import WeakSingleton
|
||||
from app.utils.string import StringUtils
|
||||
@@ -906,38 +907,60 @@ class U115Pan(StorageBase, metaclass=WeakSingleton):
|
||||
return True
|
||||
return False
|
||||
|
||||
def __get_info_item(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
查询指定路径的文件/目录项,无法确认状态时抛出 StorageQueryError。
|
||||
接口业务码 20004(记录不存在)与 0 一样视为确认结果,其余错误
|
||||
(网络失败、限流重试用尽、未知业务错误)均无法确认目标状态。
|
||||
"""
|
||||
resp = self._request_api(
|
||||
"POST",
|
||||
"/open/folder/get_info",
|
||||
data={"path": path.as_posix()},
|
||||
no_error_log=True,
|
||||
)
|
||||
if resp is None:
|
||||
raise StorageQueryError(f"【115】无法确认文件状态(请求失败或接口错误): {path}")
|
||||
data = resp.get("data") if isinstance(resp, dict) else None
|
||||
if not data or not data.get("file_id"):
|
||||
# code 20004(记录不存在)等场景,确认目标不存在
|
||||
return None
|
||||
return schemas.FileItem(
|
||||
storage=self.schema.value,
|
||||
fileid=str(data["file_id"]),
|
||||
path=path.as_posix() + ("/" if data["file_category"] == "0" else ""),
|
||||
type="file" if data["file_category"] == "1" else "dir",
|
||||
name=data["file_name"],
|
||||
basename=Path(data["file_name"]).stem,
|
||||
extension=Path(data["file_name"]).suffix[1:]
|
||||
if data["file_category"] == "1"
|
||||
else None,
|
||||
pickcode=data["pick_code"],
|
||||
size=data["size_byte"] if data["file_category"] == "1" else None,
|
||||
modify_time=data["utime"],
|
||||
)
|
||||
|
||||
def get_item(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
获取指定路径的文件/目录项
|
||||
"""
|
||||
try:
|
||||
resp = self._request_api(
|
||||
"POST",
|
||||
"/open/folder/get_info",
|
||||
"data",
|
||||
data={"path": path.as_posix()},
|
||||
no_error_log=True,
|
||||
)
|
||||
if not resp:
|
||||
return None
|
||||
return schemas.FileItem(
|
||||
storage=self.schema.value,
|
||||
fileid=str(resp["file_id"]),
|
||||
path=path.as_posix() + ("/" if resp["file_category"] == "0" else ""),
|
||||
type="file" if resp["file_category"] == "1" else "dir",
|
||||
name=resp["file_name"],
|
||||
basename=Path(resp["file_name"]).stem,
|
||||
extension=Path(resp["file_name"]).suffix[1:]
|
||||
if resp["file_category"] == "1"
|
||||
else None,
|
||||
pickcode=resp["pick_code"],
|
||||
size=resp["size_byte"] if resp["file_category"] == "1" else None,
|
||||
modify_time=resp["utime"],
|
||||
)
|
||||
return self.__get_info_item(path)
|
||||
except Exception as e:
|
||||
logger.debug(f"【115】获取文件信息失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_item_strict(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
获取指定路径的文件/目录项,无法确认状态时抛出 StorageQueryError。
|
||||
"""
|
||||
try:
|
||||
return self.__get_info_item(path)
|
||||
except StorageQueryError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise StorageQueryError(f"【115】查询文件信息失败: {path} - {e}") from e
|
||||
|
||||
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
|
||||
"""
|
||||
获取指定路径的文件夹,如不存在则创建
|
||||
|
||||
@@ -23,6 +23,7 @@ from app.schemas import (
|
||||
TransferRenameBuildEventData,
|
||||
TransferRenameEventData,
|
||||
)
|
||||
from app.schemas.exception import StorageQueryError
|
||||
from app.schemas.types import MediaType, ChainEventType
|
||||
from app.utils.system import SystemUtils
|
||||
|
||||
@@ -405,8 +406,23 @@ class TransHandler:
|
||||
# 判断是否要覆盖,附加文件强制覆盖
|
||||
overflag = False
|
||||
if not __is_extra_file(fileitem):
|
||||
# 目标文件
|
||||
target_item = target_oper.get_item(new_file)
|
||||
# 目标文件(严格查询:无法确认状态时拒绝覆盖,避免已有文件被误覆盖)
|
||||
try:
|
||||
target_item = target_oper.get_item_strict(new_file)
|
||||
except StorageQueryError as query_err:
|
||||
errmsg = f"无法确认目标文件状态,已跳过整理以避免误覆盖:{new_file} - {query_err}"
|
||||
logger.warn(errmsg)
|
||||
self.__update_result(
|
||||
result=result,
|
||||
success=False,
|
||||
message=errmsg,
|
||||
fileitem=fileitem,
|
||||
target_diritem=target_diritem,
|
||||
fail_list=[fileitem.path],
|
||||
transfer_type=transfer_type,
|
||||
need_notify=need_notify,
|
||||
)
|
||||
return result
|
||||
if target_item:
|
||||
# 目标文件已存在
|
||||
target_file = new_file
|
||||
|
||||
Reference in New Issue
Block a user