feat: integrate Quark and UC drive APIs
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.
This commit is contained in:
@@ -55,19 +55,21 @@ class QuarkAdapter(BaseCloudDriveAdapter):
|
||||
r"pan\.quark\.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": True,
|
||||
"share_staging_folder": False,
|
||||
}
|
||||
|
||||
def __init__(self, config: PlatformConfig, transfer_config: TransferConfig) -> None:
|
||||
"""初始化夸克适配器。
|
||||
|
||||
Args:
|
||||
config: 平台配置(含 Cookie 等)。
|
||||
transfer_config: 全局转存配置(超时、重试、轮询参数等)。
|
||||
"""
|
||||
super().__init__(config, transfer_config)
|
||||
|
||||
# 初始化三个子模块
|
||||
self._credential: QuarkCredentialManager = QuarkCredentialManager(
|
||||
cookie=config.cookie
|
||||
)
|
||||
"""初始化适配器。"""
|
||||
self._credential: QuarkCredentialManager = QuarkCredentialManager(cookie=config.cookie)
|
||||
self._transfer_engine: QuarkTransfer = QuarkTransfer(
|
||||
credential=self._credential,
|
||||
timeout=transfer_config.request_timeout,
|
||||
@@ -78,6 +80,7 @@ class QuarkAdapter(BaseCloudDriveAdapter):
|
||||
credential=self._credential,
|
||||
timeout=transfer_config.request_timeout,
|
||||
)
|
||||
super().__init__(config, transfer_config)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 公开接口实现
|
||||
@@ -116,8 +119,9 @@ class QuarkAdapter(BaseCloudDriveAdapter):
|
||||
platform=self.PLATFORM_KEY,
|
||||
)
|
||||
|
||||
# 目标目录:默认根目录 "0"
|
||||
target_dir: str = save_dir or self.config.save_dir or "0"
|
||||
# 目标目录:支持 fid 或路径;路径会先创建/解析为 fid。
|
||||
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)
|
||||
|
||||
# 分享密码
|
||||
pwd: str = share_password or self.config.share_password or ""
|
||||
@@ -250,18 +254,13 @@ class QuarkAdapter(BaseCloudDriveAdapter):
|
||||
def _save_files(self, pwd_id: str, detail: dict, save_dir: str) -> List[str]:
|
||||
"""转存文件到自己的夸克网盘(基类 transfer() 流程中的步骤③④)。
|
||||
|
||||
Args:
|
||||
pwd_id: 分享 ID。
|
||||
detail: 分享详情(来自 _get_share_detail)。
|
||||
save_dir: 目标目录 ID。
|
||||
|
||||
Returns:
|
||||
转存后的新文件 ID 列表。
|
||||
save_dir 可传 fid 或路径;路径会先通过 ensure_dir 创建/解析为 fid。
|
||||
"""
|
||||
# 需要 stoken,从 detail 间接获取(重新请求)
|
||||
stoken: str = self._transfer_engine._get_stoken(pwd_id)
|
||||
target_fid = save_dir if save_dir and not str(save_dir).startswith("/") else self.ensure_dir(save_dir or "/")
|
||||
task_id: str = self._transfer_engine._init_save(
|
||||
pwd_id, stoken, detail, to_pdir_fid=save_dir
|
||||
pwd_id, stoken, detail, to_pdir_fid=target_fid
|
||||
)
|
||||
return self._transfer_engine._poll_save_task(task_id)
|
||||
|
||||
@@ -351,6 +350,160 @@ class QuarkAdapter(BaseCloudDriveAdapter):
|
||||
logger.warning("[QuarkAdapter] 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://drive-pc.quark.cn/1/clouddrive/file"
|
||||
params = {"pr": "ucpro", "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://drive-pc.quark.cn/1/clouddrive/file/rename"
|
||||
params = {"pr": "ucpro", "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://drive-pc.quark.cn/1/clouddrive/file/info/path_list"
|
||||
params = {"pr": "ucpro", "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://drive-pc.quark.cn/1/clouddrive/file/move"
|
||||
params = {"uc_param_str": "", "fr": "pc", "pr": "ucpro"}
|
||||
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": "ucpro",
|
||||
"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://drive-pc.quark.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 recycle_list(self, page: int = 1, size: int = 30) -> List[Dict[str, Any]]:
|
||||
url = "https://drive-pc.quark.cn/1/clouddrive/file/recycle/list"
|
||||
params = {"_page": page, "_size": size, "pr": "ucpro", "fr": "pc", "uc_param_str": ""}
|
||||
data = self._drive_api_json(self._get(url, params=params, headers=self._credential.get_headers()), context="列出回收站")
|
||||
return data.get("data", {}).get("list", [])
|
||||
|
||||
def recycle_remove(self, record_list: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
url = "https://drive-pc.quark.cn/1/clouddrive/file/recycle/remove"
|
||||
params = {"uc_param_str": "", "fr": "pc", "pr": "ucpro"}
|
||||
payload = {"select_mode": 2, "record_list": record_list}
|
||||
return self._drive_api_json(self._post(url, json_data=payload, params=params, headers=self._credential.get_headers()), context="写入网盘目录")
|
||||
|
||||
def cleanup_recycle(self, fids: List[str]) -> Dict[str, Any]:
|
||||
target_fids = {str(fid) for fid in fids if fid}
|
||||
if not target_fids:
|
||||
return {"code": 0, "message": "无回收站记录需要清理", "data": {"removed": 0}}
|
||||
records = [
|
||||
item for item in self.recycle_list()
|
||||
if str(item.get("fid") or item.get("file_id") or "") in target_fids
|
||||
]
|
||||
if not records:
|
||||
return {"code": 0, "message": "未找到匹配的回收站记录", "data": {"removed": 0}}
|
||||
result = self.recycle_remove(records)
|
||||
if self._api_success(result):
|
||||
result.setdefault("data", {})["removed"] = len(records)
|
||||
return result
|
||||
|
||||
# ─── get_files / delete ────────────────────────────────────
|
||||
|
||||
def get_files(self, parent_fid: str = "0") -> List[FileInfo]:
|
||||
|
||||
Reference in New Issue
Block a user