烛龙智元 · 文件中转站 — API 接口使用说明
适用版本:filehub 2.1 · 线上地址
https://files.dragonai.tech本文面向需要用脚本/服务/AI agent 对接上传下载的开发者。浏览器端 UI 已内置全部能力,无需对接本文档。 想让 coding agent 直接代管上传下载? 用现成的 Agent SKILL(零依赖 CLI,已封装下面所有接口与鉴权握手),无需自己实现本文档。
1. 总体架构(对接前必读)
文件字节永远不经过后端。 后端只做三件事:鉴权、签发指向阿里云 OSS 的预签名 URL、维护对象列表/索引。真正的上传(PUT)和下载(GET)由客户端直接对 OSS 完成。
┌─────────┐ 1. 登录/签 URL (小流量 JSON) ┌──────────────┐
client │ 你的脚本 │ ───────────────────────────────▶ │ filehub 后端 │
└─────────┘ └──────────────┘
│ 2. 用拿到的预签名 URL 直接 PUT/GET 文件字节(大流量)
▼
┌──────────────────────────────────────┐
│ 阿里云 OSS (oss-cn-heyuan.aliyuncs.com) │
└──────────────────────────────────────┘
带来的几个对接要点: - 上传:先向后端要一个预签名 PUT URL,再把文件字节 PUT 到该 URL(目标主机是 OSS,不是 filehub)。 - 下载:请求后端的 /api/download,它返回 302 重定向到一个预签名 GET URL,跟随重定向即可从 OSS 直接下载(支持 HTTP Range 断点续下)。 - 不限带宽、不限大小,瓶颈是 OSS 而非这台服务器。
2. 认证 —— 口令即空间(password-as-namespace)
没有固定口令。 口令会确定性地映射到它专属的隔离空间:用同一口令登录就能看到同一批文件,换一个口令就是另一个(全新空)空间。要共享文件,上传方与下载方必须用同一口令。
口令安全要求(对所有登录统一强制):口令须至少 8 位,同时包含数字和至少 2 个字母,且不得出现 4 个及以上的连续数字或字母(如 1234、abcd,倒序 4321、dcba 同样不允许,字母不分大小写),也不得出现 4 个及以上连续相同的字符(如 1111、aaaa,任意字符,区分大小写)。任何不合规口令一律拒绝(400 weak_password),无老空间放行。
认证系统升级说明:早期弱口令的老空间已由管理员逐一重新分配了强口令(通过服务端别名
space_aliases.json将新口令映射到原有文件,不搬动数据)。老用户若原口令无法登录,请联系管理员获取该空间的新口令。
安全性 = 口令强度;空间不可枚举(口令经服务器密钥做 HMAC),但弱口令对应的空间也弱。口令打错不一定报错,可能静默进入另一个空空间——所以登录后请看响应里的
space指纹和isNew确认进对了地方。
所有写操作(POST)都必须带 CSRF 头 X-Requested-With。
| 约定 | 说明 |
|---|---|
| Base URL | https://files.dragonai.tech |
| 会话 | 登录成功后返回 Set-Cookie: fh_token=...(HttpOnly、Secure、SameSite=Lax、有效期 7 天)。后续请求带上该 Cookie。 |
| CSRF | 所有 POST /api/* 必须带请求头 X-Requested-With: XMLHttpRequest(值任意非空)。缺失返回 403。 |
| 登录限频 | 同一 IP 5 分钟内 40 次登录后返回 429,需稍后重试(放慢空间猜测)。 |
| 防撞库 | 同一 IP 连续 5 个不同的失败口令(弱口令被拒,或进入全新空空间)后锁定 60 秒(429 login_locked + Retry-After);此后每个新的失败口令再锁 60 秒;成功进入有文件的空间即重置。锁定期间的提交不被处理。 |
| 内容类型 | 请求体为 JSON 时带 Content-Type: application/json。 |
POST /api/login
请求体:{"password": "<口令>"}(口令须至少 8 位,同时包含数字和至少 2 个字母,且不含 4 位及以上连续数字/字母或连续相同字符;对所有登录统一强制) - 200 {"ok": true, "space": "1a2b3c4d", "isNew": false, "hint": "已进入既有空间。"} + 下发 Cookie - space:该空间的短指纹(非敏感,仅供「是不是上次那个空间」确认)。 - isNew:true 表示这是个全新空白空间(尚无文件)——若你预期这里有文件,多半是口令打错了。 - 400 {"code": "weak_password", "detail": "口令不符合安全要求:..."} 口令不合规,一律拒绝(无老空间放行);老用户原口令无法登录时 hint 提示联系管理员获取新口令。 - 400 {"code": "empty_password", "detail": "口令不能为空。..."} 口令为空 - 429 {"code": "login_locked", ...} + Retry-After:防撞库锁定(连续 5 个不同失败口令触发,每次继续失败再锁 60 秒),等待后重试 - 429 {"code": "rate_limited", ...} 普通登录限频(40 次/5 分钟)
POST /api/logout
清除会话 Cookie。200 {"ok": true}
GET /api/me
探测登录态。200 {"authed": true, "space": "1a2b3c4d", "isNew": false} 或 401(见 §3 的鉴权错误结构)。
GET /api/health
健康检查(免鉴权)。200 {"ok": true}
GET /api/info
能力发现(免鉴权)——让 agent 不读文档即可自配置:返回版本、鉴权模型、CSRF 头、分片/URL 有效期/限频等限制、错误契约。
{
"ok": true, "service": "filehub", "version": "3.2.0",
"authModel": "password-as-namespace", "csrfHeader": "X-Requested-With",
"passwordPolicy": { "minLength": 8, "minDigits": 1, "minLetters": 2, "forbidSequentialRunOf": 4, "forbidRepeatRunOf": 4, "appliesTo": "all-logins" },
"login": { "method": "POST", "endpoint": "/api/login", "body": {"password": "<口令>"}, "agentAction": "ask_user_for_password" },
"limits": { "multipartPartMinBytes": 8388608, "multipartMaxParts": 10000, "uploadUrlExpiresSec": 3600, "downloadUrlExpiresSec": 3600, "loginRateLimit": {"max": 40, "windowSec": 300}, "stuffingGuard": {"freeDistinctFailures": 5, "lockSeconds": 60} },
"errorContract": { "shape": {"detail":"string","code":"string?","hint":"string?"}, "authCodes": ["auth_required","session_expired","invalid_session"] },
"docs": "/docs/", "skill": "/skill/"
}
2.5 给 AI agent 的接入约定(鉴权握手)
本平台对所有「未带口令 / 会话失效」的请求返回结构化、可自纠的 401,让 agent 无需读文档就知道该做什么:
HTTP 401
{
"detail": "未提供访问口令(未登录)。",
"code": "auth_required",
"hint": "本平台「口令即空间」:请向用户索取访问口令,用它登录后即可读写对应的独立空间。",
"auth": {
"method": "POST", "endpoint": "/api/login",
"body": {"password": "<访问口令 / access password>"},
"note": "口令须至少 8 位,同时包含数字和至少 2 个字母,且不得出现 4 个及以上的连续数字或字母如 1234/abcd、或 4 个及以上连续相同字符如 1111/aaaa(对所有登录统一强制,无例外);同口令可重复访问,共享文件需双方用同一口令。老用户原口令不合规无法登录时请联系管理员获取新口令。",
"agentAction": "ask_user_for_password"
}
}
agent 推荐流程: 1. 先调一次接口(如 GET /api/me)。若收到 code: "auth_required"(或 session_expired / invalid_session), 2. 停下来向用户索取访问口令(不要自己编), 3. 用用户给的口令 POST /api/login,再用返回的 space / isNew 向用户确认进对了空间, 4. 继续正常的上传/下载/列举。
code取值:auth_required(从未登录)、session_expired(会话过期)、invalid_session(凭证无效)。三者都应触发「重新用口令登录」,只有auth_required通常意味着要先向用户要口令。
更省事的方式:直接用 Agent SKILL 里的 filehub.py,它已把这套握手封装好——口令缺失时退出码为 3 并打印 AUTH_REQUIRED,提示 agent 去要口令。
3. 通用约定
- 成功:HTTP 2xx + JSON。
- 失败:非 2xx +
{"detail": "错误描述"}。错误体里可能附带两个供机器消费的字段:code:机器可读错误码(如auth_required/session_expired/oss_NoSuchKey)。hint:给人/agent 的下一步建议(如 404 下载会提示「key 不存在,或口令对应的空间不对」)。
- 常见状态:
400参数错误 /401未登录(见 §2.5 结构化体)/403缺 CSRF 头 /404对象不存在 /409上传会话过期 /422请求体格式错 /429限频。
错误码速查(code 字段;每个都带可读 detail,多数带 hint):
| code | HTTP | 含义 / agent 应对 |
|---|---|---|
auth_required |
401 | 未登录 → 向用户要口令再 login(错误体带 auth 对象) |
session_expired |
401 | 会话过期 → 用同一口令重新 login |
invalid_session |
401 | 凭证无效 → 重新 login |
empty_password |
400 | 口令为空 → 提供非空口令 |
csrf_required |
403 | 缺 X-Requested-With 头 → 补上 |
invalid_request |
422 | 请求体字段/类型不符 → 看 errors 数组修正 |
rate_limited |
429 | 登录限频 → 读 Retry-After(秒)等待后重试 |
invalid_path / invalid_key / reserved_key / invalid_filename |
400 | 路径/key 非法(穿越、保留前缀、空) → 修正 |
invalid_part_number / invalid_hash |
400 | 分片号/哈希格式非法 |
missing_etag / no_parts |
400 | 合并分片参数缺失 |
upload_session_expired |
409 | uploadId 失效 → 重新 init(CLI 自动处理) |
hash_not_indexed / source_missing |
409 | 秒传不可用 → 走普通上传 |
object_not_found |
400 | 目标对象不存在 |
oss_<Code> |
4xx/5xx | 透传的 OSS 错误(如 oss_NoSuchKey),404 附 hint |
- key:对象在桶内的完整路径,如
报表/2026/Q2.xlsx。由folder + filename组合而成;禁止..路径穿越;.filehub/为系统保留前缀。 - 每一个 PUT 到 OSS 的请求,Content-Type 必须是
application/octet-stream,且要与签名时一致——这是 OSS v1 签名的硬性要求,不一致会返回403 SignatureDoesNotMatch。
4. 上传文件
客户端自行决定走哪条路:文件 ≤ 100MB 用单次 PUT;> 100MB 用分片(也可以一律走分片)。
4.1 小文件 — 单次预签名 PUT
第 1 步 向后端要 PUT URL:
POST /api/upload/put-url
{ "filename": "report.pdf", "size": 1048576, "folder": "报表/2026/" }
返回:
{ "key": "报表/2026/report.pdf", "url": "https://dragonai-filehub.oss-cn-heyuan.aliyuncs.com/...&Signature=..." }
folder可选(不传则上传到根目录),需以/结尾。
第 2 步 把整个文件 PUT 到该 url:
curl -X PUT "<url>" \
-H "Content-Type: application/octet-stream" \
--data-binary @report.pdf
# 期望 HTTP 200
第 3 步(可选,秒传索引) 见 §6,上传完成后登记内容哈希。
4.2 大文件 — 分片上传(multipart)
第 1 步 初始化:
POST /api/upload/init
{ "filename": "big.zip", "size": 1234567890, "folder": "" }
返回:
{ "key": "big.zip", "uploadId": "AF3...", "partSize": 8388608, "partCount": 148 }
partSize = max(8MB, ⌈size/9000⌉ 向上取整到 1MB);partCount = ⌈size/partSize⌉(≤ 10000)。客户端必须按返回的 partSize 切片。
第 2 步 批量取分片签名 URL(可分批,一批最多几百个):
POST /api/upload/sign-parts
{ "key": "big.zip", "uploadId": "AF3...", "partNumbers": [1, 2, 3, 4, 5] }
返回:
{ "urls": [ { "partNumber": 1, "url": "https://...partNumber=1&uploadId=AF3...&Signature=..." }, ... ] }
第 3 步 逐片(可并发)PUT 到 OSS,读取每片响应头里的 ETag:
# 第 n 片 = 文件第 [(n-1)*partSize, n*partSize) 字节
curl -X PUT "<part_url>" \
-H "Content-Type: application/octet-stream" \
--data-binary @part_n.bin -D - -o /dev/null | grep -i '^etag:'
# 响应头含: ETag: "E37530F3...." ← 记下来
第 4 步 合并:
POST /api/upload/complete
{
"key": "big.zip",
"uploadId": "AF3...",
"parts": [
{ "partNumber": 1, "etag": "E37530F387794404C7C2C867BA4C7638" },
{ "partNumber": 2, "etag": "1159C07ED1CF610FC244E5C1CCF92258" }
]
}
返回 {"ok": true, "key": "big.zip"}。 > etag 带不带外层引号都行,后端会去掉;parts 必须覆盖全部 1..partCount 且按 partNumber 升序。
放弃上传:POST /api/upload/abort { "key", "uploadId" } → {"ok": true}(释放未完成的分片)。
5. 断点续传
分片上传中断后,只要还持有 key 和 uploadId,即可向 OSS 查询已经成功收到的分片,只补传缺失的:
POST /api/upload/list-parts
{ "key": "big.zip", "uploadId": "AF3..." }
返回(含进度汇总,便于 agent 直接读 pct):
{
"key": "big.zip", "uploadId": "AF3...",
"parts": [ { "partNumber": 1, "etag": "E375...", "size": 8388608 }, { "partNumber": 2, "etag": "1159...", "size": 8388608 } ],
"partsUploaded": 2, "bytesUploaded": 16777216
}
- 续传逻辑:
待传分片 = 全部分片 − list-parts 返回的分片;补传完后用 list-parts 的 etag + 新传分片的 etag 一起complete。 409 {"detail": "upload session expired"}:uploadId已过期/被中止,需重新init。
GET /api/upload/uploads — 列出未完成的分片上传
枚举当前空间里所有在途/未完成的分片上传,供发现并续传或放弃中断的任务:
{ "uploads": [ { "key": "big.zip", "uploadId": "AF3...", "initiated": "2026-06-14T11:29:41+00:00" } ], "count": 1 }
拿到 key+uploadId 后:用 list-parts 查进度续传,或用 abort 放弃。
浏览器端 UI 会把
{key, uploadId, partSize, 已完成分片}持久化到 localStorage,刷新/断网后自动用本接口续传;filehub.pyCLI 把这些落到~/.filehub/uploads/,中断后重跑upload自动续传(见 Agent SKILL)。脚本自行对接时持久化这几个字段即可。
6. 秒传(相同内容免上传)
若桶里已存在相同内容的对象,可让 OSS 服务端复制到目标 key,0 字节上传。
内容哈希算法(务必一致,否则命不中):把文件按 8MB 分块,对每块取 SHA-256,将各块的 32 字节摘要按顺序拼接,再对拼接结果取一次 SHA-256,输出 hex(64 位小写)。
POST /api/upload/exists{ "hash": "<hex>", "size": 123 }→{ "exists": true, "key": "已有对象key", "size": 123 }或{ "exists": false }- 命中则
POST /api/upload/instant{ "hash", "filename", "folder?" }→{ "ok": true, "key": "目标key" }(服务端copy_object,瞬时完成) - 未命中则正常上传(§4),完成后
POST /api/upload/record-hash{ "hash", "key" }→{ "ok": true },把内容登记进索引,供后续秒传。
7. 列举 / 下载 / 预览 / 删除 / 新建文件夹
GET /api/files?prefix=<目录>&marker=<分页游标>
按目录层级列举(prefix 以 / 结尾,空为根)。
{
"prefix": "报表/",
"folders": ["报表/2026/"],
"files": [ { "key": "报表/汇总.xlsx", "name": "汇总.xlsx", "size": 20480, "lastModified": "2026-06-14T03:00:00+00:00", "type": "xlsx" } ],
"nextMarker": null
}
- 单页最多 200 项;
nextMarker非空时带着它再请求下一页。
GET /api/download?key=<key>
返回 302 重定向到带 attachment 的预签名 GET URL(文件名按 RFC5987 编码,支持中文)。跟随重定向即从 OSS 直接下载:
curl -L -b cookies.txt "https://files.dragonai.tech/api/download?key=报表/汇总.xlsx" -o 汇总.xlsx
GET /api/preview?key=<key>
同上,但 inline(浏览器内预览图片/PDF),不强制下载。
POST /api/delete
{ "keys": ["a.txt", "报表/旧.xlsx"] }
→ { "deleted": ["a.txt", "报表/旧.xlsx"] }(批量删除)。
POST /api/mkdir
{ "path": "新目录/子目录" }
→ { "ok": true, "key": "新目录/子目录/" }(创建空目录标记对象)。
7.5 文件分享链接(对外分发,免登录)
把单个文件生成一个免口令的下载链接发给客户。访问者只能看到/下载这一个文件,无法进入任何内部页面、无法看到其它文件或目录。前端「文件浏览器」每个文件旁的「复制链接」即调用本接口。
安全模型:分享令牌是一枚 SECRET_KEY 签名的 bearer 凭证,只编码 {命名空间, 该文件 key, 该文件内容 etag}。它只授权这一个对象——没有任何「令牌 + 任意 key」的接口,持有者既不能列目录也不能碰其它文件。令牌与登录 cookie 用不同 salt,互不可用。改令牌即失效(签名);删除/移动该文件即失效;用不同内容覆盖同名 key 也会失效(etag 绑定,防止旧链接指向新文件)。可选过期:配置 SHARE_EXPIRES(秒,0=永久)。公开读接口按 IP 限频(120 次/60 秒)。
POST /api/share(需登录 + CSRF)
{ "key": "报表/2026/Q2.xlsx" }
→ { "token": "<签名令牌>", "path": "/s/<令牌>", "name": "Q2.xlsx", "expiresSec": null } 完整链接 = https://files.dragonai.tech + path。只能分享具体文件(目录返回 400 invalid_key;文件不存在返回 404 object_not_found)。
GET /s/<token>(公开,浏览器)
返回一个独立的极简下载页 share.html:只展示该文件名/大小 + 下载按钮(图片内联预览、PDF 可新标签预览)。该页不含任何通往登录/文件浏览/文档/技能页的链接,文件名以 textContent 渲染(防 XSS)。
GET /api/share/info?token=<token>(公开)
→ { "name": "Q2.xlsx", "size": 20480, "type": "xlsx" }(Cache-Control: no-store)。
GET /api/share/download?token=<token>(公开)
→ 302 重定向到带 attachment 的预签名 OSS URL(直接从 OSS 下载,免登录)。
GET /api/share/preview?token=<token>(公开)
→ 同上但 inline(图片/PDF 内联)。
失效情形统一返回 404:invalid_share(令牌被改/截断)、share_gone(文件已删/已被改成别的内容)、share_expired(配了过期且已过期)。
8. 端点速查表
| 方法 | 路径 | 鉴权 | CSRF | 用途 |
|---|---|---|---|---|
| POST | /api/login |
— | ✓ | 登录,下发 Cookie |
| POST | /api/logout |
✓ | ✓ | 登出 |
| GET | /api/me |
✓ | — | 探测登录态 |
| GET | /api/health |
— | — | 健康检查 |
| GET | /api/info |
— | — | 能力发现(版本/鉴权模型/限制/错误契约) |
| POST | /api/upload/put-url |
✓ | ✓ | 小文件单 PUT 签名 |
| POST | /api/upload/init |
✓ | ✓ | 初始化分片上传 |
| POST | /api/upload/sign-parts |
✓ | ✓ | 批量签分片 URL |
| POST | /api/upload/list-parts |
✓ | ✓ | 查已传分片 + 进度汇总(断点续传) |
| GET | /api/upload/uploads |
✓ | — | 列出未完成的分片上传 |
| POST | /api/upload/complete |
✓ | ✓ | 合并分片 |
| POST | /api/upload/abort |
✓ | ✓ | 放弃分片上传 |
| POST | /api/upload/exists |
✓ | ✓ | 秒传:查内容是否已存在 |
| POST | /api/upload/instant |
✓ | ✓ | 秒传:服务端复制到目标 key |
| POST | /api/upload/record-hash |
✓ | ✓ | 秒传:登记内容哈希 |
| GET | /api/files |
✓ | — | 列举对象 |
| GET | /api/download |
✓ | — | 下载(302→OSS) |
| GET | /api/preview |
✓ | — | 预览(302→OSS) |
| POST | /api/delete |
✓ | ✓ | 批量删除 |
| POST | /api/mkdir |
✓ | ✓ | 新建文件夹 |
| POST | /api/share |
✓ | ✓ | 为单个文件生成免登录分享链接 |
| GET | /s/<token> |
— | — | 独立下载页(隔离,无内部入口) |
| GET | /api/share/info |
— | — | 分享文件的名称/大小(公开) |
| GET | /api/share/download |
— | — | 分享下载(302→OSS,公开) |
| GET | /api/share/preview |
— | — | 分享预览(302→OSS,公开) |
9. 限制与注意事项
- Content-Type:每个对 OSS 的 PUT 都必须发
application/octet-stream,与签名一致;否则 403。 - ETag:分片 PUT 的响应头
ETag必须取到并回传给complete(浏览器需 OSS CORS 暴露 ETag,本桶已配置)。 - 预签名 URL 有效期:上传/分片/预览 URL 约 1 小时;下载 URL 约 1 小时。大文件慢传中途某片 URL 过期,重新
sign-parts再传该片即可。 - 分片:单片最小 8MB,最多 10000 片(单文件理论上限约 48.8TB);务必按
init返回的partSize切片。 - 下载断点续传:由 OSS 的 HTTP Range 支持,用支持续传的下载器/浏览器即可。
- 文件名:支持中文/空格/括号;后端用 RFC5987 编码下载文件名。
10. 完整示例(Python · requests)
import requests, hashlib, math, os
BASE = "https://files.dragonai.tech"
PASSWORD = "你的口令"
HDR = {"X-Requested-With": "XMLHttpRequest"} # 所有 POST 必带
OCTET = {"Content-Type": "application/octet-stream"}
SINGLE_MAX = 100 * 1024 * 1024 # ≤100MB 走单 PUT
s = requests.Session()
s.post(f"{BASE}/api/login", json={"password": PASSWORD}, headers=HDR).raise_for_status()
def content_hash(path, chunk=8 * 1024 * 1024):
"""与系统一致的分块 SHA-256(用于秒传);文件大时可跳过秒传直接上传。"""
digs = []
with open(path, "rb") as f:
while (b := f.read(chunk)):
digs.append(hashlib.sha256(b).digest())
return hashlib.sha256(b"".join(digs)).hexdigest()
def upload(path, folder=""):
name, size = os.path.basename(path), os.path.getsize(path)
# —— 秒传尝试(可选)——
h = content_hash(path)
ex = s.post(f"{BASE}/api/upload/exists", json={"hash": h, "size": size}, headers=HDR).json()
if ex.get("exists"):
r = s.post(f"{BASE}/api/upload/instant",
json={"hash": h, "filename": name, "folder": folder}, headers=HDR).json()
print("秒传完成:", r["key"]); return r["key"]
if size <= SINGLE_MAX:
# —— 小文件单 PUT ——
r = s.post(f"{BASE}/api/upload/put-url",
json={"filename": name, "size": size, "folder": folder}, headers=HDR).json()
with open(path, "rb") as f:
requests.put(r["url"], data=f, headers=OCTET).raise_for_status()
key = r["key"]
else:
# —— 大文件分片 ——
r = s.post(f"{BASE}/api/upload/init",
json={"filename": name, "size": size, "folder": folder}, headers=HDR).json()
key, upload_id, ps, pc = r["key"], r["uploadId"], r["partSize"], r["partCount"]
urls = {u["partNumber"]: u["url"] for u in s.post(
f"{BASE}/api/upload/sign-parts",
json={"key": key, "uploadId": upload_id, "partNumbers": list(range(1, pc + 1))},
headers=HDR).json()["urls"]}
parts = []
with open(path, "rb") as f:
for n in range(1, pc + 1):
chunk = f.read(ps)
resp = requests.put(urls[n], data=chunk, headers=OCTET); resp.raise_for_status()
parts.append({"partNumber": n, "etag": resp.headers["ETag"]})
s.post(f"{BASE}/api/upload/complete",
json={"key": key, "uploadId": upload_id, "parts": parts}, headers=HDR).raise_for_status()
# 登记秒传索引(可选)
s.post(f"{BASE}/api/upload/record-hash", json={"hash": h, "key": key}, headers=HDR)
print("上传完成:", key); return key
def download(key, save_as):
# 跟随 302 直接从 OSS 下;requests 默认跟随重定向
with s.get(f"{BASE}/api/download", params={"key": key}, stream=True) as r:
r.raise_for_status()
with open(save_as, "wb") as f:
for c in r.iter_content(1024 * 1024):
f.write(c)
print("已下载:", save_as)
# 用法
key = upload("./report.pdf", folder="报表/2026/")
download(key, "./report_back.pdf")
实测:小文件与 116MB 分片文件上传 + 下载,md5 往返一致。
