bdpan-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. bdpan_cli/__init__.py +15 -0
  2. bdpan_cli/_vendor/__init__.py +9 -0
  3. bdpan_cli/_vendor/openapi_client/__init__.py +27 -0
  4. bdpan_cli/_vendor/openapi_client/api/__init__.py +6 -0
  5. bdpan_cli/_vendor/openapi_client/api/auth_api.py +649 -0
  6. bdpan_cli/_vendor/openapi_client/api/fileinfo_api.py +737 -0
  7. bdpan_cli/_vendor/openapi_client/api/filemanager_api.py +669 -0
  8. bdpan_cli/_vendor/openapi_client/api/fileupload_api.py +599 -0
  9. bdpan_cli/_vendor/openapi_client/api/multimediafile_api.py +391 -0
  10. bdpan_cli/_vendor/openapi_client/api/userinfo_api.py +315 -0
  11. bdpan_cli/_vendor/openapi_client/api_client.py +864 -0
  12. bdpan_cli/_vendor/openapi_client/configuration.py +454 -0
  13. bdpan_cli/_vendor/openapi_client/exceptions.py +197 -0
  14. bdpan_cli/_vendor/openapi_client/model/__init__.py +5 -0
  15. bdpan_cli/_vendor/openapi_client/model/oauth_token_authorization_code_response.py +276 -0
  16. bdpan_cli/_vendor/openapi_client/model/oauth_token_device_code_response.py +276 -0
  17. bdpan_cli/_vendor/openapi_client/model/oauth_token_device_token_response.py +276 -0
  18. bdpan_cli/_vendor/openapi_client/model/oauth_token_refresh_token_response.py +276 -0
  19. bdpan_cli/_vendor/openapi_client/model/quotaresponse.py +276 -0
  20. bdpan_cli/_vendor/openapi_client/model/uinforesponse.py +284 -0
  21. bdpan_cli/_vendor/openapi_client/model_utils.py +2039 -0
  22. bdpan_cli/_vendor/openapi_client/rest.py +385 -0
  23. bdpan_cli/_version.py +3 -0
  24. bdpan_cli/account.py +78 -0
  25. bdpan_cli/apply_scrape_rename.py +99 -0
  26. bdpan_cli/auth.py +218 -0
  27. bdpan_cli/cli.py +1428 -0
  28. bdpan_cli/config.py +26 -0
  29. bdpan_cli/dedup.py +310 -0
  30. bdpan_cli/downloader.py +178 -0
  31. bdpan_cli/file_manager.py +389 -0
  32. bdpan_cli/foldering.py +181 -0
  33. bdpan_cli/logger.py +101 -0
  34. bdpan_cli/mcp_server.py +547 -0
  35. bdpan_cli/media_info.py +251 -0
  36. bdpan_cli/ops.py +173 -0
  37. bdpan_cli/organize.py +108 -0
  38. bdpan_cli/preview_scrape.py +182 -0
  39. bdpan_cli/scraper.py +505 -0
  40. bdpan_cli/uploader.py +391 -0
  41. bdpan_cli-0.1.0.dist-info/METADATA +167 -0
  42. bdpan_cli-0.1.0.dist-info/RECORD +44 -0
  43. bdpan_cli-0.1.0.dist-info/WHEEL +4 -0
  44. bdpan_cli-0.1.0.dist-info/entry_points.txt +5 -0
bdpan_cli/uploader.py ADDED
@@ -0,0 +1,391 @@
1
+ """文件上传客户端:预上传 → 分片上传 → 创建文件,支持断点续传。
2
+
3
+ 三阶段规格依据官方文档(更新时间以 openapi 文档站为准):
4
+ - 预上传 precreate:autoinit 固定为 1,block_list 为各分片 MD5(32 位小写)
5
+ JSON 数组;响应 block_list 为**需要上传的分片序号列表**(从 0 起)。
6
+ - 分片上传 superfile2:type 固定为 "tmpfile",partseq 从 0 起;非末片必须
7
+ 足 4MB(errno=31299),普通用户单文件上限 4GB。
8
+ - 创建文件 create:path/size/isdir/block_list/uploadid/rtype 须与预上传
9
+ 完全一致;rtype 0 冲突失败 / 1 冲突重命名 / 3 覆盖。
10
+
11
+ 断点续传说明(2026-10-07 真网盘实测):服务端**不按路径跨任务记忆分片**,
12
+ 换新 uploadid 重新预上传后缺片列表仍为全量;分片记忆绑定在 uploadid 上。
13
+ 故续传采用本地状态文件(记录 uploadid + 已成功分片),恢复时凭原 uploadid
14
+ 仅补传缺失分片;uploadid 失效或服务端校验不过时自动回退全新上传。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ import io
21
+ import json
22
+ import logging
23
+ import os
24
+ import time
25
+ import urllib.request
26
+ from pathlib import Path
27
+ from typing import Any, Callable
28
+
29
+ from bdpan_cli._vendor import openapi_client
30
+ from bdpan_cli._vendor.openapi_client.api.fileupload_api import FileuploadApi
31
+ from bdpan_cli._vendor.openapi_client.rest import ApiException
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ CHUNK_SIZE = 4 * 1024 * 1024 # 官方分片基准 4MB(普通用户非末片必须足 4MB)
36
+ _CHUNK_RETRY = 3 # 分片上传瞬时失败重试次数
37
+
38
+
39
+ class UploadError(RuntimeError):
40
+ """上传流程异常。"""
41
+
42
+
43
+ class _NamedBytesIO(io.BytesIO):
44
+ """SDK 多部件序列化要求文件对象带 .name 属性(取文件名与 MIME 推断)。"""
45
+
46
+ name = "blob"
47
+
48
+
49
+ class Uploader:
50
+ """网盘文件上传封装(三阶段,本地状态文件断点续传)。"""
51
+
52
+ def __init__(self, access_token: str, api_client: openapi_client.ApiClient | None = None) -> None:
53
+ self.access_token = access_token
54
+ self.api_client = api_client or openapi_client.ApiClient()
55
+ self._fileupload_api = FileuploadApi(self.api_client)
56
+
57
+ def locate_upload_host(self) -> str:
58
+ """动态探测最优机房上传 host,失败时回退至默认 d.pcs.baidu.com。"""
59
+ url = f"https://d.pcs.baidu.com/rest/2.0/pcs/file?method=locateupload&access_token={self.access_token}"
60
+ try:
61
+ req = urllib.request.Request(url, headers={"User-Agent": "pan.baidu.com"})
62
+ with urllib.request.urlopen(req, timeout=5) as resp:
63
+ data = json.loads(resp.read().decode("utf-8"))
64
+ servers = data.get("servers") or []
65
+ if servers and isinstance(servers, list):
66
+ host = servers[0].get("host")
67
+ if host:
68
+ return str(host)
69
+ except Exception as e:
70
+ logger.debug("动态机房探测失败,使用默认 host: %s", e)
71
+ return "d.pcs.baidu.com"
72
+
73
+ # ------------------------------------------------------------------
74
+ # 分片规划与哈希
75
+ # ------------------------------------------------------------------
76
+
77
+ @staticmethod
78
+ def _plan_chunks(size: int, chunk_size: int = CHUNK_SIZE) -> list[tuple[int, int]]:
79
+ """规划分片: 返回 (offset, length) 列表;≤chunk_size 单片。"""
80
+ if size <= chunk_size:
81
+ return [(0, size)]
82
+ return [(i, min(chunk_size, size - i)) for i in range(0, size, chunk_size)]
83
+
84
+ @staticmethod
85
+ def _hash_file(local_path: str, chunks: list[tuple[int, int]]) -> list[str]:
86
+ """逐片流式计算内容 MD5(32 位小写),不整读大文件进内存。"""
87
+ md5s: list[str] = []
88
+ with open(local_path, "rb") as f:
89
+ for offset, length in chunks:
90
+ f.seek(offset)
91
+ h = hashlib.md5()
92
+ remaining = length
93
+ while remaining > 0:
94
+ buf = f.read(min(1024 * 1024, remaining))
95
+ if not buf:
96
+ raise UploadError(f"本地文件读取提前结束 [{local_path}] @{offset}")
97
+ h.update(buf)
98
+ remaining -= len(buf)
99
+ md5s.append(h.hexdigest())
100
+ return md5s
101
+
102
+ # ------------------------------------------------------------------
103
+ # 三阶段流程
104
+ # ------------------------------------------------------------------
105
+
106
+ def upload(
107
+ self,
108
+ local_path: str,
109
+ remote_path: str,
110
+ rtype: int = 1,
111
+ progress: Callable[[dict[str, Any]], None] | None = None,
112
+ state_file: str | Path | None = None,
113
+ local_key: str | None = None,
114
+ ) -> dict[str, Any]:
115
+ """上传本地文件到网盘指定路径。
116
+
117
+ Args:
118
+ local_path: 本地文件路径(必须为已存在文件)。
119
+ remote_path: 网盘目标文件绝对路径。
120
+ rtype: 冲突策略,与预上传/创建两阶段保持一致
121
+ (1 冲突重命名,3 覆盖)。
122
+ progress: 进度回调,接收 phase=precreate/resume/chunk/create 事件。
123
+ state_file: 断点续传状态文件路径;None 为不持久化(单次完成,
124
+ 中断即从头再来)。
125
+ local_key: 状态键的本地侧标识(默认 local_path),用于同一本地
126
+ 文件分批传到多个远端路径时区分任务。
127
+
128
+ Returns:
129
+ 创建文件阶段的响应(含 path/size/fs_id 等)。
130
+ """
131
+ if not os.path.isfile(local_path):
132
+ raise UploadError(f"本地文件不存在 [{local_path}]")
133
+
134
+ key = f"{local_key or local_path}->{remote_path}"
135
+ size = os.path.getsize(local_path)
136
+ chunks = self._plan_chunks(size)
137
+ md5s = self._hash_file(local_path, chunks)
138
+
139
+ state = self._load_state(state_file, key) if state_file else None
140
+ if state and state.get("size") == size and state.get("md5s") == md5s:
141
+ try:
142
+ res = self._upload_resume(
143
+ state, chunks, local_path, remote_path, rtype, md5s, progress
144
+ )
145
+ self._clear_state(state_file, key)
146
+ return res
147
+ except UploadError as e:
148
+ logger.info("续传任务失效,回退全新上传: %s", e)
149
+ self._clear_state(state_file, key)
150
+
151
+ return self._upload_fresh(
152
+ local_path, remote_path, size, chunks, md5s, rtype, progress, state_file, key
153
+ )
154
+
155
+ def _upload_resume(
156
+ self,
157
+ state: dict[str, Any],
158
+ chunks: list[tuple[int, int]],
159
+ local_path: str,
160
+ remote_path: str,
161
+ rtype: int,
162
+ md5s: list[str],
163
+ progress: Callable[[dict[str, Any]], None] | None,
164
+ ) -> dict[str, Any]:
165
+ """凭本地状态(原 uploadid + 已传分片)续传。"""
166
+ uploadid = str(state["uploadid"])
167
+ uploaded: set[int] = set(state.get("uploaded") or [])
168
+ missing = [i for i in range(len(chunks)) if i not in uploaded]
169
+ if progress:
170
+ progress({"phase": "resume", "present": len(uploaded), "missing": len(missing)})
171
+ for seq in missing:
172
+ self._upload_chunk(uploadid, remote_path, seq, chunks, local_path, progress)
173
+ return self._create(remote_path, int(state["size"]), md5s, uploadid, rtype, progress)
174
+
175
+ def _upload_fresh(
176
+ self,
177
+ local_path: str,
178
+ remote_path: str,
179
+ size: int,
180
+ chunks: list[tuple[int, int]],
181
+ md5s: list[str],
182
+ rtype: int,
183
+ progress: Callable[[dict[str, Any]], None] | None,
184
+ state_file: str | Path | None,
185
+ key: str,
186
+ ) -> dict[str, Any]:
187
+ """全新三阶段上传;状态文件存在时边传边记账。"""
188
+ pre = self._precreate(remote_path, size, md5s, rtype)
189
+ if progress:
190
+ progress({"phase": "precreate"})
191
+ uploadid = str(pre["uploadid"])
192
+
193
+ # 极速秒传判定:云端命中相同特征文件,无需上传任何分片直接创建
194
+ if pre.get("return_type") == 2:
195
+ if progress:
196
+ progress({"phase": "fast_upload", "uploadid": uploadid})
197
+ res = self._create(remote_path, size, md5s, uploadid, rtype, progress)
198
+ self._clear_state(state_file, key)
199
+ return res
200
+
201
+ missing = [int(x) for x in pre.get("block_list") or []]
202
+ uploaded = [i for i in range(len(chunks)) if i not in missing]
203
+ self._save_state(
204
+ state_file, key, local_path, remote_path, size, uploadid, md5s, uploaded
205
+ )
206
+
207
+ for seq in missing:
208
+ self._upload_chunk(uploadid, remote_path, seq, chunks, local_path, progress)
209
+ uploaded.append(seq)
210
+ self._save_state(
211
+ state_file, key, local_path, remote_path, size, uploadid, md5s, uploaded
212
+ )
213
+
214
+ res = self._create(remote_path, size, md5s, uploadid, rtype, progress)
215
+ self._clear_state(state_file, key)
216
+ return res
217
+
218
+ # ------------------------------------------------------------------
219
+ # 状态文件(断点续传账本)
220
+ # ------------------------------------------------------------------
221
+
222
+ @staticmethod
223
+ def _load_state(state_file: str | Path | None, key: str) -> dict[str, Any] | None:
224
+ if not state_file:
225
+ return None
226
+ try:
227
+ with open(state_file, encoding="utf-8") as f:
228
+ book = json.load(f)
229
+ return book.get(key) if isinstance(book, dict) else None
230
+ except (OSError, json.JSONDecodeError) as e:
231
+ logger.warning("续传状态文件不可读,按全新上传处理 [%s]: %s", state_file, e)
232
+ return None
233
+
234
+ @staticmethod
235
+ def _save_state(
236
+ state_file: str | Path | None,
237
+ key: str,
238
+ local_path: str,
239
+ remote_path: str,
240
+ size: int,
241
+ uploadid: str,
242
+ md5s: list[str],
243
+ uploaded: list[int],
244
+ ) -> None:
245
+ if not state_file:
246
+ return
247
+ book: dict[str, Any] = {}
248
+ try:
249
+ with open(state_file, encoding="utf-8") as f:
250
+ loaded = json.load(f)
251
+ if isinstance(loaded, dict):
252
+ book = loaded
253
+ except (OSError, json.JSONDecodeError):
254
+ pass
255
+ book[key] = {
256
+ "local_path": local_path,
257
+ "remote_path": remote_path,
258
+ "size": size,
259
+ "uploadid": uploadid,
260
+ "md5s": md5s,
261
+ "uploaded": uploaded,
262
+ "updated_at": int(time.time()),
263
+ }
264
+ with open(state_file, "w", encoding="utf-8") as f:
265
+ json.dump(book, f, ensure_ascii=False, indent=2)
266
+
267
+ @staticmethod
268
+ def _clear_state(state_file: str | Path | None, key: str) -> None:
269
+ if not state_file:
270
+ return
271
+ try:
272
+ with open(state_file, encoding="utf-8") as f:
273
+ book = json.load(f)
274
+ if isinstance(book, dict) and book.pop(key, None) is not None:
275
+ if book:
276
+ with open(state_file, "w", encoding="utf-8") as f:
277
+ json.dump(book, f, ensure_ascii=False, indent=2)
278
+ else:
279
+ os.unlink(state_file)
280
+ except (OSError, json.JSONDecodeError):
281
+ pass
282
+
283
+ # ------------------------------------------------------------------
284
+ # 三阶段原语
285
+ # ------------------------------------------------------------------
286
+
287
+ def _precreate(
288
+ self,
289
+ remote_path: str,
290
+ size: int,
291
+ md5s: list[str],
292
+ rtype: int,
293
+ ) -> dict[str, Any]:
294
+ block_list = json.dumps(md5s, separators=(",", ":"))
295
+ try:
296
+ res = self._fileupload_api.xpanfileprecreate(
297
+ access_token=self.access_token,
298
+ path=remote_path,
299
+ isdir=0,
300
+ size=size,
301
+ autoinit=1,
302
+ block_list=block_list,
303
+ rtype=rtype,
304
+ )
305
+ except ApiException as e:
306
+ raise UploadError(f"预上传失败 [{remote_path}]: {e.status} {e.body}") from e
307
+ return self._check(res, f"预上传返回错误 [{remote_path}]")
308
+
309
+ def _upload_chunk(
310
+ self,
311
+ uploadid: str,
312
+ remote_path: str,
313
+ seq: int,
314
+ chunks: list[tuple[int, int]],
315
+ local_path: str,
316
+ progress: Callable[[dict[str, Any]], None] | None,
317
+ ) -> None:
318
+ offset, length = chunks[seq]
319
+ with open(local_path, "rb") as f:
320
+ f.seek(offset)
321
+ data = f.read(length)
322
+ if len(data) != length:
323
+ raise UploadError(f"本地文件读取提前结束 [{local_path}] @{offset}")
324
+
325
+ last_error: Exception | None = None
326
+ for attempt in range(1, _CHUNK_RETRY + 1):
327
+ try:
328
+ # SDK 的 file 参数要求带 .name 的 IOBase 文件对象
329
+ res = self._fileupload_api.pcssuperfile2(
330
+ access_token=self.access_token,
331
+ partseq=str(seq),
332
+ path=remote_path,
333
+ uploadid=uploadid,
334
+ type="tmpfile",
335
+ file=_NamedBytesIO(data),
336
+ )
337
+ self._check_chunk(res, f"分片上传返回错误 [{remote_path} #{seq}]")
338
+ if progress:
339
+ progress({"phase": "chunk", "seq": seq, "total": len(chunks)})
340
+ return
341
+ except (ApiException, UploadError) as e:
342
+ last_error = e
343
+ logger.warning("分片 %d 上传第 %d 次尝试失败: %s", seq, attempt, e)
344
+ if attempt < _CHUNK_RETRY:
345
+ time.sleep(1)
346
+ raise UploadError(f"分片上传失败 [{remote_path} #{seq}] 重试 {_CHUNK_RETRY} 次后放弃: {last_error}") from last_error
347
+
348
+ def _create(
349
+ self,
350
+ remote_path: str,
351
+ size: int,
352
+ md5s: list[str],
353
+ uploadid: str,
354
+ rtype: int,
355
+ progress: Callable[[dict[str, Any]], None] | None,
356
+ ) -> dict[str, Any]:
357
+ block_list = json.dumps(md5s, separators=(",", ":"))
358
+ try:
359
+ res = self._fileupload_api.xpanfilecreate(
360
+ access_token=self.access_token,
361
+ path=remote_path,
362
+ isdir=0,
363
+ size=size,
364
+ uploadid=uploadid,
365
+ block_list=block_list,
366
+ rtype=rtype,
367
+ )
368
+ except ApiException as e:
369
+ raise UploadError(f"创建文件失败 [{remote_path}]: {e.status} {e.body}") from e
370
+ if progress:
371
+ progress({"phase": "create"})
372
+ return self._check(res, f"创建文件返回错误 [{remote_path}]")
373
+
374
+ @staticmethod
375
+ def _check(res: Any, ctx: str) -> dict[str, Any]:
376
+ if not isinstance(res, dict) or res.get("errno") != 0:
377
+ detail = res if isinstance(res, dict) else repr(res)
378
+ errno = res.get("errno") if isinstance(res, dict) else "?"
379
+ raise UploadError(f"{ctx}: errno={errno} ({detail})")
380
+ return res
381
+
382
+ @staticmethod
383
+ def _check_chunk(res: Any, ctx: str) -> dict[str, Any]:
384
+ """分片上传响应校验。
385
+
386
+ 官方规格:成功响应仅含 md5/request_id,errno 字段失败时才返回,
387
+ 故"无 errno"视作成功(区别于预上传/创建恒带 errno 的形态)。
388
+ """
389
+ if isinstance(res, dict) and res.get("errno") not in (0, None):
390
+ raise UploadError(f"{ctx}: errno={res.get('errno')} ({res})")
391
+ return res
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.3
2
+ Name: bdpan-cli
3
+ Version: 0.1.0
4
+ Summary: 百度网盘工程化 CLI 管理工具
5
+ Keywords: baidu-pan,netdisk,cli,scraper,backup,cloud-storage
6
+ Author: CodingRookie98
7
+ Author-email: CodingRookie98 <520whjoker@gmail.com>
8
+ License: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: System :: Archiving
19
+ Classifier: Topic :: Utilities
20
+ Requires-Dist: python-dotenv>=1.2.3
21
+ Requires-Dist: urllib3>=1.25.3
22
+ Requires-Dist: python-dateutil>=2.9.0.post0
23
+ Requires-Python: >=3.12
24
+ Project-URL: Homepage, https://github.com/CodingRookie98/bdpan-cli
25
+ Project-URL: Repository, https://github.com/CodingRookie98/bdpan-cli
26
+ Project-URL: Documentation, https://github.com/CodingRookie98/bdpan-cli#readme
27
+ Description-Content-Type: text/markdown
28
+
29
+ # bdpan-cli
30
+
31
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
32
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
33
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
34
+
35
+ [中文](README.md) | [English](README_en.md)
36
+
37
+ 百度网盘工程化 CLI 管理工具。基于[百度网盘开放平台](https://pan.baidu.com/union/doc/)官方 SDK 与协议标准构建,提供文件管理、分片上传续传、冲突副本治理与影视媒体库规范化自动化刮削整理。
38
+
39
+ ---
40
+
41
+ ## 快速安装 (Installation)
42
+
43
+ ### 方式 1:uv tool 安装(推荐,隔离环境)
44
+ ```bash
45
+ uv tool install bdpan-cli
46
+ ```
47
+
48
+ ### 方式 2:即用即走(无需预先安装)
49
+ ```bash
50
+ uvx bdpan-cli --help
51
+ ```
52
+
53
+ ### 方式 3:传统 pip 安装
54
+ ```bash
55
+ pip install bdpan-cli
56
+ ```
57
+
58
+ ### 方式 4:跨平台单文件二进制(免 Python 运行时)
59
+ 前往 [GitHub Releases](https://github.com/CodingRookie98/bdpan-cli/releases) 直接下载对应平台单文件二进制可执行包,加入 `PATH` 即可运行:
60
+ - `bdpan-linux-amd64`
61
+ - `bdpan-linux-arm64`
62
+ - `bdpan-macos-arm64` (Apple Silicon)
63
+ - `bdpan-macos-x86_64` (Intel)
64
+ - `bdpan-windows-amd64.exe`
65
+
66
+ ---
67
+
68
+ ## 命令行使用 (CLI)
69
+
70
+ ```bash
71
+ # 查看帮助
72
+ bdpan --help
73
+
74
+ # 常用命令
75
+ bdpan ls /影视/电影
76
+ bdpan mkdir /新目录
77
+ bdpan mv <源路径>... <目标目录> # 多源时目标须为已存在目录(自动记录流水)
78
+ bdpan cp <源路径>... <目标目录|目标目录/新名> # 单源支持改名
79
+ bdpan rename <路径> <新名称> # 原地更名(自动记录流水)
80
+ bdpan rm <路径>... [-y]
81
+ bdpan quota [--check-expire] [--check-free] # 网盘容量
82
+ bdpan whoami # 账户信息
83
+ bdpan search <关键词> [--dir 目录] [--no-recursion]
84
+ bdpan tree <路径> [--depth N] [--index 索引.json] # 递归树+本地索引
85
+ bdpan upload <本地文件> <目标目录|目标目录/新名> [--overwrite] # 分片上传+断点续传(首片秒传+动态机房)
86
+ bdpan download <网盘文件>... [--dest 本地目录] [--overwrite] # 大文件断点续传下载
87
+ bdpan dlink <网盘文件>... [--aria2 | --curl] # 提取高速直链或导出下载脚本
88
+ bdpan dedup /影视/电影 --staging-dir /隔离区 # 治理 (1)/(2) 冲突副本
89
+ bdpan scrape foldering /影视/电影/unscraped # 媒体库电影散落文件建目录归集(默认方案 A,支持 --format 模板)
90
+ bdpan scrape preview /影视/电影/待刮削 # 刮削重命名预演(支持 --format 模板,生成对照表)
91
+ bdpan scrape apply preview.csv # 批量应用刮削重命名
92
+
93
+
94
+ # 安全运维体系与操作撤销 (Safety & Undo)
95
+ bdpan ops list [--limit 20] # 查看关键变更操作流水审计日志
96
+ bdpan ops show <op_id> # 查看某次操作的明细与映射清单
97
+ bdpan undo <op_id> [--dry-run] [-y] # 逆向撤销回滚指定批次操作(支持预演)
98
+
99
+ # 多账号认证与隔离 (Multi-Account Auth)
100
+ bdpan auth list # 查看本地保存的网盘账号及凭证有效性
101
+ bdpan auth switch <alias> # 切换当前默认活跃账号
102
+ bdpan auth login [--account <alias>] # 登录/重新授权指定账号别名
103
+ bdpan --account <alias> <subcommand> # 临时以指定账号执行单次命令(多租户并发隔离)
104
+
105
+ # 媒体可观测性与全局选项 (Media Insights & Global Options)
106
+ bdpan info /影视/电影/Inception.mkv # 探测视频精准分辨率(4K/1080P)、时长、HEVC编码与音轨
107
+ bdpan stats /影视/电影 [--depth N] # 递归聚合统计目录总容量、总文件数及媒体格式分类占比
108
+ bdpan -V / --version # 查看当前 CLI 工具版本号
109
+ bdpan --json <subcommand> # 所有核心查询命令支持标准化 JSON 信封输出(脚本化集成)
110
+ bdpan --debug <subcommand> # 开启详细诊断调试日志(输出至 stderr,双写至 ~/.config/bdpan/logs/)
111
+ bdpan -q <subcommand> # 静音非错误输出(仅保留 ERROR)
112
+
113
+
114
+ # AI 原生集成 (Model Context Protocol / MCP)
115
+ bdpan mcp # 启动标准 stdio MCP Server,供 Claude Code / Cursor / AI 智能体调用
116
+ ```
117
+
118
+ > **操作安全机制**:`mv` / `cp` 遵循严格目标判定;`rm` 默认交互确认,`-y` 跳过;网盘删除由服务端移入回收站。批量操作支持 `--dry-run` 预演;所有破坏性移动更名自动落盘流水审计,支持一键逆向回滚还原。
119
+
120
+ ---
121
+
122
+ ## 本地开发环境
123
+
124
+ ```bash
125
+ # 克隆仓库
126
+ git clone https://github.com/CodingRookie98/bdpan-cli.git
127
+ cd bdpan-cli
128
+
129
+ # 安装依赖
130
+ uv sync
131
+
132
+ # 本地调试运行
133
+ uv run bdpan --help
134
+
135
+ # 运行全量测试
136
+ PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 uv run pytest
137
+ ```
138
+
139
+ ## 应用凭证与用户数据
140
+
141
+ 1. 开发者应用密钥:复制模板并填入开放平台应用的 AppKey / AppSecret(`.env` 已被 git 忽略,不会提交):
142
+ ```bash
143
+ cp .env.example .env
144
+ ```
145
+ 2. 用户登录授权凭据:用户首次通过 OAuth 登录授权后,Token 自动保存在 `~/.config/bdpan/tokens.json`(受系统权限 `0o600` 保护)。
146
+ 3. 诊断调试日志:完整执行细节自动落盘双写至 `~/.config/bdpan/logs/bdpan.log`。
147
+
148
+ ---
149
+
150
+ ## 项目结构
151
+
152
+ ```
153
+ ├── docs/ # 治理文档(Diátaxis 四象限,见 docs/GOVERNANCE.md)
154
+ │ ├── index.md # 知识库总入口
155
+ │ ├── llms.txt # AI 智能体机器地图
156
+ │ ├── project/ # 演进路线图 (roadmap.md)、待办 (backlog.md)、计划 (plans/)
157
+ │ └── reference/api/ # SDK 接口参考(机器真理源)
158
+ ├── pyproject.toml # 标准 Python 项目配置与 CLI 命令行入口
159
+ ├── src/bdpan_cli/ # 核心业务代码与 CLI 子命令
160
+ │ └── _vendor/ # 内聚内嵌的 OpenAPI Client 依赖包(标准轮子直接打包)
161
+ ├── tests/ # 单元测试与回归套件 (180+ 项全绿)
162
+ └── .python-version # 3.12
163
+ ```
164
+
165
+ ## 贡献与开源
166
+
167
+ 详细贡献规范、测试门禁与提交信息格式见 [CONTRIBUTING.md](./CONTRIBUTING.md)。开源许可协议遵循 [MIT License](./LICENSE)。
@@ -0,0 +1,44 @@
1
+ bdpan_cli/__init__.py,sha256=43zVFN3Z9HqGu-GamN_qz97i61laImxOdQJQJofexzs,433
2
+ bdpan_cli/_vendor/__init__.py,sha256=NcVpi0ktLVqOMVf39mSkYOJrkG6-01OWbOPW6sAW6so,255
3
+ bdpan_cli/_vendor/openapi_client/__init__.py,sha256=mp15Wbp-0VGMrvg3kPcmdQeSO10UrvWems9Ggh1urH4,806
4
+ bdpan_cli/_vendor/openapi_client/api/__init__.py,sha256=wfB-C-yN5UCFDbXIJDO2ANz7VnaqdHWSw9mkGK56iE4,253
5
+ bdpan_cli/_vendor/openapi_client/api/auth_api.py,sha256=NU9glyl5Zfe6tkJQ-ft3ID8U9OpKxKpjDnvBnabRoHI,23724
6
+ bdpan_cli/_vendor/openapi_client/api/fileinfo_api.py,sha256=iZ2x3_wNpKTi-szs5xp-BL2KAPDRW6xqu89djuvi1mc,25884
7
+ bdpan_cli/_vendor/openapi_client/api/filemanager_api.py,sha256=lKNCR9Y6gi0xzRtG8Z9Z8wMWKqOHyDrpKxFPbz1ZMto,23633
8
+ bdpan_cli/_vendor/openapi_client/api/fileupload_api.py,sha256=FE34o08M0D7gfUnagY7pZr6r1J8iAE7RmS1gg0ZL9Ck,21449
9
+ bdpan_cli/_vendor/openapi_client/api/multimediafile_api.py,sha256=Le66HLWp4bm_EZ3oSQJ4YOHfFNnFqOPYL3yRS3ETdL0,13717
10
+ bdpan_cli/_vendor/openapi_client/api/userinfo_api.py,sha256=uv_woQ3QDA-Zvb9DyBcYkzY8FJgtcYvJWiF5pDYZMB8,11049
11
+ bdpan_cli/_vendor/openapi_client/api_client.py,sha256=d9H6Q1v_AcclE9kLBdinsB8iMQxFrYCzSiezhYW_InU,37645
12
+ bdpan_cli/_vendor/openapi_client/configuration.py,sha256=krMUom9Zt014nuY0bdQmrZ2KgOb4bNidDsbPRyYzHqE,16450
13
+ bdpan_cli/_vendor/openapi_client/exceptions.py,sha256=KVNRAl1loqB327yecI6O9qLVGCTR08QdV2AcpX3zrL0,5565
14
+ bdpan_cli/_vendor/openapi_client/model/__init__.py,sha256=N49d9K35V_Hd5lOHWcMeVRl0Iy_-L-03rZgfKXwlESM,348
15
+ bdpan_cli/_vendor/openapi_client/model/oauth_token_authorization_code_response.py,sha256=zYDlt4ggc_cNOcjT51Ma_DlF-cUQb6iqhGL6hvpMXMA,12166
16
+ bdpan_cli/_vendor/openapi_client/model/oauth_token_device_code_response.py,sha256=MtnlQzxdehg6k5yLnovDHFSi-2T_G8RI3fUpejmNYW8,12140
17
+ bdpan_cli/_vendor/openapi_client/model/oauth_token_device_token_response.py,sha256=nacbzBa8CQrJ_C8ECm9USVv5Mjut05Hr3ZX5h9YEX90,12148
18
+ bdpan_cli/_vendor/openapi_client/model/oauth_token_refresh_token_response.py,sha256=C8r0JWAnPGwsAz2PO13M5W86OltuGWhp3Zbklj0MghI,12151
19
+ bdpan_cli/_vendor/openapi_client/model/quotaresponse.py,sha256=zDos_i5lmzv6m3v29PE1fUJyg_qm4z0JoZcVkfEMtYY,11948
20
+ bdpan_cli/_vendor/openapi_client/model/uinforesponse.py,sha256=rzYlQH6o6JvJgc0x1iM57K-uUqNDrq5XYGM8zone3Y0,12406
21
+ bdpan_cli/_vendor/openapi_client/model_utils.py,sha256=eVnKEkzJvMKz7zHZV8gyYml5Vpx9KpGi3k9WAifNtgc,81921
22
+ bdpan_cli/_vendor/openapi_client/rest.py,sha256=c2uweRsBDAQT5W2ZjviVclNK1r3AYjPkpFDwA1Zi5Fg,14799
23
+ bdpan_cli/_version.py,sha256=YyZPgBPtX1TTvnWa3bqs1EWFiGX0OUBZTSh6_vaPx2A,63
24
+ bdpan_cli/account.py,sha256=Ex7L7DRZtZEG70nqiJHoDCzc843ODM_pcj7NLIo8xS8,3103
25
+ bdpan_cli/apply_scrape_rename.py,sha256=MjHPDIfKuxslkVa7QFhz_7sfLJTkx51zrkklpnrLrgo,3821
26
+ bdpan_cli/auth.py,sha256=-OgGKIKGPxExQOdTIfNsMnwo7xOjI41joiIZBhs0pRg,8875
27
+ bdpan_cli/cli.py,sha256=Z3x0oXWZAOrY3ZLzP_IdHMC8Up9EgBYMYmH3nBeLCGs,60653
28
+ bdpan_cli/config.py,sha256=iCOFZ-fFDICHiKc5Cq2toefZc6idL1kqgfZOKys-R5k,736
29
+ bdpan_cli/dedup.py,sha256=Eix5ALoY-AIdxkfBiiLM0YYL53BiFnOzammvTlP-Fgw,10982
30
+ bdpan_cli/downloader.py,sha256=mPb-FOnYMoSKwFfLYIp-2iYri1Jzb38ASat4g9M3tV0,6981
31
+ bdpan_cli/file_manager.py,sha256=1qb6qWg73vHvYSladiurKWKkwWlxBLCGV5Yep-MhSc8,15171
32
+ bdpan_cli/foldering.py,sha256=pgYVbKbhfu6Q-Y0EVlZIvv_iCkGdVmQh26TqnwGVmh0,6368
33
+ bdpan_cli/logger.py,sha256=jXZMgQ1aW4YG1N4aQiYzvGXgV8x4VeoqJFn0DAVdQL0,3663
34
+ bdpan_cli/mcp_server.py,sha256=cwO2Ipq3HE74--5HQGTaBOYjSE_pmzqGVbCtv3yx3wo,25086
35
+ bdpan_cli/media_info.py,sha256=1_cfJfXq_KxtWLFkAMS9SnmON_WUCq8bFyiL2g3dZeM,8604
36
+ bdpan_cli/ops.py,sha256=bPOB6xAgmCNr4NsT48236Lc-wdCDN6ZA_dvjdvKNaiE,6454
37
+ bdpan_cli/organize.py,sha256=U-5QtVm4nvI3dY9K84oy0rVMRIXL1mkmQ2pZktKv76o,4357
38
+ bdpan_cli/preview_scrape.py,sha256=FSFgiZZW5p4jhTPS_Z1aKcJeuxTAVZUtMxPeJ2FMeT0,6422
39
+ bdpan_cli/scraper.py,sha256=AVklD7_zZjop0b6CZhK660bFsal2IyZ36qYX3Ch_2f8,20089
40
+ bdpan_cli/uploader.py,sha256=usJJxTMrtmJyUzbktBjKfIoX_bZImx9GF5qCgkd_rc0,15739
41
+ bdpan_cli-0.1.0.dist-info/WHEEL,sha256=3NO1oYsF7mANFUWVdyEQLM-zmecczWWEmmF5rEbXrWk,80
42
+ bdpan_cli-0.1.0.dist-info/entry_points.txt,sha256=2lUDk2LxVidWUk1OKKrrwA0g9n2kQBSnLexFIvmg508,114
43
+ bdpan_cli-0.1.0.dist-info/METADATA,sha256=2apyne_uaMQhBGt7c56dlXIN31GxxY2wyxZzCZF0nsg,7605
44
+ bdpan_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.6
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ bd-net-disc-mgr = bdpan_cli.cli:main
3
+ bdpan = bdpan_cli.cli:main
4
+ bdpan-cli = bdpan_cli.cli:main
5
+