4c161b63c6
Add optional drive API capabilities for Quark and UC adapters, including directory lookup/creation, rename, move, delete, task polling, Quark recycle cleanup, and UC share staging folder support.\n\nAdd unittest coverage for capability declarations, production transfer path save_dir resolution, staging-folder flow, delete_files behavior, task polling params, and HTTP/JSON error handling.\n\nDocument the Hong Kong test server deployment boundary and verification commands.
639 lines
25 KiB
Python
639 lines
25 KiB
Python
"""
|
|
CloudSearch Transfer — UC网盘适配器 v1.0.0
|
|
|
|
将 UcCredentialManager、UcTransfer、UcCleanup 组合为
|
|
BaseCloudDriveAdapter 的完整实现。
|
|
|
|
UC网盘 7 步 API 转存流程(与夸克高度相似,API 域名不同):
|
|
① POST .../share/sharepage/v2/detail?pr=UCBrowser&fr=pc → stoken
|
|
② GET .../share/sharepage/detail → fid, share_fid_token, title
|
|
③ POST .../share/sharepage/save → task_id (转存)
|
|
④ 轮询 GET .../task → save_as_top_fids
|
|
⑤ POST .../share → task_id (创建分享)
|
|
⑥ 轮询 GET .../task → share_id
|
|
⑦ POST .../share/password → share_url, passcode
|
|
|
|
参考 cloud-auto-save 的 quark 实现,域名从 drive-pc.quark.cn 改为 pc-api.uc.cn。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
from urllib.parse import urlparse, parse_qs
|
|
|
|
from ..base import BaseCloudDriveAdapter, FileInfo, TransferResult, VerifyResult
|
|
from ...config import PlatformConfig, TransferConfig
|
|
from ...errors import TransferError, TransferErrorCode
|
|
|
|
from .credential import UcCredentialManager
|
|
from .transfer import UcTransfer, SHARE_URL_PATTERN
|
|
from .cleanup import UcCleanup
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class UcAdapter(BaseCloudDriveAdapter):
|
|
"""UC网盘适配器。
|
|
|
|
组合 credential / transfer / cleanup 三个模块,
|
|
实现 BaseCloudDriveAdapter 定义的所有抽象方法。
|
|
|
|
Attributes:
|
|
PLATFORM_NAME: 展示用平台名称。
|
|
PLATFORM_KEY: 内部平台标识。
|
|
URL_PATTERNS: UC 分享链接匹配正则列表。
|
|
"""
|
|
|
|
# ─── 平台标识 ──────────────────────────────────────────────
|
|
PLATFORM_NAME: str = "UC网盘"
|
|
PLATFORM_KEY: str = "uc"
|
|
|
|
# ─── URL 匹配 ──────────────────────────────────────────────
|
|
# 支持 drive.uc.cn/s/<share_id>
|
|
URL_PATTERNS: List[str] = [
|
|
r"drive\.uc\.cn/s/(\w+)",
|
|
]
|
|
|
|
|
|
capabilities: Dict[str, bool] = {
|
|
"ensure_dir": True,
|
|
"save_files": True,
|
|
"poll_task": True,
|
|
"rename": True,
|
|
"move_files": True,
|
|
"delete_files": True,
|
|
"cleanup_recycle": False,
|
|
"share_staging_folder": True,
|
|
}
|
|
|
|
def __init__(self, config: PlatformConfig, transfer_config: TransferConfig) -> None:
|
|
"""初始化适配器。"""
|
|
self._credential: UcCredentialManager = UcCredentialManager(cookie=config.cookie)
|
|
self._transfer_engine: UcTransfer = UcTransfer(
|
|
credential=self._credential,
|
|
timeout=transfer_config.request_timeout,
|
|
poll_interval=transfer_config.task_poll_interval,
|
|
poll_max_attempts=transfer_config.task_poll_max_attempts,
|
|
)
|
|
self._cleanup: UcCleanup = UcCleanup(
|
|
credential=self._credential,
|
|
timeout=transfer_config.request_timeout,
|
|
)
|
|
super().__init__(config, transfer_config)
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 公开接口实现
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
def _setup_session(self) -> None:
|
|
"""将 UC Cookie 注入 session 的默认 headers。"""
|
|
headers = self._credential.get_headers()
|
|
if headers:
|
|
self.session.headers.update(headers)
|
|
logger.debug("[UcAdapter] Session headers updated with Cookie")
|
|
|
|
def transfer(self, share_url: str, save_dir: str = "",
|
|
share_password: str = "") -> TransferResult:
|
|
start: float = time.time()
|
|
|
|
if not self._credential.validate():
|
|
raise TransferError(
|
|
TransferErrorCode.NOT_LOGIN,
|
|
message="UC Cookie 无效或长度不足",
|
|
platform=self.PLATFORM_KEY,
|
|
)
|
|
|
|
requested_dir: str = save_dir or self.config.save_dir or "/"
|
|
target_dir: str = requested_dir if requested_dir and not str(requested_dir).startswith("/") else self.ensure_dir(requested_dir)
|
|
staging_dir: str = self.get_or_create_share_folder() or target_dir
|
|
pwd: str = share_password or self.config.share_password or ""
|
|
|
|
try:
|
|
pwd_id, passcode = self._parse_share_url(share_url)
|
|
stoken: str = self._transfer_engine._get_stoken(pwd_id, passcode)
|
|
detail: Dict[str, Any] = self._transfer_engine._get_detail(pwd_id, stoken)
|
|
task_id: str = self._transfer_engine._init_save(
|
|
pwd_id, stoken, detail, to_pdir_fid=staging_dir
|
|
)
|
|
new_fids: List[str] = self._transfer_engine._poll_save_task(task_id)
|
|
if not new_fids:
|
|
raise RuntimeError("转存完成但未获取到文件ID")
|
|
|
|
if staging_dir != target_dir:
|
|
move_result = self.move_files(new_fids, target_dir)
|
|
if not self._api_success(move_result):
|
|
raise RuntimeError(f"移动到目标目录失败: {move_result.get('message', move_result)}")
|
|
|
|
if self.transfer_config.ad_filter_enabled and new_fids:
|
|
new_fids = self._filter_ads(new_fids)
|
|
if not new_fids:
|
|
raise RuntimeError("广告过滤后无可分享文件")
|
|
|
|
title: str = detail.get("title", "分享")
|
|
share_task_id: str = self._transfer_engine._init_share(new_fids, title)
|
|
share_id: str = self._transfer_engine._poll_share_task(share_task_id)
|
|
share_url_new, passcode_new = self._transfer_engine._set_password(share_id, pwd)
|
|
except ValueError as exc:
|
|
raise TransferError(
|
|
TransferErrorCode.URL_INVALID,
|
|
message=str(exc),
|
|
platform=self.PLATFORM_KEY,
|
|
) from exc
|
|
except RuntimeError as exc:
|
|
msg: str = str(exc)
|
|
if "stoken" in msg or "status" in msg:
|
|
raise TransferError(
|
|
TransferErrorCode.SHARE_NOT_EXIST,
|
|
message=msg,
|
|
platform=self.PLATFORM_KEY,
|
|
) from exc
|
|
raise TransferError(
|
|
TransferErrorCode.NETWORK_ERROR,
|
|
message=msg,
|
|
platform=self.PLATFORM_KEY,
|
|
) from exc
|
|
|
|
elapsed: int = int((time.time() - start) * 1000)
|
|
return TransferResult(
|
|
success=True,
|
|
platform=self.PLATFORM_KEY,
|
|
new_file_id=",".join(new_fids),
|
|
file_name=title,
|
|
share_url=share_url_new,
|
|
share_password=passcode_new,
|
|
original_url=share_url,
|
|
elapsed_ms=elapsed,
|
|
)
|
|
|
|
def verify(self, share_url: str) -> VerifyResult:
|
|
"""验证 UC 分享链接有效性。
|
|
|
|
Args:
|
|
share_url: UC 分享链接。
|
|
|
|
Returns:
|
|
VerifyResult 包含验证结果。
|
|
"""
|
|
try:
|
|
pwd_id, passcode = self._parse_share_url(share_url)
|
|
|
|
if not self._credential.validate():
|
|
return VerifyResult(
|
|
valid=False,
|
|
platform=self.PLATFORM_KEY,
|
|
error=TransferError(
|
|
TransferErrorCode.NOT_LOGIN,
|
|
platform=self.PLATFORM_KEY,
|
|
),
|
|
)
|
|
|
|
stoken: str = self._transfer_engine._get_stoken(pwd_id, passcode)
|
|
detail: Dict[str, Any] = self._transfer_engine._get_detail(pwd_id, stoken)
|
|
files: List[FileInfo] = self._extract_file_list(detail)
|
|
|
|
return VerifyResult(
|
|
valid=True,
|
|
platform=self.PLATFORM_KEY,
|
|
title=detail.get("title", ""),
|
|
file_count=len(files),
|
|
files=files,
|
|
)
|
|
|
|
except TransferError as exc:
|
|
return VerifyResult(valid=False, platform=self.PLATFORM_KEY, error=exc)
|
|
except (ValueError, RuntimeError) as exc:
|
|
return VerifyResult(
|
|
valid=False,
|
|
platform=self.PLATFORM_KEY,
|
|
error=TransferError(
|
|
TransferErrorCode.SHARE_NOT_EXIST,
|
|
message=str(exc),
|
|
platform=self.PLATFORM_KEY,
|
|
),
|
|
)
|
|
except Exception as exc:
|
|
return VerifyResult(
|
|
valid=False,
|
|
platform=self.PLATFORM_KEY,
|
|
error=TransferError(
|
|
TransferErrorCode.NETWORK_ERROR,
|
|
message=str(exc),
|
|
platform=self.PLATFORM_KEY,
|
|
),
|
|
)
|
|
|
|
# ─── 核心抽象方法 ─────────────────────────────────────────
|
|
|
|
def _get_share_detail(self, pwd_id: str, passcode: str = "") -> dict:
|
|
"""获取 UC 分享详情。
|
|
|
|
Args:
|
|
pwd_id: 分享 ID。
|
|
passcode: 提取码。
|
|
|
|
Returns:
|
|
分享详情字典,包含 title, fid, share_fid_token 等字段。
|
|
"""
|
|
stoken: str = self._transfer_engine._get_stoken(pwd_id, passcode)
|
|
return self._transfer_engine._get_detail(pwd_id, stoken)
|
|
|
|
def _save_files(self, pwd_id: str, detail: dict, save_dir: str) -> List[str]:
|
|
"""转存文件到自己的 UC 网盘。
|
|
|
|
Args:
|
|
pwd_id: 分享 ID。
|
|
detail: 分享详情(来自 _get_share_detail)。
|
|
save_dir: 目标目录 ID。
|
|
|
|
Returns:
|
|
转存后的新文件 ID 列表。
|
|
"""
|
|
stoken: str = self._transfer_engine._get_stoken(pwd_id)
|
|
task_id: str = self._transfer_engine._init_save(
|
|
pwd_id, stoken, detail, to_pdir_fid=save_dir
|
|
)
|
|
return self._transfer_engine._poll_save_task(task_id)
|
|
|
|
def _create_share(
|
|
self, file_ids: List[str], title: str, password: str = ""
|
|
) -> Tuple[str, str]:
|
|
"""创建 UC 分享链接。
|
|
|
|
Args:
|
|
file_ids: 要分享的文件 ID 列表。
|
|
title: 分享标题。
|
|
password: 分享密码。
|
|
|
|
Returns:
|
|
(share_url, share_password) 元组。
|
|
"""
|
|
task_id: str = self._transfer_engine._init_share(file_ids, title)
|
|
share_id: str = self._transfer_engine._poll_share_task(task_id)
|
|
return self._transfer_engine._set_password(share_id, password)
|
|
|
|
def _extract_file_list(self, detail: dict) -> List[FileInfo]:
|
|
"""从 UC 分享详情中提取文件列表。
|
|
|
|
UC 的 sharepage/detail 返回格式与夸克一致:
|
|
{
|
|
"files": [
|
|
{"fid": "...", "file_name": "...", "size": 123, "dir": false, ...},
|
|
]
|
|
}
|
|
|
|
Args:
|
|
detail: 分享详情字典。
|
|
|
|
Returns:
|
|
FileInfo 对象列表。
|
|
"""
|
|
files_data: List[Dict[str, Any]] = detail.get("files", [])
|
|
result: List[FileInfo] = []
|
|
|
|
for f in files_data:
|
|
file_info = FileInfo(
|
|
fid=str(f.get("fid", f.get("file_id", ""))),
|
|
name=str(f.get("file_name", f.get("name", ""))),
|
|
size=int(f.get("size", 0)),
|
|
is_dir=bool(f.get("dir", f.get("is_dir", False))),
|
|
ext=str(f.get("ext", f.get("file_extension", ""))),
|
|
)
|
|
result.append(file_info)
|
|
|
|
# 如果 files 为空,尝试用 detail 顶层字段构造单个文件信息
|
|
if not result and detail.get("fid"):
|
|
result.append(
|
|
FileInfo(
|
|
fid=str(detail.get("fid", "")),
|
|
name=str(detail.get("title", detail.get("file_name", ""))),
|
|
size=0,
|
|
is_dir=False,
|
|
)
|
|
)
|
|
|
|
return result
|
|
|
|
def _filter_ads(self, file_ids: List[str]) -> List[str]:
|
|
"""过滤广告文件。
|
|
|
|
Args:
|
|
file_ids: 文件 ID 列表。
|
|
|
|
Returns:
|
|
过滤后的文件 ID 列表。
|
|
"""
|
|
keywords: List[str] = list(
|
|
set(self.config.banned_keywords)
|
|
| set(self.transfer_config.default_banned_keywords)
|
|
)
|
|
if not keywords:
|
|
return file_ids
|
|
|
|
try:
|
|
files: List[FileInfo] = self.get_files()
|
|
file_names: List[str] = [f.name for f in files]
|
|
return UcCleanup.filter_ad_ids(file_ids, file_names, keywords)
|
|
except Exception:
|
|
logger.warning(
|
|
"[UcAdapter] Cannot fetch file list for ad filtering, skipping"
|
|
)
|
|
return file_ids
|
|
|
|
|
|
# ─── Drive API capability helpers ─────────────────────────────
|
|
|
|
@staticmethod
|
|
def _normalize_dir_path(dir_path: str) -> str:
|
|
path = "/" + str(dir_path or "").strip().strip("/")
|
|
return "/" if path == "/" else path
|
|
|
|
@staticmethod
|
|
def _api_success(payload: Dict[str, Any]) -> bool:
|
|
if not isinstance(payload, dict):
|
|
return False
|
|
code = payload.get("code")
|
|
status = payload.get("status")
|
|
return code == 0 or status in (0, 200)
|
|
|
|
def ensure_dir(self, dir_path: str) -> str:
|
|
normalized = self._normalize_dir_path(dir_path)
|
|
if normalized == "/":
|
|
return "0"
|
|
|
|
parts = [part for part in normalized.strip("/").split("/") if part]
|
|
prefixes = ["/" + "/".join(parts[:idx]) for idx in range(1, len(parts) + 1)]
|
|
existing = {
|
|
item.get("file_path"): str(item.get("fid"))
|
|
for item in self.get_fids(prefixes)
|
|
if item.get("file_path") and item.get("fid")
|
|
}
|
|
|
|
leaf_fid = existing.get(normalized)
|
|
if leaf_fid:
|
|
return leaf_fid
|
|
|
|
for prefix in prefixes:
|
|
if prefix in existing:
|
|
continue
|
|
result = self.mkdir(prefix)
|
|
if self._api_success(result) and result.get("data", {}).get("fid"):
|
|
existing[prefix] = str(result["data"]["fid"])
|
|
continue
|
|
raise TransferError(
|
|
TransferErrorCode.NETWORK_ERROR,
|
|
message=f"创建目录失败: {result.get('message', result)}",
|
|
platform=self.PLATFORM_KEY,
|
|
)
|
|
return existing[normalized]
|
|
|
|
def mkdir(self, dir_path: str) -> Dict[str, Any]:
|
|
url = "https://pc-api.uc.cn/1/clouddrive/file"
|
|
params = {"pr": "UCBrowser", "fr": "pc", "uc_param_str": ""}
|
|
payload = {
|
|
"pdir_fid": "0",
|
|
"file_name": "",
|
|
"dir_path": self._normalize_dir_path(dir_path),
|
|
"dir_init_lock": False,
|
|
}
|
|
return self._drive_api_json(self._post(url, json_data=payload, params=params, headers=self._credential.get_headers()), context="写入网盘目录")
|
|
|
|
def rename(self, fid: str, file_name: str) -> Dict[str, Any]:
|
|
url = "https://pc-api.uc.cn/1/clouddrive/file/rename"
|
|
params = {"pr": "UCBrowser", "fr": "pc", "uc_param_str": ""}
|
|
payload = {"fid": fid, "file_name": file_name}
|
|
return self._drive_api_json(self._post(url, json_data=payload, params=params, headers=self._credential.get_headers()), context="写入网盘目录")
|
|
|
|
def get_fids(self, file_paths: List[str]) -> List[Dict[str, Any]]:
|
|
pending = [self._normalize_dir_path(p) for p in file_paths]
|
|
result: List[Dict[str, Any]] = []
|
|
while pending:
|
|
batch, pending = pending[:50], pending[50:]
|
|
url = "https://pc-api.uc.cn/1/clouddrive/file/info/path_list"
|
|
params = {"pr": "UCBrowser", "fr": "pc"}
|
|
payload = {"file_path": batch, "namespace": "0"}
|
|
data = self._drive_api_json(self._post(url, json_data=payload, params=params, headers=self._credential.get_headers()), context="按路径获取文件ID")
|
|
if not self._api_success(data):
|
|
raise TransferError(
|
|
TransferErrorCode.NETWORK_ERROR,
|
|
message=f"获取目录ID失败: {data.get('message', data)}",
|
|
platform=self.PLATFORM_KEY,
|
|
)
|
|
result.extend(data.get("data", []))
|
|
return result
|
|
|
|
def move_files(self, fids: List[str], to_pdir_fid: str) -> Dict[str, Any]:
|
|
if not fids:
|
|
return {"code": 0, "message": "无文件需要移动"}
|
|
last: Dict[str, Any] = {"code": 0, "message": "success"}
|
|
for offset in range(0, len(fids), 100):
|
|
batch = fids[offset:offset + 100]
|
|
url = "https://pc-api.uc.cn/1/clouddrive/file/move"
|
|
params = {"uc_param_str": "", "fr": "pc", "pr": "UCBrowser"}
|
|
payload = {"filelist": batch, "to_pdir_fid": to_pdir_fid, "exclude_fids": [], "action_type": 1}
|
|
last = self._drive_api_json(self._post(url, json_data=payload, params=params, headers=self._credential.get_headers()), context="移动网盘文件")
|
|
if not self._api_success(last):
|
|
return last
|
|
task_id = last.get("data", {}).get("task_id")
|
|
if task_id:
|
|
task = self.query_task(task_id)
|
|
if not self._api_success(task) or task.get("data", {}).get("status") == -1:
|
|
return {"code": 1, "message": task.get("data", {}).get("message", task.get("message", "移动任务失败")), "data": task.get("data", {})}
|
|
return {"code": 0, "message": "移动完成", "data": last.get("data", {})}
|
|
|
|
def _task_query_params(self, retry_index: int = 0) -> Dict[str, Any]:
|
|
now_ms = int(time.time() * 1000)
|
|
return {
|
|
"pr": "UCBrowser",
|
|
"fr": "pc",
|
|
"uc_param_str": "",
|
|
"retry_index": retry_index,
|
|
"__dt": 300,
|
|
"__t": now_ms,
|
|
}
|
|
|
|
def delete_files(self, fids: List[str]) -> Dict[str, Any]:
|
|
if not fids:
|
|
return {"code": 0, "status": 200}
|
|
if self.delete(fids):
|
|
return {"code": 0, "status": 200}
|
|
return {"code": 1, "status": 500, "message": "删除文件失败"}
|
|
|
|
def query_task(self, task_id: str) -> Dict[str, Any]:
|
|
url = "https://pc-api.uc.cn/1/clouddrive/task"
|
|
try:
|
|
data = self._poll_task(url, task_id, query_params=self._task_query_params)
|
|
return {"code": 0, "status": 200, "data": data}
|
|
except TransferError as exc:
|
|
return {"code": 1, "status": 500, "message": str(exc), "data": {"status": -1}}
|
|
|
|
def get_or_create_share_folder(self) -> Optional[str]:
|
|
if getattr(self, "_share_folder_fid", None):
|
|
return self._share_folder_fid
|
|
root = self.ls_dir("0")
|
|
if self._api_success(root):
|
|
for item in root.get("data", {}).get("list", []):
|
|
if item.get("file_name") == "来自:分享" and item.get("dir"):
|
|
self._share_folder_fid = str(item["fid"])
|
|
return self._share_folder_fid
|
|
result = self.mkdir("/来自:分享")
|
|
if self._api_success(result) and result.get("data", {}).get("fid"):
|
|
self._share_folder_fid = str(result["data"]["fid"])
|
|
return self._share_folder_fid
|
|
return None
|
|
|
|
def ls_dir(self, pdir_fid: str) -> Dict[str, Any]:
|
|
url = "https://pc-api.uc.cn/1/clouddrive/file/sort"
|
|
params = {"pr": "UCBrowser", "fr": "pc", "pdir_fid": pdir_fid or "0", "_page": 1, "_size": 50, "_fetch_total": 1, "_fetch_sub_dirs": 0, "_sort": "file_type:asc,updated_at:desc"}
|
|
return self._drive_api_json(self._get(url, params=params, headers=self._credential.get_headers()), context="列出网盘目录")
|
|
|
|
|
|
# ─── get_files / delete ────────────────────────────────────
|
|
|
|
def get_files(self, parent_fid: str = "0") -> List[FileInfo]:
|
|
"""列出 UC 网盘指定目录下的文件。
|
|
|
|
GET /1/clouddrive/file/sort?pdir_fid=<parent_fid>&_page=1&_size=100&_sort=updated_at:desc
|
|
|
|
Args:
|
|
parent_fid: 父目录 ID,默认 "0" 即根目录。
|
|
|
|
Returns:
|
|
FileInfo 列表。
|
|
"""
|
|
url: str = f"https://pc-api.uc.cn/1/clouddrive/file/sort"
|
|
params: Dict[str, str] = {
|
|
"pdir_fid": parent_fid,
|
|
"_page": "1",
|
|
"_size": "100",
|
|
"_sort": "updated_at:desc",
|
|
}
|
|
headers: Dict[str, str] = self._credential.get_headers()
|
|
|
|
try:
|
|
resp = self._get(url, params=params, headers=headers)
|
|
except Exception as exc:
|
|
raise TransferError(
|
|
TransferErrorCode.NETWORK_ERROR,
|
|
message=f"获取文件列表失败: {exc}",
|
|
platform=self.PLATFORM_KEY,
|
|
) from exc
|
|
|
|
data: Dict[str, Any] = resp.json()
|
|
status: int = data.get("status", -1)
|
|
if status != 0 and data.get("code") not in (0, None):
|
|
raise TransferError(
|
|
TransferErrorCode.NETWORK_ERROR,
|
|
message=f"获取文件列表失败: {data.get('message')}",
|
|
platform=self.PLATFORM_KEY,
|
|
)
|
|
|
|
files_data: List[Dict[str, Any]] = data.get("data", {}).get("list", [])
|
|
result: List[FileInfo] = []
|
|
for f in files_data:
|
|
result.append(
|
|
FileInfo(
|
|
fid=str(f.get("fid", "")),
|
|
name=str(f.get("file_name", f.get("name", ""))),
|
|
size=int(f.get("size", 0)),
|
|
is_dir=bool(f.get("dir", f.get("is_dir", False))),
|
|
ext=str(f.get("file_extension", f.get("ext", ""))),
|
|
)
|
|
)
|
|
|
|
logger.debug("[UcAdapter] Listed %d files in dir=%s", len(result), parent_fid)
|
|
return result
|
|
|
|
def delete(self, file_ids: List[str]) -> bool:
|
|
"""删除 UC 网盘文件(移到回收站)。
|
|
|
|
Args:
|
|
file_ids: 要删除的文件 ID 列表。
|
|
|
|
Returns:
|
|
True 表示删除成功。
|
|
"""
|
|
if not self._credential.validate():
|
|
raise TransferError(
|
|
TransferErrorCode.NOT_LOGIN,
|
|
platform=self.PLATFORM_KEY,
|
|
)
|
|
|
|
try:
|
|
return self._cleanup.delete_files(file_ids)
|
|
except RuntimeError as exc:
|
|
raise TransferError(
|
|
TransferErrorCode.NETWORK_ERROR,
|
|
message=str(exc),
|
|
platform=self.PLATFORM_KEY,
|
|
) from exc
|
|
|
|
def delete_permanent(self, file_ids: List[str]) -> bool:
|
|
"""彻底删除 UC 网盘文件(不可恢复)。
|
|
|
|
Args:
|
|
file_ids: 要彻底删除的文件 ID 列表。
|
|
|
|
Returns:
|
|
True 表示删除成功。
|
|
"""
|
|
if not self._credential.validate():
|
|
raise TransferError(
|
|
TransferErrorCode.NOT_LOGIN,
|
|
platform=self.PLATFORM_KEY,
|
|
)
|
|
|
|
try:
|
|
return self._cleanup.delete_files_permanent(file_ids)
|
|
except RuntimeError as exc:
|
|
raise TransferError(
|
|
TransferErrorCode.NETWORK_ERROR,
|
|
message=str(exc),
|
|
platform=self.PLATFORM_KEY,
|
|
) from exc
|
|
|
|
# ─── 工具方法 ─────────────────────────────────────────────
|
|
|
|
def _parse_share_url(self, url: str) -> Tuple[str, str]:
|
|
"""解析 UC 分享 URL 提取 (pwd_id, passcode)。
|
|
|
|
UC 链接格式:https://drive.uc.cn/s/<pwd_id> 或带 ?pwd=xxxx
|
|
|
|
Args:
|
|
url: UC 分享链接。
|
|
|
|
Returns:
|
|
(pwd_id, passcode) 元组。
|
|
|
|
Raises:
|
|
TransferError: URL 格式无法识别。
|
|
"""
|
|
pwd_id: Optional[str] = UcTransfer.parse_share_url(url)
|
|
if not pwd_id:
|
|
raise TransferError(
|
|
TransferErrorCode.URL_INVALID,
|
|
message=f"无法解析UC链接: {url}",
|
|
platform=self.PLATFORM_KEY,
|
|
)
|
|
|
|
parsed = urlparse(url)
|
|
params = parse_qs(parsed.query)
|
|
passcode: str = params.get("pwd", params.get("code", [""]))[0]
|
|
|
|
return pwd_id, passcode
|
|
|
|
def update_cookie(self, cookie: str) -> None:
|
|
"""动态更新 Cookie 并同步到 session headers。
|
|
|
|
Args:
|
|
cookie: 新的 Cookie 字符串。
|
|
"""
|
|
self._credential.update_cookie(cookie)
|
|
self._setup_session()
|
|
logger.info("[UcAdapter] Cookie updated, new length=%d", len(cookie))
|
|
|
|
def close(self) -> None:
|
|
"""关闭所有子模块的 HTTP 会话。"""
|
|
self._transfer_engine.close()
|