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: