From 4053b0a4f5a3e8d26bd39c5683284838aab65d2f Mon Sep 17 00:00:00 2001 From: baoqing <919836565@qq.com> Date: Wed, 13 May 2026 00:19:55 +0800 Subject: [PATCH] feat: add folder upload & make ls auto-refreshing --- README.md | 7 +- pan123_cli.py | 20 ++++- pan123_core.py | 210 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 230 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1036e62..e874b45 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,8 @@ | 方法名 | 参数说明 | 返回值类型 | 功能描述 | |-------------------------------------------------------------------------|---------------------------------------------------------------------------------|--------|-----------------| -| `upload_file(file_path, duplicate=0, on_progress=None)` | `file_path`: 本地文件路径
`duplicate`: 冲突策略(0=报错,1=覆盖,2=保留)
`on_progress`: 进度回调 | Result | 上传文件(支持秒传和分块上传) | +| `upload_file(file_path, duplicate=0, on_progress=None)` | `file_path`: 本地文件或文件夹路径
`duplicate`: 冲突策略(0=报错,1=覆盖,2=保留)
`on_progress`: 进度回调 | Result | 上传文件;传入文件夹时递归上传 | +| `upload_directory(dir_path, duplicate=0, on_progress=None)` | `dir_path`: 本地文件夹路径
其他参数同上 | Result | 上传文件夹(保留根目录结构) | | `get_download_url(index)` | `index`: `file_list` 中的目标文件下标 | Result | 获取文件直链(自动处理重定向) | | `share(file_ids, share_pwd="", expiration="2099-12-12T08:00:00+08:00")` | `file_ids`: 文件 ID 列表
`share_pwd`: 提取码
`expiration`: 过期时间 | Result | 创建分享链接 | @@ -256,7 +257,7 @@ | 方法名 | 参数说明 | 返回值类型 | 功能描述 | |---------------------------------------------------------|----------------------------|--------|-------------------| -| `upload_file(file_path, duplicate=0, on_progress=None)` | 同 `Pan123Core.upload_file` | Result | 上传文件(与 Core 方法一致) | +| `upload_file(file_path, duplicate=0, on_progress=None)` | 同 `Pan123Core.upload_file` | Result | 上传文件或文件夹(与 Core 方法一致) | --- @@ -372,4 +373,4 @@ result = core.share( # 5、免责声明 本工具用于学习场景,请勿用于违法用途。对任何滥用造成的后果,作者概不负责。 -任何未经允许的api调用都是不被官方允许的,对于因此产生的账号风险、数据损失等后果自负。 \ No newline at end of file +任何未经允许的api调用都是不被官方允许的,对于因此产生的账号风险、数据损失等后果自负。 diff --git a/pan123_cli.py b/pan123_cli.py index 6c30bec..c014322 100644 --- a/pan123_cli.py +++ b/pan123_cli.py @@ -35,7 +35,7 @@ class Pan123CLI: ls - 显示当前目录 cd [编号|..|/] - 切换目录 mkdir [名称] - 创建目录 - upload [路径] - 上传文件 + upload [路径] - 上传文件或文件夹 rm [编号] - 删除文件 share [编号 ...] - 创建分享 link [编号] - 获取文件直链 @@ -152,7 +152,8 @@ class Pan123CLI: arg = parts[1].strip() if len(parts) > 1 else "" handler = { - "ls": lambda: self._show_files(), + #"ls": lambda: self._show_files(), + "ls": lambda: self._do_refresh(), # 用户希望ls是列出目前文件,故直接刷新并列出 "login": lambda: self._do_login(), "logout": lambda: self._do_logout(), "clearaccount": lambda: self._do_clear_account(), @@ -442,6 +443,21 @@ class Pan123CLI: @staticmethod def _upload_progress(data) -> None: + uploaded_total = data.get("uploaded_total") + total_size = data.get("total_size") + if uploaded_total is not None and total_size is not None: + pct = data.get("percent", uploaded_total / total_size * 100 if total_size else 100) + file_index = data.get("file_index", 0) + file_count = data.get("file_count", 0) + file_name = data.get("file_name", "") + print( + f"\r上传进度: {pct:.1f}% | {format_size(uploaded_total)}/{format_size(total_size)} | " + f"{file_index}/{file_count} {file_name}", + end=" ", + flush=True, + ) + return + uploaded = data.get("uploaded", 0) total = data.get("total", 0) if total > 0: diff --git a/pan123_core.py b/pan123_core.py index fecbcf6..4e619db 100644 --- a/pan123_core.py +++ b/pan123_core.py @@ -1266,7 +1266,147 @@ class Pan123Core: if not os.path.exists(file_path): return make_result(-1, "文件不存在") if os.path.isdir(file_path): - return make_result(-1, "暂不支持文件夹上传") + return self.upload_directory(file_path, duplicate=duplicate, on_progress=on_progress) + + return self._upload_file_at(file_path, self.cwd_id, duplicate, on_progress) + + def upload_directory( + self, + dir_path: str, + duplicate: int = 0, + on_progress: ProgressCallback = None, + ) -> Dict[str, Any]: + """递归上传本地文件夹到当前目录,并保留本地根目录名。 + + Args: + dir_path: 本地目录路径。 + duplicate: 文件同名处理策略,同 upload_file。 + on_progress: 上传进度回调,除单文件进度外会附加目录整体进度字段。 + + Returns: + Result 字典:: + + 成功: {"code": 0, "message": "文件夹上传完成", "data": {...}} + 部分失败: {"code": -1, "message": "部分文件上传失败: ...", "data": {...}} + 失败: {"code": -1, "message": "...", "data": None} + """ + dir_path = dir_path.strip().replace('"', "") + if not os.path.exists(dir_path): + return make_result(-1, "目录不存在") + if not os.path.isdir(dir_path): + return make_result(-1, "不是文件夹") + + root_path = os.path.abspath(dir_path) + root_name = os.path.basename(os.path.normpath(root_path)) + if not root_name: + return make_result(-1, "目录名不能为空") + + total_size = 0 + file_count = 0 + dir_count = 1 + for current_dir, dir_names, file_names in os.walk(root_path): + dir_count += len(dir_names) + for file_name in file_names: + file_path = os.path.join(current_dir, file_name) + if os.path.isfile(file_path): + file_count += 1 + total_size += os.path.getsize(file_path) + + root_res = self._mkdir_at(root_name, self.cwd_id) + if root_res["code"] != CODE_OK: + return root_res + + root_remote_id = root_res["data"]["file_id"] + remote_dirs = {root_path: root_remote_id} + errors: List[str] = [] + uploaded_total = 0 + uploaded_files = 0 + + def emit_progress(file_path: str, uploaded_in_file: int = 0) -> None: + if not on_progress: + return + current_total = min(uploaded_total + uploaded_in_file, total_size) + percent = current_total / total_size * 100 if total_size else 100.0 + on_progress({ + "type": Pan123EventType.UPLOAD_PROGRESS, + "file_name": os.path.basename(file_path) if file_path else "", + "current_file": file_path, + "uploaded": uploaded_in_file, + "total": os.path.getsize(file_path) if file_path and os.path.isfile(file_path) else 0, + "uploaded_total": current_total, + "total_size": total_size, + "percent": percent, + "file_index": uploaded_files + (1 if uploaded_in_file > 0 else 0), + "file_count": file_count, + }) + + for current_dir, dir_names, file_names in os.walk(root_path): + parent_remote_id = remote_dirs.get(current_dir) + if parent_remote_id is None: + dir_names[:] = [] + continue + + for dir_name in list(dir_names): + local_dir = os.path.join(current_dir, dir_name) + sub_res = self._mkdir_at(dir_name, parent_remote_id) + if sub_res["code"] == CODE_OK: + remote_dirs[local_dir] = sub_res["data"]["file_id"] + else: + errors.append(f"{os.path.relpath(local_dir, root_path)}: {sub_res['message']}") + dir_names.remove(dir_name) + + for file_name in file_names: + local_file = os.path.join(current_dir, file_name) + if not os.path.isfile(local_file): + continue + + def file_progress(data: Dict[str, Any], current_file: str = local_file) -> None: + emit_progress(current_file, data.get("uploaded", 0)) + + sub = self._upload_file_at(local_file, parent_remote_id, duplicate, file_progress) + rel_path = os.path.relpath(local_file, root_path) + if sub["code"] == CODE_OK: + file_size = os.path.getsize(local_file) + uploaded_files += 1 + uploaded_total += file_size + emit_progress(local_file, 0) + else: + errors.append(f"{rel_path}: {sub['message']}") + + data = { + "path": root_path, + "file_id": root_remote_id, + "file_count": file_count, + "dir_count": dir_count, + "uploaded_files": uploaded_files, + "total_size": total_size, + "errors": errors, + } + if errors: + return make_result(-1, f"部分文件上传失败: {'; '.join(errors)}", data) + return make_result(CODE_OK, "文件夹上传完成", data) + + def upload_folder( + self, + dir_path: str, + duplicate: int = 0, + on_progress: ProgressCallback = None, + ) -> Dict[str, Any]: + """upload_directory 的别名。""" + return self.upload_directory(dir_path, duplicate=duplicate, on_progress=on_progress) + + def _upload_file_at( + self, + file_path: str, + parent_id: int, + duplicate: int = 0, + on_progress: ProgressCallback = None, + ) -> Dict[str, Any]: + """上传本地文件到指定网盘目录。""" + if not os.path.exists(file_path): + return make_result(-1, "文件不存在") + if os.path.isdir(file_path): + return make_result(-1, "不是文件") file_name = os.path.basename(file_path) file_size = os.path.getsize(file_path) @@ -1280,7 +1420,7 @@ class Pan123Core: "driveId": 0, "etag": md5, "fileName": file_name, - "parentFileId": self.cwd_id, + "parentFileId": parent_id, "size": file_size, "type": 0, "duplicate": duplicate, @@ -1307,6 +1447,70 @@ class Pan123Core: on_progress=on_progress, ) + def _mkdir_at(self, name: str, parent_id: int) -> Dict[str, Any]: + """在指定网盘目录下创建子目录,并返回新目录 FileId。""" + if not name: + return make_result(-1, "目录名不能为空") + payload = { + "driveId": 0, + "etag": "", + "fileName": name, + "parentFileId": parent_id, + "size": 0, + "type": 1, + "duplicate": 1, + "NotReuse": True, + "event": "newCreateFolder", + "operateType": 1, + } + r = self._request("POST", URL_MKDIR, json_data=payload) + if r["code"] != CODE_OK: + return make_result(r["code"], f"创建目录失败: {r['message']}", r["data"]) + + file_id = self._find_child_folder_id(parent_id, name) + if file_id is None: + file_id = self._extract_file_id(r["data"].get("data")) + if file_id is None: + return make_result(-1, "创建目录成功但未获取到目录 ID", r["data"]) + return make_result(CODE_OK, "ok", {"file_id": file_id, "response": r["data"]}) + + def _extract_file_id(self, data: Any) -> Optional[int]: + """从接口返回数据中尽量提取 FileId/fileId。""" + if isinstance(data, dict): + for key in ("FileId", "fileId", "FileID", "fileID"): + value = data.get(key) + if isinstance(value, int) and value > 0: + return value + if isinstance(value, str) and value.isdigit(): + file_id = int(value) + if file_id > 0: + return file_id + for value in data.values(): + file_id = self._extract_file_id(value) + if file_id is not None: + return file_id + elif isinstance(data, list): + for item in data: + file_id = self._extract_file_id(item) + if file_id is not None: + return file_id + return None + + def _find_child_folder_id(self, parent_id: int, name: str) -> Optional[int]: + """在父目录下按名称查找子目录 ID,作为创建目录响应缺少 ID 时的兜底。""" + for attempt in range(3): + r = self.list_dir_all(parent_id=parent_id) + if r["code"] == CODE_OK: + matches = [ + item for item in r["data"]["items"] + if item.get("Type") == 1 and item.get("FileName") == name and item.get("FileId") is not None + ] + if matches: + return max(int(item["FileId"]) for item in matches) + if attempt < 2: + time.sleep(0.5) + return None + def _upload_chunks( self, file_path: str, @@ -1618,6 +1822,8 @@ class Pan123Tool: "total": total, "speed": speed, }) + # 换一下行 + print("") os.rename(temp_path, full_path) return make_result(CODE_OK, "下载完成", {"path": full_path}) except Exception as e: