--- title: CloudDrive2 API 调用指南 date: 2026-08-01 tags: [clouddrive2, api, nas, grpc, webdav] --- # CloudDrive2 API 调用指南 ## 基本信息 - **服务器**:[[VPS/服务器清单|ZLSH_NAS]](`192.168.6.23`,公司白群晖 DS923+) - **CloudDrive2 版本**:v1.0.13(Synology 套件安装) - **安装路径**:`/volume1/@appstore/CloudDrive2/` - **数据路径**:`/volume1/@appdata/CloudDrive2/Config/` - **Web UI**:`http://192.168.6.23:19798` / `https://192.168.6.23:19799` - **API 类型**:gRPC-web(HTTP/1.1 承载的 gRPC) - **gRPC 服务名**:`clouddrive.CloudDriveFileSrv` ## 当前已挂载的云盘 | 云盘 | 路径 | 账号 | |------|------|------| | 115 网盘 | `/115open` | ID: 22499506 | | OneDrive | `/OneDrive` | zhensolid@vip.qq.com | | 阿里云盘Open | `/阿里云盘Open` | c370ce6b7e134a6785d51b7cecf31940 | | 天翼云盘 | `/{4130419372}` | 18184109687@189.cn | | 123 云盘 | `/{123-1821196620}` | — | ## API Token | 属性 | 值 | |------|-----| | Token | `9b194899-23fc-4db9-bebe-b2eba5a8f07a` | | 名称 | `hermes` | | 根目录 | `/` | | 过期 | 永不过期 | | 权限 | 全部 45 项 | --- ## 鉴权方式 ```http Authorization: Bearer 9b194899-23fc-4db9-bebe-b2eba5a8f07a ``` 错误码: - `grpc-status: 16` `Invalid auth token` — token 格式错误 - `grpc-status: 16` `No valid auth token` — 未提供 token --- ## gRPC-web 协议 ### 请求格式 ``` POST http://192.168.6.23:19798/clouddrive.CloudDriveFileSrv/<方法名> Content-Type: application/grpc-web+proto X-Grpc-Web: 1 Authorization: Bearer Body: gRPC-web 帧 ``` ### 帧结构 ``` 请求帧: | 1B flags(0x00) | 4B 大端长度 | protobuf 消息体 | 响应帧: | 1B flags | 4B 大端长度 | protobuf 消息体 | ``` **响应帧 flags 含义**: - `0x00` = 普通数据帧 - `0x80` = 最后一帧,包含 gRPC trailer(grpc-status 等)在 protobuf 数据之后 ### gRPC 状态码 | 码 | 含义 | |----|------| | 0 | OK | | 5 | NOT_FOUND | | 7 | PERMISSION_DENIED | | 12 | UNIMPLEMENTED | | 13 | INTERNAL | | 16 | UNAUTHENTICATED | --- ## ✅ 完整 Python 客户端(已验证可用) 以下客户端已通过实测,支持列出任意云盘目录。 ```python #!/usr/bin/env python3 """ CloudDrive2 gRPC-web API 客户端 依赖: 无(仅标准库) 用法: python3 clouddrive_client.py """ import struct import subprocess import tempfile import os import re BASE = "http://192.168.6.23:19798" SVC = "clouddrive.CloudDriveFileSrv" TOKEN = "9b194899-23fc-4db9-bebe-b2eba5a8f07a" def make_frame(proto_body: bytes) -> bytes: """构建 gRPC-web 请求帧""" return b'\x00' + struct.pack('>I', len(proto_body)) + proto_body def call(method: str, proto_body: bytes = b"") -> bytes: """ 调用 gRPC 方法,返回纯 protobuf 响应体(已跳过帧头和 trailer) 抛出 RuntimeError 如果 grpc-status != 0 """ data = make_frame(proto_body) with tempfile.NamedTemporaryFile(delete=False) as f: f.write(data) tmp = f.name try: result = subprocess.run([ 'curl', '-s', '--connect-timeout', '15', '-X', 'POST', f'{BASE}/{SVC}/{method}', '-H', 'Content-Type: application/grpc-web+proto', '-H', 'X-Grpc-Web: 1', '-H', f'Authorization: Bearer {TOKEN}', '--data-binary', f'@{tmp}', '-o', '/tmp/grpc_body.bin', '-D', '/tmp/grpc_hdr.txt' ], capture_output=True, text=True, timeout=20) with open('/tmp/grpc_body.bin', 'rb') as fb: raw = fb.read() # 解析 gRPC-web 响应帧 bodies = [] i = 0 grpc_status = None while i + 5 <= len(raw): flags = raw[i]; i += 1 msg_len = struct.unpack('>I', raw[i:i+4])[0]; i += 4 if i + msg_len > len(raw): break chunk = raw[i:i+msg_len] i += msg_len if flags & 0x80: # trailer 帧:解析 grpc-status for line in chunk.decode('utf-8', errors='replace').split('\r\n'): if line.startswith('grpc-status:'): grpc_status = int(line.split(':')[1].strip()) else: bodies.append(chunk) if grpc_status and grpc_status != 0: raise RuntimeError(f"gRPC error: status={grpc_status}") return b"".join(bodies) finally: os.unlink(tmp) def encode_path(path: str) -> bytes: """编码 path 字段(field 1, wire type 2 = string)""" data = path.encode('utf-8') return bytes([0x0a, len(data)]) + data def extract_strings(body: bytes, min_len: int = 3) -> list[str]: """从 protobuf 中提取所有可读 ASCII 字符串""" result = [] current = b"" for byte in body: if 32 <= byte < 127: current += bytes([byte]) else: if len(current) >= min_len: result.append(current.decode('ascii', errors='replace')) current = b"" if len(current) >= min_len: result.append(current.decode('ascii', errors='replace')) return result def list_dir(path: str) -> list[str]: """ 列出目录内容,返回文件名列表。 自动过滤系统噪音(ID、路径、URL 等)。 """ body = call("GetSubFiles", encode_path(path)) strings = extract_strings(body, min_len=3) skip = ['grpc-status', 'token=', 'https://', '/static/', 'CloudDrive', '
', '115open', 'OneDrive', 'Open'] seen = set() names = [] for s in strings: if any(k in s for k in skip): continue if re.match(r'^\d{10,}$', s): # 纯数字 ID continue if re.match(r'^[A-F0-9]{25,}$', s): # 哈希值 continue if s.startswith('/') or s.startswith('b/'): continue if '@' in s: continue name = s.strip() if name and name not in seen and len(name) > 1: seen.add(name) names.append(name) return names # ========== 使用示例 ========== if __name__ == '__main__': # 列出 115 网盘根目录 print("📂 115 网盘:") for name in list_dir("/115open"): print(f" • {name}") print() print("📂 OneDrive:") for name in list_dir("/OneDrive"): print(f" • {name}") print() print("📂 阿里云盘:") for name in list_dir("/阿里云盘Open"): print(f" • {name}") ``` ### 运行 ```bash python3 clouddrive_client.py ``` 不需要安装任何第三方依赖(纯标准库 + curl)。 --- ## ✅ 已验证的 gRPC 方法 ### GetSubFiles — 列出目录 **请求**:`{path: "/115open"}` (field 1, string) ```python body = call("GetSubFiles", encode_path("/115open")) ``` **实测路径**: | 路径 | 内容 | |------|------| | `/` | 列出所有挂载的云盘 | | `/115open` | 115 网盘根目录(含 Atomic Heart、PRAGMATA 等) | | `/OneDrive` | OneDrive 目录(DOS、KaliLinux、PythonStudy、SQL 等) | | `/阿里云盘Open` | 阿里云盘根目录 | ### GetApiTokenInfo — 获取 token 信息 **请求**:`{token: "9b194899-..."}` (field 1, string) ```python req = bytes([0x0a, 36]) + TOKEN.encode() body = call("GetApiTokenInfo", req) # 返回 token 元数据(根目录、权限等) ``` ### GetMountPoints — 获取挂载点列表 ```python body = call("GetMountPoints") # 返回本地挂载路径信息 ``` ### GetSpaceInfo — 空间使用情况 **请求**:`{path: "/115open"}` ```python body = call("GetSpaceInfo", encode_path("/115open")) ``` --- ## 常用 gRPC 方法速查 > ⚠️ 方法名从 CloudDrive2 二进制中提取,注意不要用 `GetSubFilesGetSubFiles` 这种重复名(grep 拼接产物),实际方法名是 `GetSubFiles`。 ### 文件操作 | 方法名 | 功能 | 参数 | |--------|------|------| | `GetSubFiles` ✅ | 列出子目录 | path (field 1, string) | | `CreateFolder` | 创建文件夹 | path | | `CreateFile` | 创建文件 | path, content | | `RenameFile` | 重命名 | old_path, new_path | | `DeleteFile` | 删除(回收站) | path | | `DeleteFilePermanently` | 永久删除 | path | | `CopyFile` | 复制 | src, dst | | `MoveFile` | 移动 | src, dst | | `GetSearchResults` | 搜索 | query, path | | `GetFileDetailProperties` | 文件详情 | path | ### 下载/上传 | 方法名 | 功能 | |--------|------| | `GetDownloadUrlPath` | 获取下载直链 | | `WriteToFile` | 写入文件 | | `GetDownloadFileList` | 下载任务列表 | | `GetUploadFileList` | 上传任务列表 | ### 空间/系统 | 方法名 | 功能 | |--------|------| | `GetSpaceInfo` ✅ | 空间使用情况 | | `GetRuntimeInfo` | 运行时信息 | | `GetSystemInfo` | 系统信息 | | `GetSystemSettings` | 系统设置 | | `SetSystemSettings` | 修改设置 | ### Token 管理 | 方法名 | 功能 | |--------|------| | `GetApiTokenInfo` ✅ | 查询 token 信息 | | `ListTokens` | 列出所有 token | | `CreateToken` | 创建 token | | `RemoveToken` | 删除 token | | `ModifyToken` | 修改 token 权限 | ### 挂载点 | 方法名 | 功能 | |--------|------| | `GetMountPoints` ✅ | 列出挂载点 | | `AddMountPoint` | 添加挂载点 | | `RemoveMountPoint` | 移除挂载点 | | `Mount` / `Unmount` | 挂载/卸载 | ### 离线下载 | 方法名 | 功能 | |--------|------| | `AddOfflineFiles` | 添加离线下载 | | `ListOfflineFilesByPath` | 按路径列离线任务 | | `ListAllOfflineFiles` | 所有离线任务 | | `RemoveOfflineFiles` | 删除离线任务 | --- ## Protobuf 编码速查 CloudDrive2 的请求参数简单,主要是 string 字段: ```python # field N, string value def encode_string(field_num: int, value: str) -> bytes: tag = (field_num << 3) | 2 # wire type 2 = LEN data = value.encode('utf-8') return bytes([tag, len(data)]) + data # 常用: # path -> field 1: 0x0a + len + path_bytes # token -> field 1: 0x0a + len + token_bytes ``` **实测参数映射**(通过交叉验证推测): | 方法 | 参数 | 编码 | |------|------|------| | `GetSubFiles` | path (field 1) | `0x0a` + len + path | | `GetApiTokenInfo` | token (field 1) | `0x0a` + len + token | | `GetSpaceInfo` | path (field 1) | `0x0a` + len + path | | `CreateFolder` | path (field 1) | `0x0a` + len + path | --- ## 踩坑记录 ### 1. 方法名不要用 grep 拼接结果 从二进制 `strings` 提取时,`GetSubFilesGetSubFiles` 是 grep 把相邻同名方法串在一起。实际方法名是 `GetSubFiles`。 ### 2. gRPC-web response trailer 解析 CloudDrive2 的 gRPC 响应可能把 `grpc-status` 放在最后的 trailer 帧中(flags=`0x80`),而不是 HTTP header。需要解析 trailer 帧才能拿到正确的状态码。 ```python if flags & 0x80: # trailer 帧内容: "grpc-status:0\r\ngrpc-message:...\r\n" for line in chunk.decode('utf-8').split('\r\n'): if line.startswith('grpc-status:'): status = int(line.split(':')[1]) ``` ### 3. Python requests 库版本问题 Hermes venv 中的 Python 3.11 + requests 的 urllib3 有类型注解兼容性问题。**直接用 subprocess + curl** 绕过,零依赖。 ### 4. 路径编码 中文路径(如 `/阿里云盘Open`)直接用 UTF-8 编码即可,CloudDrive2 内部处理正确。 ### 5. 空目录返回 `GetSubFiles` 对空目录返回空 body(0 字节 protobuf),这是正常的。 --- ## WebDAV(替代方案) WebDAV 已启用但没有配置用户(`dav_users: []`)。 要在 Web UI 中添加 DAV 用户后使用: ```bash # macOS 挂载 mkdir -p /tmp/clouddrive mount_webdav http://192.168.6.23:19798/dav/ /tmp/clouddrive ``` --- ## 相关笔记 - [[家庭实验室/Docker服务/Docker-compose合集/Clouddrive2|CloudDrive2 Docker 部署]] - [[VPS/服务器清单|服务器清单]] - [[家庭实验室/群晖NAS/群晖密钥登录|群晖 SSH 密钥登录]] --- ## 更新日志 - **2026-08-01 v2**:添加完整 Python 客户端(已验证)、修复方法名、添加 trailer 解析、添加踩坑记录、添加全部 5 个云盘挂载信息 - **2026-08-01 v1**:初始版本