Files
FileShare/server/web/dist/assets/docsSource-Df5ur5C4.js
T
SKYMirror 6f1a925833
Release 镜像 / 测试(推送前置门禁) (push) Failing after 12s
Release 镜像 / 多架构构建并推送 ACR (push) Skipped
26.9:品牌统一(fileshare)+ 版本号改为日期式
- 数据库默认文件 filecodebox.db → fileshare.db(config.go 默认值与全部文档/编排同步)
- Go module filecodebox → fileshare(全部 import 同步,build/vet/test 全绿)
- 应用版本 APP_VERSION 2.5.6 → 26.9(health 接口已验证返回 26.9)
- deploy 编排统一:compose 项目名、Postgres 默认凭据、minio 桶名、env 注释
- JWT issuer、存储临时目录前缀、web 包名同步 fileshare
- CI:镜像 tag 以 APP_VERSION 为唯一版本源,main/tag 推送即发布
  ${VER} + latest;tag 触发时校验 tag 名与 APP_VERSION 一致,防错版
- 本地开发库文件已改名 fileshare.db(含 -shm/-wal 清理)
2026-09-05 06:32:18 +08:00

2518 lines
158 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-BKnWAKao.js","assets/index-CtyCxWf5.css"])))=>i.map(i=>d[i]);
import{ay as i}from"./index-BKnWAKao.js";const p='# API 概述\n\n文件快传 Go 版(26.9)对外提供一套 REST API,覆盖文本/文件分享、分片上传、\n预签名直传、管理后台与审计日志查询。本文档与 `server/internal/api/` 实际实现逐一对齐,\n交互式规范见站内 `/openapi`(源文件 `docs/openapi.yaml`)。\n\n## Base URL\n\n- 服务默认监听 `:8466`Base URL 为 `http://<host>:8466`(下文示例统一用 `http://localhost:8466`)。\n- **业务路由挂根路径**(与参考实现一致):`/share/*`、`/chunk/*`、`/presign/*`、`/admin/*`、`/setup`。\n- 仅两个公共接口带 `/api/v1` 前缀:`/api/v1/health`、`/api/v1/config`。\n\n## 统一响应封装\n\n所有 JSON 接口返回统一结构,HTTP 状态码与 `code` 一致;失败时 `data` 缺省:\n\n```json\n{ "code": 200, "msg": "ok", "data": { } }\n```\n\n失败示例(404):\n\n```json\n{ "code": 404, "msg": "文件不存在" }\n```\n\n个别端点直接返回原始内容而非 JSON 封装(文档中已单独标注):\n\n| 端点 | 响应形式 |\n|---|---|\n| `GET /share/select?code=`(文本分享) | `text/plain; charset=utf-8` 正文 |\n| `GET /share/select?code=`(文件分享) | 文件二进制流(200/206,支持 Range |\n| `GET /share/download?key=&code=`(文件分享) | 文件二进制流(200/206,支持 Range |\n| `GET /admin/file/download?id=`(文件分享) | 文件二进制流 |\n| `GET /setup` / `POST /setup`(表单) | HTML 向导/成功页 |\n\n## 字段命名约定\n\n- 接口字段以 **snake_case** 为主(`file_code`、`size_bytes`)。\n- 文件列表与审计日志的行字段同时输出 **snake_case 与 camelCase 双份**(如 `expired_at` 与 `expiredAt`),文档以 snake_case 为准,camelCase 仅为前端兼容保留。\n\n## 认证\n\n- 游客接口无需认证;是否允许游客上传由配置 `openUpload` 控制(关闭时上传类接口要求管理员 `Authorization: Bearer <token>`,否则 403)。\n- 管理接口(`/admin/login` 除外)一律要求 `Authorization: Bearer <JWT>`,无效/缺失返回 401。\n- 详情见《认证与限流》。\n\n## 限流\n\n按 IP(可信代理场景解析 `X-Forwarded-For`)维度限流,超限返回 **423**:\n\n| 规则 | 计数时机 | 默认(次/窗口) | 相关配置 |\n|---|---|---|---|\n| `upload` | **上传成功**后计数 | 10 次 / 1 分钟 | `uploadCount` / `uploadMinute` |\n| `error` | 取件失败(404/过期)时计数 | 10 次 / 1 分钟 | `errorCount` / `errorMinute` |\n| `login` | 登录失败时计数 | 5 次 / 15 分钟 | `loginCount` / `loginMinute` |\n| `metadata` | 每次访问即计数 | 同 `error` | `errorCount` / `errorMinute` |\n\n## 初始化守卫\n\n系统未初始化(未设置管理员密码)时,除 `GET|POST /setup` 与 `GET /api/v1/health` 外,\n**所有接口一律返回 428**\n\n```json\n{ "code": 428, "msg": "系统未初始化,请先完成初始化" }\n```\n\n首次部署请先访问 `GET /setup` 获取 HTML 向导,或直接 `POST /setup` 完成初始化(见《管理后台 API》初始化章节)。\n\n## 审计\n\n所有上传/下载端点经审计中间件自动落库(操作时间/IP/UA/设备解析/动作/结果/字节数/耗时/角色),\n失败与被拒绝的请求同样记录;管理端经 `GET /admin/audit/list` 查询,详见《审计日志》。\n\n## 端点总览\n\n| 模块 | 端点 |\n|---|---|\n| 公共 | `GET /api/v1/health` · `GET /api/v1/config` · `GET /robots.txt`(输出 `robotsText` 配置) |\n| 初始化 | `GET /setup` · `POST /setup` |\n| 文本分享 | `POST /share/text` |\n| 文件分享 | `POST /share/file` |\n| 查询与取件 | `GET/POST /share/metadata` · `GET /share/select` · `POST /share/select` · `GET /share/download` |\n| 分片上传 | `POST /chunk/upload/init` · `POST /chunk/upload/{uploadID}/{chunkIndex}` · `GET /chunk/upload/status/{uploadID}` · `POST /chunk/upload/complete/{uploadID}` · `DELETE /chunk/upload/{uploadID}` |\n| 预签名直传 | `POST /presign/upload/init` · `PUT /presign/upload/proxy/{uploadID}` · `POST /presign/upload/confirm/{uploadID}` · `GET /presign/upload/status/{uploadID}` · `DELETE /presign/upload/{uploadID}` |\n| 管理后台 | `POST /admin/login` · `GET /admin/verify` · `POST /admin/logout` · `GET /admin/dashboard` · 文件管理 `/admin/file/*` · 配置 `/admin/config/*` · 密码 `/admin/settings/password` |\n| 审计日志 | `GET /admin/audit/list`(别名 `/admin/audit/logs` |\n\n## 时间与编码\n\n- 时间字段一律 RFC 3339(如 `2025-06-01T12:00:00+08:00`);管理员会话过期时间为 Unix 秒。\n- 请求体支持 `application/json` 与 `application/x-www-form-urlencoded`(上传类为 `multipart/form-data`),文档示例以 JSON/curl 为主。\n- CORS:公开接口放开(Bearer 认证,无 Cookie CSRF 面);**管理端 `/admin/*` 已收紧**——携带 Origin 且既不同源也不在 `site_domain` 白名单时不下发 CORS 头(浏览器拦截跨域读取)。\n\n## 交互式文档\n\n- 站内文档页:`/docs`(渲染本目录 markdown,构建时内嵌)。\n- Swagger UI`/openapi`(渲染 `docs/openapi.yaml`,构建时内嵌)。\n- OpenAPI 规范源文件为仓库内 `docs/openapi.yaml`;如需经后端直接下载,\n 需在部署时把它拷贝进前端静态产物 `web/dist/`(未拷贝时该路径按 SPA 回退返回页面)。\n',d='# 认证与限流\n\n## 角色\n\n| 角色 | 能力 |\n|---|---|\n| 游客(无 Authorization 头) | 取件、查询元信息;`openUpload=1` 时可上传 |\n| 管理员(`Authorization: Bearer <JWT>` | 全部能力 + `/admin/*` 管理接口 |\n\n## 管理员令牌\n\n- 由 `POST /admin/login` 用管理员密码换取,HS256 JWT,默认有效期 **7 天**`adminSessionExpire`1~365 整天,26.9 起由 30 天缩短)。\n- 请求头格式:`Authorization: Bearer <token>`。\n- **改密/重置管理员密码会轮换 `jwt_secret`,所有已签发令牌立即失效**(401)。\n- 密码存储为 bcryptcost 12);历史 `sha256$`/明文格式在登录成功后自动升级重哈希,无需手动迁移。\n- 游客上传关闭(`openUpload=0`)时,上传类接口也可用管理员 Bearer 令牌通过鉴权。\n\n## 认证失败语义\n\n| 场景 | 状态码 |\n|---|---|\n| `/admin/*` 缺失/无效令牌 | 401 |\n| `POST /admin/login` 密码错误 | 401(并计入 login 限流) |\n| 游客上传被关闭且未携带有效令牌 | 403 |\n| 代理下载 `key` 校验失败 | 403 |\n\n## 未初始化(428\n\n管理员密码未设置(`admin_token` 为空)时,除 `GET|POST /setup` 与 `GET /api/v1/health` 外全部接口返回 428。\n完成 `POST /setup` 初始化后自动解除。\n\n## 限流规则\n\n限流按 **客户端 IP** 维度(配置 `FCB_TRUSTED_PROXIES` 声明可信代理 CIDR,命中时解析 `X-Forwarded-For` 取真实 IP),\n窗口计数原子化存储于缓存(未配置 Redis 时为进程内存)。**超限一律返回 423**\n\n```json\n{ "code": 423, "msg": "请求次数过多,请稍后再试" }\n```\n\n| 规则 | 生效端点 | 计数时机 | 默认 | 配置键 |\n|---|---|---|---|---|\n| `upload` | `/share/text`、`/share/file`、`/chunk/upload/*`、`/presign/upload/*` | **成功后**计数(进入时仅检查) | 10 次 / 1 分钟 | `uploadCount`、`uploadMinute` |\n| `error` | `/share/select`、`/share/download` | 取件失败(不存在/过期/鉴权失败)时计数 | 10 次 / 1 分钟 | `errorCount`、`errorMinute` |\n| `login` | `/admin/login` | 登录失败时计数 | 5 次 / 15 分钟 | `loginCount`、`loginMinute` |\n| `metadata` | `/share/metadata` | **每次访问即计数**(含失败) | 同 `error` | `errorCount`、`errorMinute` |\n\n- 规则值可由管理端 `PATCH /admin/config/update` 运行时修改,立即生效(无需重启)。\n- 取件成功(`/share/select`、`/share/download`)不计入 `error` 限流。\n\n## 代理下载令牌(key\n\n`GET /share/download` 的 `key` 由服务端按窗口生成:\n`sha256(code + timeFactor + "000" + jwt_secret)``timeFactor = unix秒 / 1000`(约 16.7 分钟一个窗口)。\n服务端**同时接受当前与上一窗口**的令牌,避免窗口边界竞态。令牌通过 `POST /share/select` 的响应\n`download_url` 下发,客户端不应自行构造。\n\n## 示例\n\n登录获取令牌:\n\n```bash\ncurl -s http://localhost:8466/admin/login \\\n -H \'Content-Type: application/json\' \\\n -d \'{"password":"your-admin-password"}\'\n```\n\n```json\n{\n "code": 200, "msg": "ok",\n "data": {\n "id": "admin", "username": "admin",\n "token": "eyJhbGciOiJIUzI1NiIs...",\n "token_type": "Bearer",\n "expires_at": 1750000000,\n "expires_in": 604800\n }\n}\n```\n\n携带令牌调用管理接口:\n\n```bash\nTOKEN="eyJhbGciOiJIUzI1NiIs..."\ncurl -s http://localhost:8466/admin/dashboard -H "Authorization: Bearer $TOKEN"\n```\n\n校验令牌是否有效:\n\n```bash\ncurl -s http://localhost:8466/admin/verify -H "Authorization: Bearer $TOKEN"\n```\n\n```json\n{\n "code": 200, "msg": "ok",\n "data": { "id": "admin", "username": "admin", "token": "eyJhbGciOiJIUzI1NiIs...", "token_type": "Bearer", "expires_at": 1750000000 }\n}\n```\n\n令牌失效时:\n\n```json\n{ "code": 401, "msg": "令牌无效或已过期" }\n```\n',c='# 文本分享\n\n创建纯文本分享,返回取件码。文本大小上限 **222KB**(超限建议改用文件分享);请求体全局上限 1MiB`Content-Length` >441KB 时读前直接 403。\n经审计中间件落库(action=upload)。\n\n## POST /share/text\n\n**请求参数**`application/x-www-form-urlencoded`,亦支持 multipart`text` 为必需):\n\n| 参数 | 类型 | 必需 | 默认 | 说明 |\n|---|---|---|---|---|\n| `code` | string,可选;自定义提取码,5-8 位字母或数字(空=随机生成;占用 400「该提取码已被占用」) |\n| `text` | string | ✅ | - | 文本内容(≤222KB,按 UTF-8 字节数) |\n| `expire_value` | int | ❌ | `1` | 过期值(配合 `expire_style` |\n| `expire_style` | string | ❌ | `day` | `day`/`hour`/`minute`/`count`/`forever`(须在站点允许列表内) |\n\n过期语义:\n\n- `day`/`hour`/`minute`:按时间过期,`expired_count = -1`。\n- `count`:按次数过期,取件 `expire_value` 次后失效(`expired_count = expire_value`);\n **v2 需求 ④**`max_save_count>0` 时 `expire_value` 不得超出该上限,超限 403。\n- `forever`:永久(需站点允许;`max_save_seconds>0` 时其他方式受最长保存上限约束,超限 403)。\n\n> 可选值与上限来自公开配置 `GET /api/v1/config``expireStyle`、`max_save_seconds`、\n> `max_save_count`),上传页动态读取并在范围内选择;管理端改策略后立即生效。\n\n**curl 示例**\n\n```bash\n# 自定义提取码(可选):-d \'code=MYCODE1\'\ncurl -s -X POST http://localhost:8466/share/text \\\n -d \'text=你好,文件快传\' \\\n -d \'expire_value=1\' \\\n -d \'expire_style=day\'\n```\n\n**成功响应**200):\n\n```json\n{ "code": 200, "msg": "ok", "data": { "code": "8XQ2M" } }\n```\n\n`data.code` 为 5 位取件码(数字或大写字母+数字,取决于 `code_generate_type`)。\n\n**错误响应**\n\n```json\n{ "code": 400, "msg": "过期时间类型错误" }\n```\n\n```json\n{ "code": 400, "msg": "过期时间值必须大于 0" }\n```\n\n```json\n{ "code": 403, "msg": "内容过多,建议采用文件形式" }\n```\n\n```json\n{ "code": 403, "msg": "限制最长时间为 7天,可换用其他方式" }\n```\n\n```json\n{ "code": 403, "msg": "限制次数最多为 5 次" }\n```\n\n```json\n{ "code": 423, "msg": "请求次数过多,请稍后再试" }\n```\n\n> 游客上传关闭(`openUpload=0`)时需携带管理员令牌,否则 403\n> `{"code":403,"msg":"本站未开启游客上传,如需上传请先登录后台"}`\n\n## 取回文本\n\n文本分享的取回走统一的取件接口(消耗次数):\n\n- `GET /share/select?code=<code>` → `text/plain` 正文即文本内容(响应头 `Content-Disposition` 带文件名,无扩展名时为 `<prefix>.txt`)。\n- `POST /share/select``{"code":"8XQ2M"}`)→ JSON`data.text` / `data.content` 为文本内容。\n\n示例:\n\n```bash\ncurl -s "http://localhost:8466/share/select?code=8XQ2M"\n```\n\n```text\n你好,文件快传\n```\n\n**v3.1 变更**:① 支持 JSON 提交(`Content-Type: application/json`,字段同名);② 空文本 400「分享内容不能为空」;③ 可选 `code` 自定义提取码(5-8 位字母数字,占用 400)。\n',l='# 文件分享\n\n上传单个文件并创建分享。支持扩展名/MIME 白名单 + **magic bytes 防伪**(读文件前 64 字节校验,\n伪造类型返回 403)。经审计中间件落库(action=upload,记录文件总大小与实际传输字节)。\n\n## POST /share/file\n\n**请求参数**`multipart/form-data`):\n\n| 参数 | 类型 | 必需 | 默认 | 说明 |\n|---|---|---|---|---|\n| `code` | string,可选;自定义提取码,5-8 位字母或数字(空=随机生成;占用 400 |\n| `file` | file | ✅ | - | 上传的文件(大小 ≤ 生效上限:`max_file_size>0` 时为其,否则 `uploadSize` |\n| `expire_value` | int | ❌ | `1` | 过期值(配合 `expire_style``count` 型受 `max_save_count` 约束) |\n| `expire_style` | string | ❌ | `day` | `day`/`hour`/`minute`/`count`/`forever`(须在 `expireStyle` 白名单内) |\n\n**curl 示例**\n\n```bash\ncurl -s -X POST http://localhost:8466/share/file \\\n -F \'file=@./report.pdf;type=application/pdf\' \\\n -F \'expire_value=7\' \\\n -F \'expire_style=day\'\n```\n\n**成功响应**200):\n\n```json\n{ "code": 200, "msg": "ok", "data": { "code": "K3P9W", "name": "report.pdf" } }\n```\n\n**错误响应**\n\n```json\n{ "code": 400, "msg": "缺少上传文件 file 字段" }\n```\n\n```json\n{ "code": 403, "msg": "大小超过限制,最大为10.00 MB" }\n```\n\n> 大小上限为动态策略(v2 需求 ④⑩):管理端改 `max_file_size`0=回落 `uploadSize`)后\n> **下一次上传立即按新上限执行**,无需重启;上限值可经 `GET /api/v1/config` 的\n> `max_file_size`/`maxFileSize` 字段读取。\n\n```json\n{ "code": 403, "msg": "不允许上传该类型文件" }\n```\n\n```json\n{ "code": 403, "msg": "文件内容与扩展名不匹配,疑似伪造类型" }\n```\n\n```json\n{ "code": 403, "msg": "限制最长时间为 7天,可换用其他方式" }\n```\n\n```json\n{ "code": 403, "msg": "限制次数最多为 5 次" }\n```\n\n```json\n{ "code": 403, "msg": "请求次数过多,请稍后再试" }\n```\n\n```json\n{ "code": 507, "msg": "存储空间已达到管理员设置的容量上限" }\n```\n\n```json\n{ "code": 503, "msg": "存储服务不可用,请稍后再试" }\n```\n\n## 文件类型白名单\n\n由配置 `allowed_file_types` 控制(管理端可改):\n\n- `*`:不限制(默认)。\n- 扩展名规则:`.png`、`pdf`(自动补点)等,按文件名后缀匹配。\n- MIME 规则:`image/*`、`application/pdf` 等,按请求 `Content-Type` 通配匹配。\n\n已知类型(png/jpg/gif/webp/bmp/pdf/zip/rar/7z/gz/mp3/mp4/exe/elf)会做 **magic bytes 交叉校验**\n扩展名或 Content-Type 声明了已知类型,但文件头不匹配时拒绝(403「疑似伪造类型」)。\n\n## 下载取件\n\n- `GET /share/select?code=<code>`:消耗 1 次取件,返回文件流(`200` 全量 / `206` 区间,\n 支持 `Range` 请求头;响应含 `Accept-Ranges: bytes`、`Content-Disposition: attachment; filename*=UTF-8\'\'...`)。\n- `POST /share/select`:返回详情 JSON`download_url` 为代理下载地址(见下)。\n- `GET /share/download?key=<token>&code=<code>`:代理下载,消耗 1 次,同样支持 Range。\n\nRange 示例(取前 1024 字节):\n\n```bash\ncurl -s -H \'Range: bytes=0-1023\' -o part.bin \\\n "http://localhost:8466/share/select?code=K3P9W"\n```\n\n区间越界返回:\n\n```json\n{ "code": 416, "msg": "请求范围超出文件大小" }\n```\n',m=`# 分享查询与取件
查询分享元信息(不消耗次数)与真正取件(消耗次数)的完整接口。
除 \`metadata\` 每次 423 限流计数外,取件失败还会计入 \`error\` 限流。
## 元信息:GET /share/metadata
按取件码查询元信息,**不消耗次数**。每次访问即计入 \`metadata\` 限流。
**参数**\`code\`query,必需)。
\`\`\`bash
curl -s "http://localhost:8466/share/metadata?code=K3P9W"
\`\`\`
**成功响应**200):
\`\`\`json
{
"code": 200, "msg": "ok",
"data": {
"code": "K3P9W",
"name": "report.pdf",
"size": 1048576,
"type": "file",
"is_text": false,
"created_at": "2025-06-01T12:00:00+08:00",
"expired_at": "2025-06-08T12:00:00+08:00",
"expires_at": "2025-06-08T12:00:00+08:00",
"expired_count": -1,
"used_count": 3,
"remaining_downloads": null
}
}
\`\`\`
字段说明:
| 字段 | 说明 |
|---|---|
| \`type\` | \`text\`(文本分享)/ \`file\`(文件分享) |
| \`is_text\` | 是否文本分享 |
| \`size\` | 字节数 |
| \`expired_at\` / \`expires_at\` | 过期时间(RFC 3339);永久分享为 \`null\` |
| \`expired_count\` | \`-1\` 按时间/永久;\`>0\` 剩余可取次数(原始限额) |
| \`used_count\` | 已取次数 |
| \`remaining_downloads\` | 剩余可取次数(仅次数型分享有值,否则 \`null\`) |
> 不返回存储路径等敏感字段。
**错误响应**
\`\`\`json
{ "code": 404, "msg": "文件不存在" }
\`\`\`
\`\`\`json
{ "code": 404, "msg": "文件已过期" }
\`\`\`
## 元信息(POST):POST /share/metadata
等价的 JSON 版本(\`code\` 放请求体):
\`\`\`bash
curl -s -X POST http://localhost:8466/share/metadata \\
-H 'Content-Type: application/json' \\
-d '{"code":"K3P9W"}'
\`\`\`
响应与 GET 版本一致。
## 取件(消耗次数):GET /share/select
**每调用一次消耗 1 次取件**(次数型分享扣减 \`expired_count\`;时间型扣减不计)。
- 文本分享:返回 \`text/plain; charset=utf-8\` 正文(非 JSON 封装),\`Content-Disposition\` 携带文件名。
- 文件分享:返回文件流(\`200\` 全量 / \`206\` 区间),支持 \`Range\`。
\`\`\`bash
curl -s -OJ "http://localhost:8466/share/select?code=K3P9W"
\`\`\`
次数耗尽或已过期:
\`\`\`json
{ "code": 404, "msg": "文件已过期" }
\`\`\`
超限(计入 error 限流):
\`\`\`json
{ "code": 423, "msg": "请求次数过多,请稍后再试" }
\`\`\`
## 取件详情:POST /share/select
返回元信息 + 文本内容/下载地址的 JSON 详情。**消耗语义**:次数型分享(\`expired_count >= 0\`
返回代理地址 \`download_url\` 且本次**不消耗**(消耗发生在访问代理地址时);时间型/永久/文本分享在本次消耗。
\`\`\`bash
curl -s -X POST http://localhost:8466/share/select \\
-H 'Content-Type: application/json' \\
-d '{"code":"K3P9W"}'
\`\`\`
**文件分享响应**200):
\`\`\`json
{
"code": 200, "msg": "ok",
"data": {
"code": "K3P9W",
"name": "report.pdf",
"size": 1048576,
"type": "file",
"is_text": false,
"created_at": "2025-06-01T12:00:00+08:00",
"expired_at": "2025-06-08T12:00:00+08:00",
"expires_at": "2025-06-08T12:00:00+08:00",
"expired_count": -1,
"used_count": 4,
"remaining_downloads": null,
"text": "/share/download?key=9f2c…&code=K3P9W",
"download_url": "/share/download?key=9f2c…&code=K3P9W"
}
}
\`\`\`
> S3 引擎下时间型/永久分享的 \`download_url\` 可能是预签名直链(1 小时有效)而非代理地址;
> 次数型分享恒为代理地址 \`"/share/download?key=…&code=…"\`。
**文本分享响应**200):
\`\`\`json
{
"code": 200, "msg": "ok",
"data": {
"code": "8XQ2M", "name": "Text.txt", "size": 24, "type": "text", "is_text": true,
"created_at": "2025-06-01T12:00:00+08:00",
"expired_at": "2025-06-02T12:00:00+08:00", "expires_at": "2025-06-02T12:00:00+08:00",
"expired_count": -1, "used_count": 1, "remaining_downloads": null,
"text": "你好,文件快传",
"content": "你好,文件快传",
"download_url": null
}
}
\`\`\`
## 代理下载:GET /share/download
\`POST /share/select\` 返回的代理地址,**每次访问消耗 1 次**,支持 Range。
| 参数 | 说明 |
|---|---|
| \`key\` | 窗口令牌(服务端下发,双窗口校验) |
| \`code\` | 取件码 |
\`\`\`bash
curl -s -OJ "http://localhost:8466/share/download?key=9f2c…&code=K3P9W"
\`\`\`
**错误响应**
\`\`\`json
{ "code": 403, "msg": "下载鉴权失败" }
\`\`\`
\`\`\`json
{ "code": 404, "msg": "文件已过期" }
\`\`\`
> \`key\` 鉴权失败会计入 error 限流;文本分享经该接口返回 JSON 封装 \`data\` 为文本内容。
`,u=`# 分片上传
大文件分片上传:客户端把文件切成固定大小的分片逐个上传,服务端按索引合并并做 SHA256 校验。
支持**断点续传**(相同 \`file_hash\` + 大小 + 文件名的未完成会话自动续传)。
需站点开启 \`enableChunk\`(公共配置 \`enableChunk\` 返回 \`true\`)。
分片会话保留 24 小时。全部端点经审计中间件落库(action=upload)。
## 上传流程
\`\`\`text
POST /chunk/upload/init → upload_id, total_chunks
POST /chunk/upload/{id}/{index} → 逐片上传(0 起,可并发)
GET /chunk/upload/status/{id} → 断点续传时查进度
POST /chunk/upload/complete/{id} → 合并 + SHA256 → 取件码
DELETE /chunk/upload/{id} → 取消(可选)
\`\`\`
## 初始化:POST /chunk/upload/init
**请求体**(JSON,亦兼容表单):
| 参数 | 类型 | 必需 | 默认 | 说明 |
|---|---|---|---|---|
| \`file_name\` | string | ✅ | - | 文件名(会做清理与白名单校验) |
| \`file_size\` | int | ✅ | - | 文件总字节数(>0;服务端按分片数校验上限) |
| \`chunk_size\` | int | ❌ | \`5242880\`5MB | 每片大小(字节),硬上限 32MiB(超出 400「chunk_size 过大」) |
| \`file_hash\` | string | ❌ | - | 整文件 SHA256(断点续传的匹配键) |
**curl 示例**
\`\`\`bash
curl -s -X POST http://localhost:8466/chunk/upload/init \\
-H 'Content-Type: application/json' \\
-d '{"file_name":"movie.mp4","file_size":15728640,"chunk_size":5242880,"file_hash":"<sha256>"}'
\`\`\`
**成功响应**(200,新建会话):
\`\`\`json
{
"code": 200, "msg": "ok",
"data": {
"existed": false,
"upload_id": "3f6b8c2a4d5e6f708192a3b4c5d6e7f8",
"chunk_size": 5242880,
"total_chunks": 3,
"uploaded_chunks": []
}
}
\`\`\`
**断点续传响应**(200,命中未完成会话):返回既有会话,\`uploaded_chunks\` 为已传分片索引列表,
客户端只需补传缺失分片(注意:\`existed\` 字段恒为 \`false\`,是否续传以 \`upload_id\` 复用且
\`uploaded_chunks\` 非空为准):
\`\`\`json
{
"code": 200, "msg": "ok",
"data": {
"existed": false,
"upload_id": "3f6b8c2a4d5e6f708192a3b4c5d6e7f8",
"chunk_size": 5242880,
"total_chunks": 3,
"uploaded_chunks": [0, 1]
}
}
\`\`\`
**错误响应**
\`\`\`json
{ "code": 400, "msg": "file_size 必须大于 0" }
\`\`\`
\`\`\`json
{ "code": 403, "msg": "大小超过限制,最大为10.00 MB" }
\`\`\`
\`\`\`json
{ "code": 403, "msg": "分片上传未启用" }
\`\`\`
## 上传分片:POST /chunk/upload/{uploadID}/{chunkIndex}
主路径(与参考实现语义一致)。\`chunkIndex\` 从 \`0\` 起。
**multipart 字段**\`chunk\`(必需,该分片的二进制数据)。
**curl 示例**
\`\`\`bash
split -b 5242880 movie.mp4 part- # 本地分片
curl -s -X POST http://localhost:8466/chunk/upload/3f6b8c2a4d5e6f708192a3b4c5d6e7f8/0 \\
-F 'chunk=@./part-aa'
\`\`\`
**成功响应**200):
\`\`\`json
{ "code": 200, "msg": "ok", "data": { "chunk_hash": "9af1…", "chunk_index": 0 } }
\`\`\`
重复上传已完成的分片(幂等跳过):
\`\`\`json
{ "code": 200, "msg": "ok", "data": { "chunk_hash": "9af1…", "chunk_index": 0, "skipped": true } }
\`\`\`
**错误响应**
\`\`\`json
{ "code": 404, "msg": "上传会话不存在" }
\`\`\`
\`\`\`json
{ "code": 400, "msg": "无效的分片索引" }
\`\`\`
\`\`\`json
{ "code": 400, "msg": "分片大小超过声明值: 最大 5242880, 实际 5300000" }
\`\`\`
\`\`\`json
{ "code": 400, "msg": "缺少分片文件字段 chunk" }
\`\`\`
\`\`\`json
{ "code": 403, "msg": "大小超过限制,最大为10.00 MB" }
\`\`\`
> 约束:单分片 ≤ \`chunk_size\`init 声明值)且 ≤ **32MiB 硬上限**init 时 \`chunk_size>33554432\` 直接 400「chunk_size 过大」);
> 总大小(init 按分片数上限、上传/合并按累计)受**动态策略上限**约束——\`max_file_size>0\` 时为其,
> 否则回落 \`uploadSize\`(v2 需求 ④⑩,管理端改后立即生效,超限清理会话);首个分片做 magic bytes
> 防伪校验(403「文件内容校验失败:…」)。分片哈希由服务端计算;合并时与分片记录交叉校验,不一致报 400。
>
> **enableChunk 开关**:管理端关闭分片上传后,\`/chunk/upload/*\` 全部端点返回 403「分片上传未启用」(后端强制,前端仅隐藏入口)。
## 查询进度:GET /chunk/upload/status/{uploadID}
\`\`\`bash
curl -s http://localhost:8466/chunk/upload/status/3f6b8c2a4d5e6f708192a3b4c5d6e7f8
\`\`\`
**成功响应**200):
\`\`\`json
{
"code": 200, "msg": "ok",
"data": {
"upload_id": "3f6b8c2a4d5e6f708192a3b4c5d6e7f8",
"file_name": "movie.mp4",
"file_size": 15728640,
"chunk_size": 5242880,
"total_chunks": 3,
"uploaded_chunks": [0, 1],
"progress": 66.66666666666667
}
}
\`\`\`
## 完成合并:POST /chunk/upload/complete/{uploadID}
全部分片到齐后调用。服务端按索引有序合并 + SHA256 校验,成功后创建分享并清理分片。
**请求体**(JSON,亦兼容表单):
| 参数 | 类型 | 必需 | 默认 | 说明 |
|---|---|---|---|---|
| \`expire_value\` | int | ❌ | \`1\` | 过期值 |
| \`expire_style\` | string | ❌ | \`day\` | 过期方式(同文件分享) |
\`\`\`bash
curl -s -X POST http://localhost:8466/chunk/upload/complete/3f6b8c2a4d5e6f708192a3b4c5d6e7f8 \\
-H 'Content-Type: application/json' \\
-d '{"expire_value":1,"expire_style":"day"}'
\`\`\`
**成功响应**200):
\`\`\`json
{ "code": 200, "msg": "ok", "data": { "code": "R7T2K", "name": "movie.mp4" } }
\`\`\`
**错误响应**
\`\`\`json
{ "code": 400, "msg": "分片不完整" }
\`\`\`
\`\`\`json
{ "code": 400, "msg": "分片哈希校验失败,请重新上传" }
\`\`\`
\`\`\`json
{ "code": 403, "msg": "大小超过限制,最大为10.00 MB" }
\`\`\`
## 取消上传:DELETE /chunk/upload/{uploadID}
清理分片文件与上传记录,释放容量预留。
\`\`\`bash
curl -s -X DELETE http://localhost:8466/chunk/upload/3f6b8c2a4d5e6f708192a3b4c5d6e7f8
\`\`\`
\`\`\`json
{ "code": 200, "msg": "ok", "data": { "message": "上传已取消" } }
\`\`\`
\`\`\`json
{ "code": 404, "msg": "上传会话不存在" }
\`\`\`
`,g=`# 预签名直传
服务端预生成上传地址,客户端直接向存储引擎(或服务端代理)上传文件,最后确认建分享。
两种模式:
| 模式 | 引擎 | 上传方式 |
|---|---|---|
| \`direct\` | S3(含 MinIO/R2 等 S3 兼容存储) | 客户端 \`PUT\` 到预签名 URL,直连对象存储 |
| \`proxy\` | 本地 / WebDAV | 客户端 \`PUT\` multipart 到服务端代理接口 |
> 本地 / WebDAV 引擎不支持预签名直链,init 返回正常 \`proxy\` 模式(仅当引擎预签名调用本身异常时才报错)。
> 会话有效期 **900 秒**15 分钟)。proxy 模式响应另含 \`legacy_proxy_upload_url\`
> \`/api\` 前缀的兼容别名,已废弃,与 \`upload_url\` 等价)。
> 全部端点经审计中间件落库(action=upload)。
## 上传流程
\`\`\`text
direct 模式:
POST /presign/upload/init → upload_urlS3 预签名 PUT
PUT <upload_url> → 客户端直传 S3(无认证头)
POST /presign/upload/confirm/{id} → 确认 → 取件码
proxy 模式:
POST /presign/upload/init → upload_url = /presign/upload/proxy/{id}
PUT /presign/upload/proxy/{id} → multipart 上传,服务端转存并直接建分享
\`\`\`
## 初始化:POST /presign/upload/init
**请求体**(JSON,亦兼容表单):
| 参数 | 类型 | 必需 | 默认 | 说明 |
|---|---|---|---|---|
| \`file_name\` | string | ✅ | - | 文件名(清理 + 白名单校验) |
| \`file_size\` | int | ✅ | - | 文件字节数(≤ 生效上限:\`max_file_size>0\` 时为其,否则 \`uploadSize\` |
| \`expire_value\` | int | ❌ | \`1\` | 过期值(\`count\` 型受 \`max_save_count\` 约束) |
| \`expire_style\` | string | ❌ | \`day\` | 过期方式(须在 \`expireStyle\` 白名单内) |
**curl 示例**
\`\`\`bash
curl -s -X POST http://localhost:8466/presign/upload/init \\
-H 'Content-Type: application/json' \\
-d '{"file_name":"backup.zip","file_size":20971520,"expire_value":7,"expire_style":"day"}'
\`\`\`
**S3direct)成功响应**200):
\`\`\`json
{
"code": 200, "msg": "ok",
"data": {
"upload_id": "6a1e…",
"upload_url": "https://minio:9000/fileshare/share/data/2025/06/01/6a1e…/backup.zip?X-Amz-…",
"mode": "direct",
"expires_in": 900,
"file_path": "share/data/2025/06/01/6a1e…"
}
}
\`\`\`
**local/WebDAVproxy)成功响应**200):
\`\`\`json
{
"code": 200, "msg": "ok",
"data": {
"upload_id": "6a1e…",
"upload_url": "/presign/upload/proxy/6a1e…",
"proxy_upload_url": "/presign/upload/proxy/6a1e…",
"mode": "proxy",
"expires_in": 900,
"file_path": "share/data/2025/06/01/6a1e…"
}
}
\`\`\`
**错误响应**
\`\`\`json
{ "code": 403, "msg": "大小超过限制,最大为10.00 MB" }
\`\`\`
> 大小上限为动态策略(v2 需求 ④⑩):管理端改 \`max_file_size\`0=回落 \`uploadSize\`)后
> 立即按新上限校验 init 声明的 \`file_size\`。
\`\`\`json
{ "code": 403, "msg": "不允许上传该类型文件" }
\`\`\`
\`\`\`json
{ "code": 507, "msg": "存储空间已达到管理员设置的容量上限" }
\`\`\`
## 直传确认:POST /presign/upload/confirm/{uploadID}
\`direct\` 模式专用:客户端向 S3 \`PUT\` 完成后调用。服务端会核对实际对象(多引擎一致):
- **大小核对**:实际大小与声明差 >1KB → 400「文件大小与声明不符」;超过策略上限(\`max_file_size\`)→ **删除对象、释放容量预留**并 403;
- **内容校验**:取对象前 64 字节做 magic bytes 白名单校验(失败删除对象并报错);
- 全部通过后创建分享记录。
\`\`\`bash
# 1) 直传 S3(注意:不要带 Authorization 头,预签名 URL 自带鉴权)
curl -X PUT "<data.upload_url>" --upload-file ./backup.zip
# 2) 确认
curl -s -X POST http://localhost:8466/presign/upload/confirm/6a1e…
\`\`\`
**成功响应**200):
\`\`\`json
{ "code": 200, "msg": "ok", "data": { "code": "B4N8Q", "name": "backup.zip" } }
\`\`\`
**错误响应**
\`\`\`json
{ "code": 404, "msg": "文件未上传或上传失败" }
\`\`\`
\`\`\`json
{ "code": 400, "msg": "文件大小与声明不符" }
\`\`\`
\`\`\`json
{ "code": 403, "msg": "文件大小超过限制" }
\`\`\`
\`\`\`json
{ "code": 400, "msg": "此会话不支持direct模式" }
\`\`\`
## 代理上传:PUT /presign/upload/proxy/{uploadID}
\`proxy\` 模式专用:multipart 上传到服务端,服务端流式转存到存储引擎后立即创建分享记录。
**multipart 字段**\`file\`(必需)。
**curl 示例**
\`\`\`bash
curl -s -X PUT http://localhost:8466/presign/upload/proxy/6a1e… -F 'file=@./backup.zip'
\`\`\`
**成功响应**200):
\`\`\`json
{ "code": 200, "msg": "ok", "data": { "code": "B4N8Q", "name": "backup.zip" } }
\`\`\`
**错误响应**
\`\`\`json
{ "code": 400, "msg": "缺少上传文件 file 字段" }
\`\`\`
\`\`\`json
{ "code": 400, "msg": "文件大小与声明不符" }
\`\`\`
\`\`\`json
{ "code": 403, "msg": "大小超过限制,最大为10.00 MB" }
\`\`\`
> 文件实际大小须与 init 声明的 \`file_size\` 一致(±1KB 容差);成功后会话即删除,不可重复使用。
## 查询会话:GET /presign/upload/status/{uploadID}
\`\`\`bash
curl -s http://localhost:8466/presign/upload/status/6a1e…
\`\`\`
**成功响应**200):
\`\`\`json
{
"code": 200, "msg": "ok",
"data": {
"upload_id": "6a1e…",
"file_name": "backup.zip",
"file_size": 20971520,
"mode": "proxy",
"created_at": "2025-06-01T12:00:00+08:00",
"expires_at": "2025-06-01T12:15:00+08:00",
"is_expired": false
}
}
\`\`\`
## 取消会话:DELETE /presign/upload/{uploadID}
删除会话并释放容量预留;\`direct\` 模式会尽力清理已直传到 S3 的对象。
\`\`\`bash
curl -s -X DELETE http://localhost:8466/presign/upload/6a1e…
\`\`\`
\`\`\`json
{ "code": 200, "msg": "ok", "data": { "message": "上传会话已取消" } }
\`\`\`
\`\`\`json
{ "code": 404, "msg": "上传会话不存在" }
\`\`\`
\`\`\`json
{ "code": 404, "msg": "上传会话已过期" }
\`\`\`
`,_='# 管理后台 API\n\n管理端接口:除 `POST /admin/login` 与初始化向导 `/setup` 外,一律要求\n`Authorization: Bearer <token>`(见《认证与限流》),无效令牌 401。\n\n## 初始化向导:GET /setup\n\n未初始化时返回 HTML 配置页(站点名称、管理员密码、上传/限流/保存策略);\n已初始化时 `303` 重定向到 `/`。\n\n```bash\ncurl -i http://localhost:8466/setup\n```\n\n## 初始化提交:POST /setup\n\n表单(浏览器向导)或 JSON 均可;成功后写入库配置 KV 并生成密码哈希与 `jwt_secret`。\n表单提交返回成功 HTML 页;JSON 提交返回 JSON。\n\n**主要字段**\n\n| 字段 | 必需 | 默认 | 说明 |\n|---|---|---|---|\n| `admin_password` | ✅ | - | 管理员密码(≥8 位) |\n| `confirm_password` | ✅ | - | 确认密码(须一致) |\n| `site_name` | ❌ | 文件快传 | 站点名称 |\n| `upload_size_value` / `upload_size_unit` | ❌ | 10 / MB | 单文件大小限制(单位 KB/MB/GB |\n| `save_time_value` / `save_time_unit` | ❌ | 0 / day | 最长保存秒数(0=不限) |\n| `expireStyle` | ❌ | day,hour,minute,forever,count | 过期方式(可多值/逗号分隔) |\n| `code_generate_type` | ❌ | secret | 取件码类型 `number`/`secret` |\n| `errorCount` / `errorMinute` | ❌ | 10 / 1 | 取件错误限流 |\n| `loginCount` / `loginMinute` | ❌ | 5 / 15 | 登录失败限流 |\n| `uploadCount` / `uploadMinute` | ❌ | 10 / 1 | 上传限流 |\n| `allowed_file_types` | ❌ | `*` | 逗号分隔白名单 |\n| `openUpload` / `enableChunk` | ❌ | 1 / 0 | 游客上传 / 分片开关(`1`/`true`/`on`/`yes` |\n\n```bash\ncurl -s -X POST http://localhost:8466/setup \\\n -H \'Content-Type: application/json\' \\\n -d \'{"admin_password":"admin12345","confirm_password":"admin12345","site_name":"我的文件柜","upload_size_value":10,"upload_size_unit":"MB"}\'\n```\n\n```json\n{ "code": 200, "msg": "ok", "data": { "ok": true, "admin": "/#/admin" } }\n```\n\n**错误响应**400HTML 表单时内嵌错误提示):\n\n```json\n{ "code": 400, "msg": "管理员密码至少 8 位" }\n```\n\n## 登录:POST /admin/login\n\n```bash\ncurl -s -X POST http://localhost:8466/admin/login \\\n -H \'Content-Type: application/json\' \\\n -d \'{"password":"admin12345"}\'\n```\n\n**成功响应**200):\n\n```json\n{\n "code": 200, "msg": "ok",\n "data": {\n "id": "admin", "username": "admin",\n "token": "eyJhbGciOiJIUzI1NiIs...",\n "token_type": "Bearer",\n "expires_at": 1750000000,\n "expires_in": 604800\n }\n}\n```\n\n**错误响应**\n\n```json\n{ "code": 401, "msg": "密码错误" }\n```\n\n```json\n{ "code": 423, "msg": "请求次数过多,请稍后再试" }\n```\n\n## 校验会话:GET /admin/verify\n\n```bash\ncurl -s http://localhost:8466/admin/verify -H "Authorization: Bearer $TOKEN"\n```\n\n```json\n{ "code": 200, "msg": "ok", "data": { "id": "admin", "username": "admin", "token": "eyJ…", "token_type": "Bearer", "expires_at": 1750000000 } }\n```\n\n## 登出:POST /admin/logout\n\n无状态 JWT,服务端仅返回确认(客户端应丢弃令牌):\n\n```json\n{ "code": 200, "msg": "ok", "data": { "ok": true } }\n```\n\n## 仪表盘:GET /admin/dashboard\n\n```bash\ncurl -s http://localhost:8466/admin/dashboard -H "Authorization: Bearer $TOKEN"\n```\n\n```json\n{\n "code": 200, "msg": "ok",\n "data": {\n "totalFiles": 42,\n "storageUsed": "123456789",\n "sysUptime": 1750000000000,\n "yesterdayCount": 5, "yesterdaySize": "1048576",\n "todayCount": 12, "todaySize": "5242880",\n "activeCount": 40, "expiredCount": 2,\n "textCount": 10, "fileCount": 32, "chunkedCount": 3,\n "usedCount": 156,\n "storageBackend": "local",\n "uploadSizeLimit": 10485760,\n "openUpload": 1, "enableChunk": 1,\n "maxSaveSeconds": 0,\n "topSuffixes": [ { "suffix": ".pdf", "count": 12 }, { "suffix": "Text", "count": 10 } ],\n "recentFiles": [ { "id": 42, "code": "K3P9W", "name": "report.pdf", "size": 1048576, "created_at": "2025-06-01T12:00:00+08:00", "expired_at": "2025-06-08T12:00:00+08:00", "is_expired": false, "expired_count": -1, "used_count": 3, "is_text": false, "is_chunked": false, "is_permanent": false, "has_download_limit": false, "remaining_downloads": null, "file_hash": null, "prefix": "report", "suffix": ".pdf", "text": false, "createdAt": "2025-06-01T12:00:00+08:00", "expiredAt": "2025-06-08T12:00:00+08:00", "isExpired": false, "expiredCount": -1, "usedCount": 3, "isText": false, "isChunked": false, "isPermanent": false, "hasDownloadLimit": false, "remainingDownloads": null, "fileHash": null } ],\n "recentActivities": []\n }\n}\n```\n\n> `storageUsed`/`todaySize`/`yesterdaySize` 为字符串字节数;`sysUptime` 为服务启动时刻的 Unix 毫秒。\n\n## 文件列表:GET /admin/file/list\n\n**参数**\n\n| 参数 | 默认 | 说明 |\n|---|---|---|\n| `page` / `size` | 1 / 10 | 分页(size 1~100 |\n| `keyword` | - | 模糊匹配取件码/文件名/哈希/文本内容 |\n| `status` | - | `active` / `expired` |\n| `type` | - | `text` / `file` / `chunked` |\n| `sortBy` | `created_at` | `created_at`/`expired_at`/`name`/`size`/`used_count`/`code` |\n| `sortOrder` | `desc` | `asc` / `desc` |\n\n```bash\ncurl -s "http://localhost:8466/admin/file/list?page=1&size=10&status=active&sortBy=size&sortOrder=desc" \\\n -H "Authorization: Bearer $TOKEN"\n```\n\n```json\n{\n "code": 200, "msg": "ok",\n "data": {\n "page": 1, "size": 10, "total": 40,\n "summary": { "totalFiles": 42, "activeCount": 40, "expiredCount": 2, "textCount": 10, "fileCount": 32, "chunkedCount": 3, "storageUsed": 123456789, "usedCount": 156 },\n "data": [\n { "id": 42, "code": "K3P9W", "name": "report.pdf", "prefix": "report", "suffix": ".pdf", "size": 1048576, "is_text": false, "is_chunked": false, "is_expired": false, "expired_at": "2025-06-08T12:00:00+08:00", "expired_count": -1, "used_count": 3, "created_at": "2025-06-01T12:00:00+08:00", "has_download_limit": false, "is_permanent": false, "remaining_downloads": null, "file_hash": null, "text": false, "isText": false, "isChunked": false, "isExpired": false, "expiredAt": "2025-06-08T12:00:00+08:00", "expiredCount": -1, "usedCount": 3, "createdAt": "2025-06-01T12:00:00+08:00", "hasDownloadLimit": false, "isPermanent": false, "remainingDownloads": null, "fileHash": null }\n ]\n }\n}\n```\n\n## 文件详情:GET /admin/file/detail\n\n`GET ?id=42` 或 `POST {"id":42}`。返回列表条目字段;文本分享额外含 `content`(全文)。\n\n```json\n{ "code": 200, "msg": "ok", "data": { "id": 42, "code": "K3P9W", "name": "report.pdf", "size": 1048576, "is_text": false, "expired_at": "2025-06-08T12:00:00+08:00", "expired_count": -1, "used_count": 3, "created_at": "2025-06-01T12:00:00+08:00", "file_hash": null } }\n```\n\n```json\n{ "code": 404, "msg": "文件不存在" }\n```\n\n## 更新文件:PATCH /admin/file/update\n\n更新取件码/文件名(前后缀)/过期策略。**PATCH 为主名,POST 为兼容别名**。\n\n**请求体**\n\n| 字段 | 类型 | 说明 |\n|---|---|---|\n| `id` | int | 必需 |\n| `code` | string | 新取件码(冲突 400「code已存在」) |\n| `prefix` / `suffix` | string | 文件名前后缀 |\n| `expired_at` | string | 过期时间(ISO 8601,如 `2025-07-01T00:00:00+08:00` |\n| `expired_count` | int | 取件次数上限(`-1` 按时间/永久) |\n\n```bash\ncurl -s -X PATCH http://localhost:8466/admin/file/update \\\n -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\\n -d \'{"id":42,"expired_at":"2025-07-01T00:00:00+08:00","expired_count":10}\'\n```\n\n```json\n{ "code": 200, "msg": "ok", "data": "更新成功" }\n```\n\n```json\n{ "code": 400, "msg": "code已存在" }\n```\n\n## 删除文件:DELETE /admin/file/delete\n\n删除分享记录并连带删除存储文件(文本分享无存储文件)。`DELETE` 或 `POST`,请求体 `{"id":42}`。\n\n```bash\ncurl -s -X DELETE http://localhost:8466/admin/file/delete \\\n -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' -d \'{"id":42}\'\n```\n\n```json\n{ "code": 200, "msg": "ok", "data": null }\n```\n\n```json\n{ "code": 400, "msg": "请选择要删除的文件" }\n```\n\n## 批量删除:POST /admin/file/batch-delete\n\n`POST` 或 `DELETE`,请求体 `{"ids":[41,42,43]}`。返回逐条统计:\n\n```json\n{\n "code": 200, "msg": "ok",\n "data": {\n "requestedCount": 3, "deletedCount": 2, "missingCount": 1, "failedCount": 0,\n "deleted": [41, 42], "missing": [43], "failed": [],\n "requested_count": 3, "deleted_count": 2, "missing_count": 1, "failed_count": 0\n }\n}\n```\n\n## 批量更新:PATCH /admin/file/batch-update\n\n`PATCH` 或 `POST`。请求体:`ids[]` 必需;`expired_at`ISO 8601/`expired_count` 二选一;\n`clearExpiredAt: true`(或 `clear_expired_at`= 清空过期时间并置 `expired_count=-1`(永久)。\n\n```bash\ncurl -s -X PATCH http://localhost:8466/admin/file/batch-update \\\n -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\\n -d \'{"ids":[41,42],"clearExpiredAt":true}\'\n```\n\n```json\n{\n "code": 200, "msg": "ok",\n "data": { "requestedCount": 2, "updatedCount": 2, "missingCount": 0, "failedCount": 0, "updated": 2, "missing": [], "failed": [], "requested_count": 2, "updated_count": 2, "missing_count": 0, "failed_count": 0 }\n}\n```\n\n## 过期策略动作:PATCH /admin/file/policy-action\n\n对单个文件执行快捷策略。`PATCH` 或 `POST`。批量版为 `/admin/file/batch-policy-action``{"ids":[…]}`),响应结构同批量更新。\n\n**请求体**\n\n| 字段 | 说明 |\n|---|---|\n| `id` | 文件 ID |\n| `action` | `extend_24h` / `extend_7d` / `make_permanent` / `reset_download_limit` |\n| `downloadLimit` | 仅 `reset_download_limit` 用:新取件次数(默认 5,须 >0 |\n\n```bash\ncurl -s -X PATCH http://localhost:8466/admin/file/policy-action \\\n -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\\n -d \'{"id":42,"action":"reset_download_limit","downloadLimit":3}\'\n```\n\n```json\n{ "code": 200, "msg": "ok", "data": { "id": 42, "action": "reset_download_limit" } }\n```\n\n```json\n{ "code": 400, "msg": "不支持的策略动作" }\n```\n\n> `extend_24h`/`extend_7d` 在当前过期时间(未过期时)基础上顺延;`make_permanent` 清空过期时间并置次数 -1。\n\n## 管理员下载:GET /admin/file/download?id=\n\n下载原文件(**不消耗取件次数**);文件返回二进制流(支持 Range),文本分享返回 JSON`data` 为文本内容)。\n\n```bash\ncurl -s -OJ "http://localhost:8466/admin/file/download?id=42" -H "Authorization: Bearer $TOKEN"\n```\n\n## 文本预览:GET /admin/file/preview\n\n仅文本分享可用(文件分享返回 400)。`maxChars` 截断长度(默认 40001~20000)。\n\n```bash\ncurl -s "http://localhost:8466/admin/file/preview?id=41&maxChars=100" -H "Authorization: Bearer $TOKEN"\n```\n\n```json\n{\n "code": 200, "msg": "ok",\n "data": {\n "id": 41, "code": "8XQ2M", "name": "Text.txt", "type": "text",\n "content": "你好,文件快传",\n "length": 24, "previewLength": 24, "truncated": false,\n "maxChars": 100, "max_chars": 100,\n "created_at": "2025-06-01T12:00:00+08:00", "createdAt": "2025-06-01T12:00:00+08:00"\n }\n}\n```\n\n```json\n{ "code": 400, "msg": "仅文本分享支持预览" }\n```\n\n## 读取配置:GET /admin/config/get\n\n返回运行时配置 KV(含默认值与管理端修改)。`admin_token` 恒返回空串(屏蔽);\n`jwt_secret` 不下发;存储引擎为进程级单例,`_engine_hint` 提示引擎配置修改需重启。\nv2 新增键(需求 ①②③④⑩)一并返回:`background_url`、`footer_text`、`footer_beian`、\n`notify_enabled`、`max_save_count`、`max_file_size` 等。\n\n```json\n{\n "code": 200, "msg": "ok",\n "data": {\n "site_name": "文件快传",\n "name": "文件快传",\n "description": "开箱即用的文件快传系统",\n "page_explain": "…", "keywords": "…",\n "notify_title": "系统通知", "notify_content": "…", "notify_enabled": 1,\n "logo_url": "",\n "favicon_url": "",\n "background_url": "", "footer_text": "", "footer_beian": "",\n "openUpload": 1, "uploadSize": 10485760,\n "max_file_size": 0, "max_save_count": 0,\n "allowed_file_types": ["*"], "expireStyle": ["day","hour","minute","forever","count"],\n "code_generate_type": "secret", "enableChunk": 1,\n "uploadMinute": 1, "uploadCount": 10,\n "errorMinute": 1, "errorCount": 10,\n "loginCount": 5, "loginMinute": 15,\n "max_save_seconds": 0, "storageLimit": 0,\n "opacity": 0.9, "background": "", "showAdminAddr": 0, "robotsText": "User-agent: *\\nDisallow: /",\n "adminSessionExpire": 604800,\n "storage_path": "", "local_storage_path": "/app/data",\n "file_storage": "local",\n "admin_token": "",\n "_engine_hint": { "storage_backend": "local", "note": "存储引擎为进程级单例,修改存储引擎相关配置后需重启服务生效" }\n }\n}\n```\n\n## 存储引擎热切换:POST /admin/storage/switchv3\n\n运行时切换存储引擎,**无需重启**\n\n```bash\ncurl -s -X POST http://localhost:8466/admin/storage/switch \\\n -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \\\n -d \'{"engine":"s3"}\'\n# 成功:{"code":200,"msg":"ok","data":{"ok":true,"engine":"s3"}}\n# 失败:503 {"code":503,"msg":"存储引擎切换失败,已保持原引擎: …"}\n```\n\n- `engine` 仅接受 `local|s3|webdav`400 中文错误)。\n- 切换流程:用最新 KV 参数构建新引擎 → 健康检查 → 通过才替换当前引擎并持久化 `storage_engine`。\n- 失败(构建/健康检查不过)返回 503,**原引擎与 KV 均保持不变**。\n- 切到当前引擎为幂等操作(直接返回 200)。\n- 旧文件按归属引擎(`file_codes.engine`)取回,切换后旧引擎文件仍可下载。\n\n## 更新配置:PATCH /admin/config/update\n\n部分更新(JSON 对象,未提供的键不变;表单亦可)。**PATCH 为主名,POST 为兼容别名**。\n\n- 仅接受管理端可见键(见上响应键集合),未知键忽略。\n- 数值型键自动转型:`openUpload`、`enableChunk`、`uploadSize`、`storageLimit`、限流四组、`max_save_seconds`、`adminSessionExpire`、`showAdminAddr`v2 新增 `max_save_count`、`max_file_size`、`notify_enabled``opacity` 为浮点。\n- **v3.1**`site_domain`(站点对外域名)可经本端点设置,非法格式 400(仅 http/https、主机+端口、不带路径)。\n- **v3 引擎键**`storage_engine` 不经本端点修改(走 `POST /admin/storage/switch`);引擎参数键\n `local_storage_path`、`webdav_url`、`webdav_root_path`、`webdav_username`、`webdav_password`、\n `s3_endpoint_url`、`s3_region_name`、`s3_bucket_name`、`s3_access_key_id`、`s3_secret_access_key`、\n `aws_session_token`、`s3_addressing_style`)可经本端点保存——保存后对应引擎实例缓存失效,\n 下次切换/构建生效;敏感键空串或 `******` 表示不修改。\n- **v2 schema 校验**`settings.KVSchema`,越界一律 400,中文错误信息):\n - 整型边界:`max_file_size` ≤ 10GiB10737418240)、`max_save_count` ≤ 100000、`max_save_seconds` ≤ 31536000365 天)、`notify_enabled` ∈ {0,1}、`uploadCount` 1~10000、`uploadMinute` 1~1440 等;\n - 字符串长度:`background_url` ≤ 2048、`footer_text` ≤ 2000、`footer_beian` ≤ 128、`notify_title` ≤ 128、`notify_content` ≤ 2000 字符;\n - 列表键 `expireStyle` / `allowed_file_types`:须为字符串数组(或逗号分隔串)且至少保留一项;\n - 错误示例:`{"code":400,"msg":"max_file_size 必须是整数"}`、`{"code":400,"msg":"max_file_size 不能大于 10737418240"}`、`{"code":400,"msg":"footer_beian 长度不能超过 128 字符"}`、`{"code":400,"msg":"notify_enabled 不能大于 1"}`。\n- `background_url` 协议白名单(需求 ①,防 `javascript:` 注入):仅 `http(s)://`、`data:image/*` 与站内相对路径(`/`开头);空串=清除背景。非法值 400\n `{"code":400,"msg":"background_url 仅支持 http(s) 地址、data:image 图片或站内相对路径"}`\n- `admin_token`:明文密码自动哈希,**并轮换 `jwt_secret`(全部管理员令牌立即失效)**;空串忽略;已是哈希格式则原样保存。\n- `adminSessionExpire` 须为 1~365 的整天秒数(86400 的整数倍),否则 400。\n- `storageLimit` 不能小于 0。\n- 修改限流/策略配置**立即生效**(无需重启:限流规则运行时同步,策略由上传链路每次实时读取);引擎相关(`file_storage`/`s3_*`/`webdav_*`/`storage_path`/`local_storage_path`)需重启。\n\n```bash\ncurl -s -X PATCH http://localhost:8466/admin/config/update \\\n -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\\n -d \'{"site_name":"我的文件柜","uploadSize":52428800,"openUpload":1,"footer_beian":"京ICP备20240001号","max_file_size":10485760,"max_save_count":5}\'\n```\n\n```json\n{ "code": 200, "msg": "ok", "data": { "ok": true } }\n```\n\n```json\n{ "code": 400, "msg": "adminSessionExpire 必须是 1 到 365 个整天" }\n```\n\n```json\n{ "code": 400, "msg": "background_url 仅支持 http(s) 地址、data:image 图片或站内相对路径" }\n```\n\n## 修改管理员密码:PATCH /admin/settings/password\n\n`PATCH` 或 `POST`。新密码 ≥8 位;成功后哈希保存并**轮换 `jwt_secret`,所有旧令牌失效(401**,需重新登录。\n\n```bash\ncurl -s -X PATCH http://localhost:8466/admin/settings/password \\\n -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\\n -d \'{"old_password":"admin12345","new_password":"new-pass-6789"}\'\n```\n\n```json\n{ "code": 200, "msg": "ok", "data": { "ok": true } }\n```\n\n```json\n{ "code": 400, "msg": "新密码长度至少 8 位" }\n```\n\n```json\n{ "code": 401, "msg": "旧密码错误" }\n```\n',y='# 审计日志查询\n\n所有上传/下载请求由审计中间件自动落库(需求 ③),管理端分页查询。\n认证:`Authorization: Bearer <token>`。\n\n## 审计记录内容\n\n每条审计日志覆盖以下维度(需求 ③):\n\n| 维度 | 字段 | 说明 |\n|---|---|---|\n| 操作时间 | `created_at` | RFC 3339 |\n| 客户端 | `ip` | 可信代理场景解析 XFF 后的真实 IP |\n| 终端信息 | `user_agent` | 原始 UA |\n| 设备解析 | `device_os` / `device_browser` / `device_type` | 由 UA 解析(如 Windows/Chrome/desktop |\n| 动作 | `action` | `upload`(上传类) / `download`(取件/下载类) / `admin`(管理端敏感操作,26.9 新增) |\n| 结果 | `result` | `success` / `denied`(拒绝:401/403/423/428/ `failed`(失败:其余 4xx/5xx 或业务报错) |\n| 字节数 | `size_bytes` | 文件总大小;`transferred_bytes` 实际传输(**Range 下载只计实际区间字节**;下载由中间件自动统计,上传由各 handler 填充) |\n| 耗时 | `duration_ms` | 毫秒 |\n| 角色 | `actor` | `admin`(有效管理员令牌)/ `guest` |\n| 业务 | `file_code` / `file_name` | 取件码 / 文件名(分片上传时 `file_code` 为 `upload_id` |\n| 错误 | `error_msg` | 失败/拒绝原因 |\n\n**命中审计的端点**:上传类 `POST /share/text`、`POST /share/file`、`/chunk/upload*`、`/presign*`POST/PUT);\n下载类 `GET /share/download`、`GET /share/select`、`GET /share/metadata`\n管理类(`action=admin``POST /admin/login`、`POST /admin/logout`、`PATCH|POST /admin/config/update`、\n`PATCH|POST /admin/settings/password`、`POST /admin/storage/switch`、`PATCH|DELETE /admin/file/update|delete|batch-delete|batch-update|policy-action|batch-policy-action`。\n管理类动作未显式填结果时按 HTTP 状态兜底落库(401/403/423 → `denied`5xx → `failed`,其余 → `success`)。\n失败与被拒绝的请求同样落库。\n\n## 查询接口:GET /admin/audit/list\n\n**参数**\n\n| 参数 | 默认 | 说明 |\n|---|---|---|\n| `page` | 1 | 页码(≥1 |\n| `size` | 20 | 每页条数(1~200;兼容 `pageSize` |\n| `action` | - | `upload` / `download` / `admin` |\n| `result` | - | `success` / `denied` / `failed` |\n| `ip` | - | 按客户端 IP 过滤 |\n| `start_time` / `end_time` | - | 时间范围,ISO 8601(如 `2025-06-01T00:00:00+08:00`;也接受 `2006-01-02 15:04:05` / 日期) |\n\n```bash\ncurl -s "http://localhost:8466/admin/audit/list?page=1&size=20&action=download&result=success&start_time=2025-06-01T00:00:00%2B08:00" \\\n -H "Authorization: Bearer $TOKEN"\n```\n\n**成功响应**200):\n\n```json\n{\n "code": 200, "msg": "ok",\n "data": {\n "data": [\n {\n "id": 318,\n "action": "download",\n "file_code": "K3P9W",\n "file_name": "report.pdf",\n "size_bytes": 1048576,\n "transferred_bytes": 524288,\n "ip": "203.0.113.7",\n "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",\n "device_os": "Windows",\n "device_browser": "Chrome",\n "device_type": "desktop",\n "actor": "guest",\n "result": "success",\n "error_msg": "",\n "duration_ms": 128,\n "created_at": "2025-06-01T12:03:45+08:00",\n "fileCode": "K3P9W",\n "fileName": "report.pdf",\n "sizeBytes": 1048576,\n "transferredBytes": 524288,\n "userAgent": "Mozilla/5.0 …",\n "deviceOs": "Windows",\n "deviceBrowser": "Chrome",\n "deviceType": "desktop",\n "errorMsg": "",\n "durationMs": 128,\n "createdAt": "2025-06-01T12:03:45+08:00"\n }\n ],\n "total": 1180,\n "page": 1,\n "size": 20\n }\n}\n```\n\n> 行字段以 snake_case 为准;camelCase 为兼容双份输出(文档不再重复列出)。\n\n**错误响应**\n\n```json\n{ "code": 400, "msg": "start_time 时间格式错误" }\n```\n\n```json\n{ "code": 401, "msg": "令牌无效或已过期" }\n```\n\n## 别名:GET /admin/audit/logs\n\n与 `/admin/audit/list` 完全相同(同一 handler 的兼容别名),参数与响应一致。\n\n## 典型查询\n\n```bash\n# 最近的下载行为\ncurl -s "http://localhost:8466/admin/audit/list?action=download&size=50" -H "Authorization: Bearer $TOKEN"\n\n# 某 IP 的全部被拒请求(限流/鉴权失败)\ncurl -s "http://localhost:8466/admin/audit/list?ip=203.0.113.7&result=denied" -H "Authorization: Bearer $TOKEN"\n\n# 今天 0 点以来的上传失败\ncurl -s "http://localhost:8466/admin/audit/list?action=upload&result=failed&start_time=2025-06-01T00:00:00%2B08:00" \\\n -H "Authorization: Bearer $TOKEN"\n```\n',h='# 存储引擎配置\n\n存储引擎只支持三种:**本地磁盘 / S3 / WebDAV**,由进程级环境变量 `FCB_STORAGE_ENGINE` 选择。\n引擎与引擎相关配置在启动时一次性读取(`storage.SetEngineOptions` → `NewEngine`),\n**运行时修改引擎 KV 需重启服务**(管理端 `GET /admin/config/get` 的 `_engine_hint` 亦有提示)。\n\n## 引擎选择\n\n```bash\nFCB_STORAGE_ENGINE=local # 启动默认(KV storage_engine 为空时生效)\nFCB_STORAGE_ENGINE=s3\nFCB_STORAGE_ENGINE=webdav\n```\n\n非法值直接启动失败:`FCB_STORAGE_ENGINE 无效值 "xxx",仅支持 local|s3|webdav`。\n\n**v3 运行时热切换**:管理端 `POST /admin/storage/switch`(或后台设置页「存储引擎」卡)可在不重启的情况下切换引擎——\n先构建新引擎并健康检查,通过才生效;失败 503 保持原引擎。当前引擎持久化在 settings KV `storage_engine`(空=回落启动值)。\n各引擎参数(存储目录/服务地址/存储桶/密钥)同样在后台设置页运行时可改;保存后对应引擎实例缓存失效,下次切换/构建生效。\n\n## 文件归属引擎(v3\n\n每条分享记录(`file_codes.engine`)与上传会话(`upload_chunks.engine` / `presign_upload_sessions.engine`\n在创建时戳记当时的引擎名。下载、分片合并、删除按**归属引擎**操作——切换引擎后,旧引擎里的文件仍可正常下载与删除\n(空戳为历史数据,回落当前引擎)。\n\n## 按日期目录存储\n\n文件落盘路径:`[storage_path/]share/data/YYYY/MM/DD/<uuid>/<文件名>`(如 `share/data/2026/09/04/…`)。\n按日嵌套目录自然排序、无日月歧义、避免单日海量文件挤在单目录;三种引擎一致适用;历史路径记录在\n`file_codes.file_path`,不受路径规则调整影响。\n\n## 配置键与环境变量\n\n引擎相关配置键(DB settings KV)可由环境变量种子注入(优先级:默认 < 环境变量 < DB KV):\n\n| KV 键 | 环境变量 | 引擎 | 说明 |\n|---|---|---|---|\n| `local_storage_path` | `FCB_LOCAL_STORAGE_PATH` | local | 本地存储根目录(容器内默认 `/app/data` |\n| `storage_path` | `FCB_STORAGE_PATH` | 全部 | 存储相对路径前缀(空 = `share/data/…` |\n| `s3_access_key_id` | `FCB_S3_ACCESS_KEY_ID` | s3 | 访问密钥 |\n| `s3_secret_access_key` | `FCB_S3_SECRET_ACCESS_KEY` | s3 | 私有密钥 |\n| `aws_session_token` | `FCB_AWS_SESSION_TOKEN` | s3 | 可选临时会话令牌 |\n| `s3_bucket_name` | `FCB_S3_BUCKET_NAME` | s3 | 桶名 |\n| `s3_endpoint_url` | `FCB_S3_ENDPOINT_URL` | s3 | S3 兼容端点(MinIO/R2 等;AWS 原生可空) |\n| `s3_region_name` | `FCB_S3_REGION_NAME` | s3 | 区域(默认 `auto` |\n| `s3_addressing_style` | `FCB_S3_ADDRESSING_STYLE` | s3 | `auto`/`path`/`virtual` |\n| `webdav_url` | `FCB_WEBDAV_URL` | webdav | WebDAV 服务地址(如 `http://webdav:5000` |\n| `webdav_username` | `FCB_WEBDAV_USERNAME` | webdav | 用户名 |\n| `webdav_password` | `FCB_WEBDAV_PASSWORD` | webdav | 密码 |\n| `webdav_root_path` | `FCB_WEBDAV_ROOT_PATH` | webdav | 根目录(默认 `filebox_storage`,不存在自动逐级创建) |\n\n`FCB_STORAGE_ENGINE` 本身不落库(`GET /admin/config/get` 中的 `file_storage` 为 KV 记忆键,\n进程实际引擎以 `FCB_STORAGE_ENGINE` 与 `_engine_hint.storage_backend` 为准)。\n\n## 各引擎要点\n\n### 本地引擎(local\n\n- 原子写:临时文件 + fsync + rename,避免半写文件。\n- 路径安全:`SanitizePath` + 符号链接逃逸双重防穿越。\n- Range 下载基于 `SectionReader`;分片按索引有序合并 + SHA256 校验,合并后清理分片目录。\n\n### S3 引擎(s3\n\n- 原生 multipart 流式合并(失败自动 Abort),分片落临时文件保证精确 Content-Length 与可重放。\n- 预签名 GET/PUT 直链(预签名直传唯一 `direct` 模式引擎)。\n- SDK 内置 5xx 指数退避重试;`when_required` 校验模式兼容 MinIO/R2 与纯流式转发。\n\n### WebDAV 引擎(webdav,重点优化)\n\n- **连接复用**:池化 Transport,连接复用(实测 25 次请求仅 1 条 TCP 连接)。\n- **认证**Basic + DigestRFC 2617 `qop=auth`MD5/SHA-256)自动协商,401 挑战驱动。\n- **Range**`Range` 头透传 + `206` 解析,支持分块/断点下载。\n- **重试**:5xx/429/408 指数退避(封顶 2s ± 20% 抖动,尊重 `Retry-After`)。\n- **流式**:下载经 `io.Pipe` 流式转发不落盘;上下文取消挂到响应体读完之后,防止提前断连。\n- **目录**:按需逐级 `MKCOL` + 目录缓存,避免重复建目录。\n- **超时**:可配置(`webdav_url` 同级暂无独立超时键,引擎默认值内置)。\n\n## 健康检查\n\n三引擎均实现 `HealthCheck`local 写探针、s3 `ListObjectsV2`、webdav `PROPFIND`(根目录不存在时自建)。\n服务启动时预检失败仅告警不阻断;运行状态可经 `GET /api/v1/health` 的 `data.storage` 查看当前引擎名。\n\n## 预签名直传支持矩阵\n\n| 引擎 | `PresignPutURL` / `PresignGetURL` | init 返回 mode |\n|---|---|---|\n| s3 | ✅ | `direct` |\n| local / webdav | ❌(`ErrNotSupported` | `proxy`(走服务端代理上传) |\n\n引擎不支持的操作经统一映射返回 501\n\n```json\n{ "code": 501, "msg": "当前存储引擎不支持该操作" }\n```\n\n## Docker Compose 冒烟编排\n\n`deploy/docker-compose.yml` 提供可选 profile(详见 deploy/README.md):\n\n```bash\ndocker compose --profile minio up -d --build # MinIO(含 mc 自动建桶)+ FCB_STORAGE_ENGINE=s3\ndocker compose --profile webdav up -d --build # dufs WebDAV 冒烟(admin/admin123+ FCB_STORAGE_ENGINE=webdav\ndocker compose --profile redis up -d --build # Redis 缓存增强(非引擎)\n```\n\n## 存储哨兵错误 → HTTP 状态\n\n| 哨兵错误 | HTTP | 文案 |\n|---|---|---|\n| `ErrNotFound` | 404 | 文件不存在 |\n| `ErrInvalidPath` | 400 | 非法文件路径 |\n| `ErrUnavailable` | 503 | 存储服务不可用,请稍后再试 |\n| `ErrNotSupported` | 501 | 当前存储引擎不支持该操作 |\n| `ErrRangeNotSatisfiable` | 416 | 请求范围超出文件大小 |\n| `ErrHashMismatch` | 400 | 分片哈希校验失败,请重新上传 |\n\n容量超限(`storageLimit`,经容量预留判定)返回 507:`存储空间已达到管理员设置的容量上限`。\n',f='# 环境变量与配置项\n\n配置分三层:**默认值 → `FCB_*` 环境变量 → 数据库 settings KV(管理端运行时修改)**。\nv2 起进程必需的环境变量为空集:数据库默认 **SQLite**modernc.org/sqlite 纯 Go 驱动,零外部依赖,\nDSN 缺省落 `./data/fileshare.db`);`FCB_DB_DRIVER=postgres` 时 `FCB_DB_DSN` 必需(需求 ⑧)。\n\n## 环境变量(进程级)\n\n| 变量 | 必需 | 默认 | 说明 |\n|---|---|---|---|\n| `FCB_DB_DRIVER` | ❌ | `sqlite` | 数据库驱动:`sqlite` / `postgres`(需求 ⑧) |\n| `FCB_DB_DSN` | 视驱动 | `./data/fileshare.db` | postgres:连接串(**必需**,如 `postgres://user:pass@host:5432/filecodebox?sslmode=disable`);sqlite:数据库文件路径(可空,父目录自动创建) |\n| `FCB_REDIS_ADDR` | ❌ | 空 | Redis 地址(如 `redis:6379`),也支持 `redis://[:password@]host:port[/db]` / `rediss://`TLSURL 形式;**空则缓存降级为进程内存实现**(缓存故障时自动降级为进程内限流计数) |\n| `FCB_REDIS_DB` | ❌ | `0` | Redis 逻辑库号 0-15URL 形式地址显式携带 `/N` 时以 URL 为准 |\n| `FCB_ADMIN_PASSWORD` | ❌ | 空 | 设置后服务首次启动即自动初始化管理员(≥8 位,不足告警跳过),消除 `/setup` 被抢占窗口;初始化完成后建议移除 |\n| `FCB_LISTEN` | ❌ | `:8466` | HTTP 监听地址 |\n| `FCB_STORAGE_ENGINE` | ❌ | `local` | 存储引擎:`local` / `s3` / `webdav` |\n| `FCB_TRUSTED_PROXIES` | ❌ | 空 | 可信代理 CIDR(逗号分隔),命中时从 `X-Forwarded-For` 解析真实客户端 IP |\n\n- SQLite 连接参数(驱动自动注入):`busy_timeout=10s` + `WAL` 日志模式 + `foreign_keys=1`;连接池 8/4。\n- Postgres 连接池沿用 v1 参数(32/81h 轮换);`FCB_DB_DRIVER=postgres` 且未设 `FCB_DB_DSN` 时**启动直接报错**。\n\n引擎相关环境变量(种子注入 settings KV,见《存储引擎配置》):`FCB_LOCAL_STORAGE_PATH`、\n`FCB_STORAGE_PATH`、`FCB_S3_ACCESS_KEY_ID`、`FCB_S3_SECRET_ACCESS_KEY`、`FCB_AWS_SESSION_TOKEN`、\n`FCB_S3_BUCKET_NAME`、`FCB_S3_ENDPOINT_URL`、`FCB_S3_REGION_NAME`、`FCB_S3_ADDRESSING_STYLE`、\n`FCB_WEBDAV_URL`、`FCB_WEBDAV_USERNAME`、`FCB_WEBDAV_PASSWORD`、`FCB_WEBDAV_ROOT_PATH`。\n\n部署用编排变量(`deploy/.env.example`):`WEB_PORT`(默认 8466)、\n`POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB`(默认 fileshare,仅 `--profile postgres` 时使用)。\n\n## 配置项(settings KV,默认值对齐参考实现)\n\n> 键名/类型/默认值/边界以 `server/internal/config/schema.go` 的 `KVSchema()` 为单一事实来源\n> schema 同步测试保证与 defaults() 逐键一致);v2 新增键统一 snake_case。\n\n### 站点信息与展示(需求 ①②③)\n\n| 键 | 类型/边界 | 默认 | 说明 |\n|---|---|---|---|\n| `site_name` / `name` | string | 文件快传 | 站点名称(`site_name` 优先) |\n| `site_domain` | string,≤256 | 空 | **v3.1**:站点对外域名(`http(s)://host[:port]`,不带路径;裸主机自动补 `http://`)。配置后分享链接(结果卡/管理端复制)用该域名生成——内网部署也能把公网链接发出去;留空=用当前访问地址 |\n| `description` | string | 开箱即用的文件快传系统 | 站点描述 |\n| `page_explain` | string | (合规声明) | 页面说明文案 |\n| `keywords` | string | 文件快传, 文件分享… | SEO 关键词 |\n| `logo_url` | string | 空(前端回落本地打包 `/assets/logo-*.svg`,需求 ⑤) | 页面导航 Logo,管理端可设任意 URL |\n| `favicon_url` | string | 空(前端回落本地打包 `/assets/favicon-*.png`,需求 ⑤) | favicon / 备用 Logo |\n| `opacity` | float | 0.9 | 界面不透明度 |\n| `background` | string | 空 | 背景图 URL(参考实现既有键,v1 兼容保留) |\n| `background_url` | string,≤2048 字符 | 空 | **v2 需求 ①**:背景图 URL 或上传后地址(空=主题默认;取值时 legacy `background` 键兜底)。管理端保存时校验协议白名单:仅 `http(s)`、`data:image/*` 与站内相对路径(防 `javascript:` 注入,非法 400 |\n| `footer_text` | string,≤2000 字符 | 空 | **v2 需求 ②**:页脚自定义内容(纯文本或受控 HTML 片段) |\n| `footer_beian` | string,≤128 字符 | 空 | **v2 需求 ②**:备案号(如 `京ICP备2024xxxxxx号-1`),展示于页脚 |\n| `notify_enabled` | int0/1 | 1 | **v2 需求 ③**:通知开关(1=前台右上角悬浮窗展示 / 0=关闭) |\n| `notify_title` | string,≤128 字符 | 系统通知 | 通知标题 |\n| `notify_content` | string,≤2000 字符 | 欢迎使用… | 通知正文(**服务端白名单净化**:仅保留纯文本与 `<a href>` 为 http(s)/站内相对/`#` 锚点的链接,其余标签与事件属性剥离,保存与读取双侧生效) |\n| `showAdminAddr` | int0/1 | 0 | 是否展示后台入口 |\n| `robotsText` | string | `User-agent: *\\nDisallow: /` | robots.txt 内容(由公开端点 `GET /robots.txt` 输出) |\n\n### 保存策略(需求 ④,上传页动态读取并在范围内选择)\n\n| 键 | 类型/边界 | 默认 | 说明 |\n|---|---|---|---|\n| `max_save_seconds` | int640~31536000 | 0 | 最长保存秒数上限(0=仅默认 7 天兜底;>0 时按时间过期超限 403「限制最长时间为 X,可换用其他方式」)。**v3**:管理界面以「小时/天」下拉单位编辑(≥1 天自动显示天),提交时前端换算为秒——canonical 单位保持秒,接口语义不变 |\n| `max_save_count` | int0~100000 | 0 | **v2 新增**:单次分享最大可取(保存)次数上限(0=不限制;`expire_style=count` 且 `expire_value` 超上限时 403「限制次数最多为 N 次」) |\n| `expireStyle` | []string | `["day","hour","minute","forever","count"]` | 允许的过期方式白名单(上传时不在白名单 400「过期时间类型错误」) |\n\n### 存储策略(需求 ④⑩)\n\n| 键 | 类型/边界 | 默认 | 说明 |\n|---|---|---|---|\n| `uploadSize` | int641024~10GiB | 1048576010MB) | 单文件大小上限(字节),参考实现语义;`max_file_size=0` 时作为生效上限 |\n| `max_file_size` | int640~10GiB | 0 | **v2 新增**:存储策略-单文件上限(字节),0=回落 `uploadSize`;超出 403(文案 humanSize 自适应 B/KB/MB/GB)。**v3**:管理界面以「MB/GB」下拉单位编辑(≥1 GiB 自动显示 GB),提交时前端换算为字节 |\n| `allowed_file_types` | []string | `["*"]` | 允许类型白名单(扩展名/MIME 通配,`*` 不限制;非白名单 403「不允许上传该类型文件」) |\n| `storageLimit` | int64,≥0 | 0 | 站点总容量(字节),0=不限制(超限 507 |\n| `openUpload` | int0/1 | 1 | 游客上传开关(0 时上传接口要求管理员令牌 403 |\n| `enableChunk` | int0/1 | 0 | 启用分片上传 |\n\n### 上传频率限制(需求 ④,既有键对齐参考 ip_limit["upload"]\n\n| 键 | 类型/边界 | 默认 | 说明 |\n|---|---|---|---|\n| `uploadCount` / `uploadMinute` | int1~10000 / 1~1440 | 10 / 1 | 窗口内允许上传次数 / 窗口分钟(上传成功才计数,超限 423;管理端修改后运行时同步限流规则,立即生效) |\n| `errorCount` / `errorMinute` | int | 10 / 1 | 取件错误(失败计数)+ metadata 每次计数 |\n| `loginCount` / `loginMinute` | int | 5 / 15 | 登录失败计数 |\n\n### 安全与会话\n\n| 键 | 默认 | 说明 |\n|---|---|---|\n| `admin_token` | 空(未初始化) | 管理员密码哈希(`sha256$salt$hash`);GET 配置时屏蔽为空串 |\n| `jwt_secret` | 空 | JWT 签名密钥(初始化/改密时自动生成轮换;不下发;`settings.SensitiveKeys` 双模式下一致屏蔽) |\n| `adminSessionExpire` | 6048007 天) | 管理员会话秒数(须 1~365 整天) |\n\n### 存储引擎(v3 运行时可配 + 热切换)\n\n**`storage_engine`**v3 新增键):string`local|s3|webdav`,默认空=回落启动值 `FCB_STORAGE_ENGINE`。\n运行时切换走 **`POST /admin/storage/switch`**JWT 保护):构建新引擎 → 健康检查通过才生效;\n失败返回 503「存储引擎切换失败,已保持原引擎: …」且不改 KV。成功后持久化 `storage_engine`,重启沿用。\n`GET /api/v1/config` 公开下发 `storage_engine` 当前名(仅名称,任何引擎参数/凭据不下发)。\n\n引擎参数键(管理端可改;保存后对应引擎实例缓存失效,下次切换/构建生效):\n\n| 键 | 默认 |\n|---|---|\n| `file_storage` | `local` |\n| `storage_path` | 空 |\n| `local_storage_path` | 空(容器内由 `FCB_LOCAL_STORAGE_PATH=/app/data` 注入) |\n| `s3_access_key_id` / `s3_secret_access_key` / `aws_session_token` | 空 |\n| `s3_bucket_name` / `s3_endpoint_url` / `s3_hostname` | 空 |\n| `s3_region_name` | `auto` |\n| `s3_signature_version` | `s3v4` |\n| `s3_addressing_style` | `auto` |\n| `s3_proxy` | 0 |\n| `webdav_url` / `webdav_username` / `webdav_password` | 空 |\n| `webdav_root_path` | `filebox_storage` |\n| `webdav_proxy` | 0 |\n\n> 敏感键 `webdav_password` / `s3_secret_access_key` / `aws_session_token`v3 加入 `settings.SensitiveKeys`):\n> 管理端 GET 返回掩码 `******`PATCH 时空串或 `******` 表示不修改。直接写库 settings KV 后重启同样生效。\n\n## 策略动态生效机制(v2 需求 ④⑩)\n\n上传页通过 `GET /api/v1/config` 的 `config` 字段读取**当前策略快照**并在范围内渲染选项;\n上传链路(`/share/file`、`/chunk/*`、`/presign/*`**每次请求实时读取** settings KV 同一组值校验:\n\n- 管理端改策略(`PATCH /admin/config/update`)→ 公开 config 即时反映 → 后续上传立即按新策略执行(含 403/400 拒绝与恢复放行)。\n- 生效上限:`max_file_size > 0` 时为 `max_file_size`,否则回落 `uploadSize`。\n- 校验点覆盖:单文件(`/share/file`)、分片 init 按分片数上限、分片上传累计、分片 complete 累计、预签名 init 声明大小,五处口径一致(`api.UploadPolicy.CheckSize`)。\n\n## 公共配置接口\n\n前端启动时经 `GET /api/v1/config` 获取站点公开配置(无需认证;v2 扩展需求 ①②③④⑩):\n\n```json\n{\n "code": 200, "msg": "ok",\n "data": {\n "config": {\n "name": "文件快传",\n "description": "开箱即用的文件快传系统",\n "explain": "请勿上传或分享违法内容…",\n "logo_url": "",\n "favicon_url": "",\n "background_url": "",\n "footer_text": "自定义页脚内容",\n "footer_beian": "京ICP备2024xxxxxx号-1",\n "notify_enabled": 1,\n "notify_title": "系统通知",\n "notify_content": "欢迎使用文件快传…",\n "uploadSize": 10485760,\n "max_file_size": 10485760,\n "maxFileSize": 10485760,\n "allowedFileTypes": ["*"],\n "expireStyle": ["day", "hour", "minute", "forever", "count"],\n "max_save_seconds": 0,\n "maxSaveSeconds": 0,\n "max_save_count": 0,\n "maxSaveCount": 0,\n "uploadCount": 10,\n "uploadMinute": 1,\n "enableChunk": false,\n "openUpload": true\n },\n "meta": {\n "version": "26.9",\n "features": { "chunkUpload": false, "guestUpload": true }\n }\n }\n}\n```\n\n> 策略字段 snake_case 与 camelCase 双份下发(前端宽松解析);响应为白名单显式构造,\n> 任何敏感键(`admin_token`/`jwt_secret`)均不会出现。\n\n## 健康检查\n\n```bash\ncurl -s http://localhost:8466/api/v1/health\n```\n\n```json\n{\n "code": 200, "msg": "ok",\n "data": {\n "status": "ok",\n "version": "26.9",\n "storage": "local",\n "time": "2025-06-01T12:00:00+08:00"\n }\n}\n```\n',x='# 错误码\n\n## 响应结构\n\n```json\n{ "code": 404, "msg": "文件不存在" }\n```\n\n- `code` 与 HTTP 状态码一致;失败时无 `data` 字段。\n- `msg` 为中文可读信息,可直接展示给用户。\n\n## 业务状态码\n\n| 状态码 | 语义 | 典型场景 |\n|---|---|---|\n| 200 | 成功 | 全部正常响应 |\n| 400 | 参数/格式错误 | 缺字段、过期策略非法、时间格式错误、分片哈希不匹配、code 冲突、`chunk_size` 超 32MiB 上限、presign 实际大小与声明不符、请求体超过大小上限 |\n| 401 | 未认证 | 管理端令牌缺失/无效;登录密码错误 |\n| 403 | 拒绝 | 类型白名单拒绝、magic bytes 防伪、游客上传未开启、**分片上传未启用**enableChunk=0)、presign 直传对象超限(服务端删除对象并释放预留)、下载 `key` 鉴权失败、超过大小/时长限制 |\n| 404 | 不存在/已过期 | 取件码不存在、文件已过期、上传会话不存在、`/api/*` 未命中路由 |\n| 409 | 冲突 | 上传容量预留信息不一致 |\n| 416 | Range 越界 | `Range: bytes=…` 超出文件大小 |\n| 423 | 限流 | upload/error/login/metadata 任一规则超限 |\n| 428 | 未初始化 | 系统未初始化时访问除 `/setup`、`/api/v1/health` 外的接口 |\n| 500 | 服务器错误 | 数据库/内部异常 |\n| 501 | 引擎不支持 | 引擎不支持预签名等操作(local/webdav 的 `PresignGetURL/PutURL` |\n| 503 | 存储不可用 | 存储引擎连接失败/健康检查不通过时的操作 |\n| 507 | 容量超限 | 达到 `storageLimit` 上限(含上传预留判定) |\n\n## 存储哨兵错误映射\n\n存储层哨兵错误统一映射(支持错误包装链判定):\n\n| 哨兵错误 | HTTP | 响应 msg |\n|---|---|---|\n| `ErrNotFound` | 404 | 文件不存在 |\n| `ErrInvalidPath` | 400 | 非法文件路径 |\n| `ErrUnavailable` | 503 | 存储服务不可用,请稍后再试 |\n| `ErrNotSupported` | 501 | 当前存储引擎不支持该操作 |\n| `ErrRangeNotSatisfiable` | 416 | 请求范围超出文件大小 |\n| `ErrHashMismatch` | 400 | 分片哈希校验失败,请重新上传 |\n\n未识别的存储错误归入 500(`存储操作失败: …`)。\n\n## 错误结果的审计归类\n\n错误响应同时写入审计日志(需求 ③):\n\n- `denied`401 / 403 / 423 / 429 / 428(拒绝类)。\n- `failed`:其余 4xx / 5xx 及业务显式报错。\n\n## 常见排障\n\n| 现象 | 原因与处理 |\n|---|---|\n| 全部接口 428 | 未初始化:访问 `GET /setup` 或 `POST /setup` 完成向导 |\n| 上传 403「本站未开启游客上传」 | `openUpload=0`,携带管理员 Bearer 令牌或后台开启 |\n| 上传 423 | 触发 upload 限流,等待窗口或调大 `uploadCount/uploadMinute` |\n| 取件 404「文件已过期」 | 分享过期/次数耗尽;管理员可 `PATCH /admin/file/update` 调整 |\n| 下载 403「下载鉴权失败」 | `key` 窗口令牌过期/伪造:重新 `POST /share/select` 获取新地址 |\n| 预签名 init 返回 proxy | local/webdav 引擎不支持直链,按 proxy 流程走服务端代理上传 |\n| 503 存储服务不可用 | 检查引擎配置与远端服务(S3/WebDAV)连通性;`GET /api/v1/health` 的 `storage` 字段确认引擎 |\n',b='# Logo 自定义\n\n## 默认 Logo(内置,v2 需求 ⑤)\n\n| 项 | 默认值 | 用途 |\n|---|---|---|\n| 页面导航 Logo | 前端打包本地资源 `/assets/logo-*.svg`(源:`web/src/assets/brand/logo.svg` | 导航栏 `<img>``config.logo_url` 为空时回落使用 |\n| favicon / 备用 Logo | 前端打包本地资源 `/assets/favicon-*.png`(源:`web/src/assets/brand/favicon.png` | `index.html` `<link rel="icon">` + 动态 favicon 回落 |\n\nv2 起默认不再引用远程 URL`GET /api/v1/config` 中 `logo_url`/`favicon_url` 默认下发空串,\n前端 `displayLogoUrl`/`displayFaviconUrl` 判空后回落到打包的本地资源。\n管理端仍可设置任意 URL 全站替换(三步如下)。\n\n## 管理端自定义(三步)\n\n1. **登录后台**`POST /admin/login` 获取 Bearer 令牌。\n2. **保存配置**`PATCH /admin/config/update` 更新 `logo_url`(与可选 `favicon_url`),值为图片 URL 或经管理端上传后得到的地址。\n3. **全站生效**:保存即写入 settings KV 并热更新,前端读取公共配置立即换新 Logo,无需重启。\n\ncurl 示例:\n\n```bash\ncurl -s -X PATCH http://localhost:8466/admin/config/update \\\n -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\\n -d \'{"logo_url":"https://cdn.example.com/logo.svg","favicon_url":"https://cdn.example.com/favicon.png"}\'\n```\n\n```json\n{ "code": 200, "msg": "ok", "data": { "ok": true } }\n```\n\n> 也可以在管理界面「系统设置」页操作(上传图片或填写 URL),效果相同。\n\n## 校验生效\n\n```bash\ncurl -s http://localhost:8466/api/v1/config\n```\n\n```json\n{\n "code": 200, "msg": "ok",\n "data": { "config": { "logo_url": "https://cdn.example.com/logo.svg", "favicon_url": "https://cdn.example.com/favicon.png" } }\n}\n```\n\n## 恢复默认\n\n把 `logo_url` / `favicon_url` 置回默认值(空串,前端回落本地打包资源)即可:\n\n```bash\ncurl -s -X PATCH http://localhost:8466/admin/config/update \\\n -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\\n -d \'{"logo_url":"","favicon_url":""}\'\n```\n\n## 相关行为\n\n- 前端运行时优先读取配置值;空值回退前端打包的本地资源(`web/src/assets/brand/logo.svg` + `favicon.png`,经 `displayLogoUrl`/`displayFaviconUrl` 判空回落)。\n- `site_name` 同样支持运行时自定义(`PATCH /admin/config/update` 的 `site_name` 键)。\n- Logo/favicon 仅涉及展示层,修改不影响会话与令牌(不轮换 `jwt_secret`)。\n',T=`openapi: 3.0.3
info:
title: 文件快传 Go 版 API
version: 2.5.6
description: |
文件快传 Go 重写版(Gin + GORM)完整 API 规范,与 \`server/internal/api/\` 实现逐一对齐。
约定:
- 业务路由挂根路径(/share /chunk /presign /admin),公共接口保留 /api/v1 前缀。
- 统一响应 \`{"code":200,"msg":"ok","data":...}\`HTTP 状态码与 code 一致,失败时 data 缺省。
- 管理接口(/admin/login 除外)需 \`Authorization: Bearer <JWT>\`。
- 未初始化时除 /setup 与 /api/v1/health 外一律 428;限流超限 423。
contact:
name: 文件快传 Rewrite Team
servers:
- url: /
description: 同源部署(默认 :8466
tags:
- name: 公共
- name: 初始化
- name: 分享
- name: 分片上传
- name: 预签名直传
- name: 管理后台
- name: 审计日志
paths:
/api/v1/health:
get:
tags: [公共]
summary: 健康检查
operationId: health
responses:
'200':
description: 服务状态
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
properties:
status: { type: string, example: ok }
version: { type: string, example: 2.5.6 }
storage: { type: string, example: local }
time: { type: string, example: '2025-06-01T12:00:00+08:00' }
/robots.txt:
get:
tags: [公共]
summary: robots.txt
operationId: robotsText
description: |
输出管理端可配置的 \`robotsText\` 内容(text/plain)。
内容可在管理后台「站点配置」中自定义,默认 \`User-agent: *\\nDisallow: /\`。
responses:
'200':
description: robots.txt 内容
content:
text/plain:
schema: { type: string, example: 'User-agent: *\\nDisallow: /' }
/api/v1/config:
get:
tags: [公共]
summary: 公共站点配置
operationId: publicConfig
responses:
'200':
description: 站点公开配置与功能开关
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
properties:
config:
type: object
description: 策略字段为上传页动态读取的当前快照(管理端改后即时反映);snake_case 与 camelCase 双份
properties:
name: { type: string }
description: { type: string }
explain: { type: string }
logo_url: { type: string }
favicon_url: { type: string }
background_url: { type: string, description: '需求 ① 背景图 URL/上传地址,空=主题默认' }
footer_text: { type: string, description: '需求 ② 页脚自定义内容' }
footer_beian: { type: string, description: '需求 ② 备案号' }
storage_engine: { type: string, description: 当前存储引擎名(仅名称,凭据不下发) }
site_domain: { type: string, description: 'v3.1 站点对外域名(空=用当前访问地址)' }
notify_enabled: { type: integer, enum: [0, 1], description: '需求 ③ 通知开关(1=右上角悬浮窗)' }
notify_title: { type: string }
notify_content: { type: string }
uploadSize: { type: integer, format: int64, description: '回落上限(max_file_size=0 时生效)' }
max_file_size: { type: integer, format: int64, description: '需求 ⑩ 存储策略-单文件上限字节(0=回落 uploadSize' }
maxFileSize: { type: integer, format: int64, description: 同 max_file_size }
allowedFileTypes: { type: array, items: { type: string } }
expireStyle: { type: array, items: { type: string } }
max_save_seconds: { type: integer, format: int64, description: '需求 ④ 最长保存秒数上限(0=默认 7 天兜底)' }
maxSaveSeconds: { type: integer, format: int64, description: 同 max_save_seconds }
max_save_count: { type: integer, description: '需求 ④ 单次分享最大可取次数上限(0=不限制)' }
maxSaveCount: { type: integer, description: 同 max_save_count }
uploadCount: { type: integer, description: 上传频率限制-窗口内次数 }
uploadMinute: { type: integer, description: 上传频率限制-窗口分钟 }
enableChunk: { type: boolean }
openUpload: { type: boolean }
meta:
type: object
properties:
version: { type: string }
features:
type: object
properties:
chunkUpload: { type: boolean }
guestUpload: { type: boolean }
/setup:
get:
tags: [初始化]
summary: 初始化向导页面
operationId: setupPage
description: 未初始化返回 HTML 向导;已初始化 303 重定向到 /。
responses:
'200':
description: HTML 向导页
content:
text/html:
schema: { type: string }
'303':
description: 已初始化,重定向到 /
post:
tags: [初始化]
summary: 提交初始化
operationId: setupSubmit
description: |
表单或 JSON。管理员密码 ≥8 位;\`expireStyle\` 支持多值(复选框或逗号分隔)。
成功写入站点配置、密码哈希并生成 jwt_secret。表单提交返回成功 HTMLJSON 提交返回 JSON。
requestBody:
content:
application/json:
schema:
type: object
properties:
admin_password: { type: string, minLength: 8 }
confirm_password: { type: string }
site_name: { type: string }
upload_size_value: { type: integer, default: 10 }
upload_size_unit: { type: string, enum: [KB, MB, GB], default: MB }
save_time_value: { type: integer, default: 0 }
save_time_unit: { type: string, enum: [second, minute, hour, day], default: day }
expireStyle:
oneOf:
- type: string
- type: array
items: { type: string }
code_generate_type: { type: string, enum: [number, secret], default: secret }
errorCount: { type: integer, default: 10 }
errorMinute: { type: integer, default: 1 }
loginCount: { type: integer, default: 5 }
loginMinute: { type: integer, default: 15 }
uploadCount: { type: integer, default: 10 }
uploadMinute: { type: integer, default: 1 }
allowed_file_types: { type: string, default: '*' }
openUpload: { type: boolean, default: true }
enableChunk: { type: boolean, default: false }
application/x-www-form-urlencoded:
schema:
type: object
properties:
admin_password: { type: string }
confirm_password: { type: string }
site_name: { type: string }
responses:
'200':
description: 初始化成功
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
properties:
ok: { type: boolean, example: true }
admin: { type: string, example: '/#/admin' }
text/html:
schema: { type: string }
'400':
$ref: '#/components/responses/BadRequest'
/share/text:
post:
tags: [分享]
summary: 创建文本分享
operationId: shareText
description: |
文本 ≤222KB(请求体全局上限 1MiBContent-Length>441KB 直接 403);上传类接口(成功计数 upload 限流,423 超限),经审计中间件落库。
保存策略(需求 ④):expire_style 须在 expireStyle 白名单(400);count 型受
max_save_count 约束(403「限制次数最多为 N 次」);时间型受 max_save_seconds 约束(403)。
v3.1:支持 JSON 提交(字段同名);空文本 400「分享内容不能为空」;
可选 code 自定义提取码(5-8 位字母或数字,占用 400「该提取码已被占用」)。
requestBody:
content:
application/x-www-form-urlencoded:
schema:
type: object
required: [text]
properties:
text: { type: string, maxLength: 227328 }
expire_value: { type: integer, default: 1 }
expire_style: { type: string, enum: [day, hour, minute, count, forever], default: day }
code: { type: string, minLength: 5, maxLength: 8, pattern: '^[A-Za-z0-9]+$', description: 自定义提取码(5-8 位字母数字,可省略=随机) }
responses:
'200':
description: 取件码
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
properties:
code: { type: string, example: 8XQ2M }
'400': { $ref: '#/components/responses/BadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'423': { $ref: '#/components/responses/RateLimited' }
'428': { $ref: '#/components/responses/NotInitialized' }
/share/file:
post:
tags: [分享]
summary: 上传文件并创建分享
operationId: shareFile
description: |
multipart 上传;扩展名/MIME 白名单 + magic bytes 防伪;大小受动态策略上限限制
max_file_size>0 时为其,否则回落 uploadSize;403「大小超过限制,最大为10.00 MB」,
管理端改后立即生效);保存策略同 /share/text403/400);容量超限 507。
requestBody:
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
code: { type: string, minLength: 5, maxLength: 8, pattern: '^[A-Za-z0-9]+$', description: 自定义提取码(5-8 位字母数字,可省略=随机) }
file:
type: string
format: binary
expire_value: { type: integer, default: 1 }
expire_style: { type: string, enum: [day, hour, minute, count, forever], default: day }
responses:
'200':
description: 取件码与文件名
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
properties:
code: { type: string, example: K3P9W }
name: { type: string, example: report.pdf }
'400': { $ref: '#/components/responses/BadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'423': { $ref: '#/components/responses/RateLimited' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
'507': { $ref: '#/components/responses/InsufficientStorage' }
'428': { $ref: '#/components/responses/NotInitialized' }
/share/metadata:
get:
tags: [分享]
summary: 查询分享元信息(GET
operationId: shareMetadataGet
description: 不消耗次数;每次访问计入 metadata 限流。
parameters:
- $ref: '#/components/parameters/CodeQuery'
responses:
'200':
description: 元信息
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ShareMetadata' }
'404': { $ref: '#/components/responses/NotFound' }
'423': { $ref: '#/components/responses/RateLimited' }
'428': { $ref: '#/components/responses/NotInitialized' }
post:
tags: [分享]
summary: 查询分享元信息(POST
operationId: shareMetadataPost
requestBody:
content:
application/json:
schema:
type: object
required: [code]
properties:
code: { type: string }
responses:
'200':
description: 元信息(同 GET 版本)
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ShareMetadata' }
'400': { $ref: '#/components/responses/BadRequest' }
'404': { $ref: '#/components/responses/NotFound' }
'423': { $ref: '#/components/responses/RateLimited' }
'428': { $ref: '#/components/responses/NotInitialized' }
/share/select:
get:
tags: [分享]
summary: 取件(消耗次数)
operationId: shareSelectGet
description: |
每调用一次消耗 1 次取件。文本分享返回 text/plain 正文;文件分享返回文件流
(200 全量 / 206 区间,支持 RangeAccept-Ranges: bytes)。失败计入 error 限流(423)。
parameters:
- $ref: '#/components/parameters/CodeQuery'
- name: Range
in: header
required: false
schema: { type: string, example: 'bytes=0-1023' }
description: 文件分享的区间请求(单区间)
responses:
'200':
description: 文本正文或文件流
content:
text/plain:
schema: { type: string }
application/octet-stream:
schema: { type: string, format: binary }
'206':
description: Range 区间文件流(Content-Range
content:
application/octet-stream:
schema: { type: string, format: binary }
'404': { $ref: '#/components/responses/NotFound' }
'416': { $ref: '#/components/responses/RangeNotSatisfiable' }
'423': { $ref: '#/components/responses/RateLimited' }
'428': { $ref: '#/components/responses/NotInitialized' }
post:
tags: [分享]
summary: 取件详情(JSON
operationId: shareSelectPost
description: |
返回元信息 + 内容/下载地址。文本分享含 text/content;文件分享含 download_url
(代理地址或 S3 预签名直链)。次数型分享返回代理地址且本次不消耗(消耗在代理下载时)。
requestBody:
content:
application/json:
schema:
type: object
required: [code]
properties:
code: { type: string }
responses:
'200':
description: 分享详情
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ShareDetail' }
'404': { $ref: '#/components/responses/NotFound' }
'423': { $ref: '#/components/responses/RateLimited' }
'428': { $ref: '#/components/responses/NotInitialized' }
/share/download:
get:
tags: [分享]
summary: 代理下载(消耗次数)
operationId: shareDownload
description: key 为窗口令牌(双窗口校验,鉴权失败 403 且计入 error 限流);文件流 200/206 支持 Range。
parameters:
- name: key
in: query
required: true
schema: { type: string }
- $ref: '#/components/parameters/CodeQuery'
responses:
'200':
description: 文本 JSON 或文件流
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
application/octet-stream:
schema: { type: string, format: binary }
'206':
description: Range 区间文件流
content:
application/octet-stream:
schema: { type: string, format: binary }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'416': { $ref: '#/components/responses/RangeNotSatisfiable' }
'423': { $ref: '#/components/responses/RateLimited' }
'428': { $ref: '#/components/responses/NotInitialized' }
/chunk/upload/init:
post:
tags: [分片上传]
summary: 初始化分片上传会话
operationId: chunkInit
description: |
相同 file_hash+file_size+file_name 的未完成会话自动续传(uploaded_chunks 非空即续传)。
chunk_size 缺省 5MB、硬上限 32MiB(超出 400「chunk_size 过大」);需 enableChunk 开启(关闭时全部 /chunk 端点 403)。总大小按分片数上限受动态策略约束
max_file_size>0 时为其,否则 uploadSize403);类型白名单 403。
requestBody:
content:
application/json:
schema:
type: object
required: [file_name, file_size]
properties:
file_name: { type: string }
file_size: { type: integer, format: int64, minimum: 1 }
chunk_size: { type: integer, format: int64, default: 5242880, maximum: 33554432, description: 每片大小(字节),≤32MiB }
file_hash: { type: string, description: 整文件 SHA256,断点续传匹配键 }
responses:
'200':
description: 会话信息
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ChunkInitData' }
'400': { $ref: '#/components/responses/BadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'423': { $ref: '#/components/responses/RateLimited' }
'507': { $ref: '#/components/responses/InsufficientStorage' }
'428': { $ref: '#/components/responses/NotInitialized' }
/chunk/upload/{uploadID}/{chunkIndex}:
post:
tags: [分片上传]
summary: 上传单个分片
operationId: chunkUpload
description: multipart 字段 chunk;单分片 ≤ min(chunk_size, 32MiB);重复上传幂等(skipped:true);首分片做 magic bytes 防伪。
parameters:
- $ref: '#/components/parameters/UploadID'
- name: chunkIndex
in: path
required: true
schema: { type: integer, minimum: 0 }
requestBody:
content:
multipart/form-data:
schema:
type: object
required: [chunk]
properties:
chunk: { type: string, format: binary }
responses:
'200':
description: 分片结果
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ChunkUploadData' }
'400': { $ref: '#/components/responses/BadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
/chunk/upload/status/{uploadID}:
get:
tags: [分片上传]
summary: 查询上传进度
operationId: chunkStatus
parameters:
- $ref: '#/components/parameters/UploadID'
responses:
'200':
description: 进度
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ChunkStatusData' }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
/chunk/upload/complete/{uploadID}:
post:
tags: [分片上传]
summary: 合并分片并创建分享
operationId: chunkComplete
description: 分片到齐后调用;按索引有序合并 + SHA256 校验(不完整 400 / 哈希不符 400);累计大小受动态策略上限(403,超限清理会话);保存策略同 /share/text403/400)。
parameters:
- $ref: '#/components/parameters/UploadID'
requestBody:
content:
application/json:
schema:
type: object
properties:
expire_value: { type: integer, default: 1 }
expire_style: { type: string, enum: [day, hour, minute, count, forever], default: day }
responses:
'200':
description: 取件码与文件名
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ShareFileData' }
'400': { $ref: '#/components/responses/BadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'423': { $ref: '#/components/responses/RateLimited' }
'428': { $ref: '#/components/responses/NotInitialized' }
/chunk/upload/{uploadID}:
delete:
tags: [分片上传]
summary: 取消上传
operationId: chunkCancel
parameters:
- $ref: '#/components/parameters/UploadID'
responses:
'200':
description: 已取消
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
properties:
message: { type: string, example: 上传已取消 }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
/presign/upload/init:
post:
tags: [预签名直传]
summary: 初始化预签名上传
operationId: presignInit
description: |
S3 引擎返回 mode=directupload_url 为预签名 PUT URL);local/webdav 返回 mode=proxy
upload_url=/presign/upload/proxy/{uploadID})。会话 900 秒有效。
file_size 受动态策略上限(max_file_size>0 时为其,否则 uploadSize403);保存策略同 /share/text。
requestBody:
content:
application/json:
schema:
type: object
required: [file_name, file_size]
properties:
file_name: { type: string }
file_size: { type: integer, format: int64, minimum: 1 }
expire_value: { type: integer, default: 1 }
expire_style: { type: string, enum: [day, hour, minute, count, forever], default: day }
responses:
'200':
description: 上传会话
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/PresignInitData' }
'400': { $ref: '#/components/responses/BadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'423': { $ref: '#/components/responses/RateLimited' }
'507': { $ref: '#/components/responses/InsufficientStorage' }
'428': { $ref: '#/components/responses/NotInitialized' }
/presign/upload/proxy/{uploadID}:
put:
tags: [预签名直传]
summary: 代理上传(proxy 模式)
operationId: presignProxy
description: multipart 字段 file;实际大小须与声明一致(±1KB);成功即建分享并删除会话。
parameters:
- $ref: '#/components/parameters/UploadID'
requestBody:
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file: { type: string, format: binary }
responses:
'200':
description: 取件码与文件名
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ShareFileData' }
'400': { $ref: '#/components/responses/BadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'423': { $ref: '#/components/responses/RateLimited' }
'428': { $ref: '#/components/responses/NotInitialized' }
/presign/upload/confirm/{uploadID}:
post:
tags: [预签名直传]
summary: 直传确认(direct 模式)
operationId: presignConfirm
description: |
客户端完成 S3 PUT 后调用。服务端 HeadObject 核对实际对象:
超过策略上限 → 删除对象、释放容量预留并 403;实际大小与声明差 >1KB → 400
前 64 字节做 magic bytes 校验(失败删除对象)。全部通过后创建分享记录。
parameters:
- $ref: '#/components/parameters/UploadID'
responses:
'200':
description: 取件码与文件名
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/ShareFileData' }
'400': { $ref: '#/components/responses/BadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'423': { $ref: '#/components/responses/RateLimited' }
'428': { $ref: '#/components/responses/NotInitialized' }
/presign/upload/status/{uploadID}:
get:
tags: [预签名直传]
summary: 查询预签名会话
operationId: presignStatus
parameters:
- $ref: '#/components/parameters/UploadID'
responses:
'200':
description: 会话状态
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
properties:
upload_id: { type: string }
file_name: { type: string }
file_size: { type: integer, format: int64 }
mode: { type: string, enum: [direct, proxy] }
created_at: { type: string }
expires_at: { type: string }
is_expired: { type: boolean }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
/presign/upload/{uploadID}:
delete:
tags: [预签名直传]
summary: 取消预签名会话
operationId: presignCancel
description: direct 模式会尽力清理已直传的对象。
parameters:
- $ref: '#/components/parameters/UploadID'
responses:
'200':
description: 已取消
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
properties:
message: { type: string, example: 上传会话已取消 }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/login:
post:
tags: [管理后台]
summary: 管理员登录
operationId: adminLogin
description: 失败 401 并计入 login 限流(423 超限)。
requestBody:
content:
application/json:
schema:
type: object
required: [password]
properties:
password: { type: string }
responses:
'200':
description: JWT 会话
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/LoginData' }
'401': { $ref: '#/components/responses/Unauthorized' }
'423': { $ref: '#/components/responses/RateLimited' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/verify:
get:
tags: [管理后台]
summary: 校验管理员会话
operationId: adminVerify
security: [{ bearerAuth: [] }]
responses:
'200':
description: 会话信息
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/LoginData' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/logout:
post:
tags: [管理后台]
summary: 管理员登出
operationId: adminLogout
description: 无状态 JWT,客户端丢弃令牌即可。
security: [{ bearerAuth: [] }]
responses:
'200':
description: 确认
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/OkData' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/dashboard:
get:
tags: [管理后台]
summary: 管理端仪表盘统计
operationId: adminDashboard
security: [{ bearerAuth: [] }]
responses:
'200':
description: 计数/存储用量/最近文件等
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
properties:
totalFiles: { type: integer }
storageUsed: { type: string, example: '123456789' }
sysUptime: { type: integer, nullable: true, description: 启动时刻 Unix 毫秒 }
yesterdayCount: { type: integer }
yesterdaySize: { type: string }
todayCount: { type: integer }
todaySize: { type: string }
activeCount: { type: integer }
expiredCount: { type: integer }
textCount: { type: integer }
fileCount: { type: integer }
chunkedCount: { type: integer }
usedCount: { type: integer }
storageBackend: { type: string, enum: [local, s3, webdav] }
uploadSizeLimit: { type: integer, format: int64 }
openUpload: { type: integer }
enableChunk: { type: integer }
maxSaveSeconds: { type: integer, format: int64 }
topSuffixes:
type: array
items:
type: object
properties:
suffix: { type: string }
count: { type: integer }
recentFiles:
type: array
items: { $ref: '#/components/schemas/AdminFileItem' }
recentActivities: { type: array, items: { type: object } }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/file/list:
get:
tags: [管理后台]
summary: 分页查询分享列表
operationId: adminFileList
security: [{ bearerAuth: [] }]
parameters:
- { name: page, in: query, schema: { type: integer, default: 1, minimum: 1 } }
- { name: size, in: query, schema: { type: integer, default: 10, minimum: 1, maximum: 100 } }
- { name: keyword, in: query, schema: { type: string } }
- { name: status, in: query, schema: { type: string, enum: [active, expired] } }
- { name: type, in: query, schema: { type: string, enum: [text, file, chunked] } }
- { name: sortBy, in: query, schema: { type: string, enum: [created_at, expired_at, name, size, used_count, code] } }
- { name: sortOrder, in: query, schema: { type: string, enum: [asc, desc], default: desc } }
responses:
'200':
description: 列表与统计
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
properties:
page: { type: integer }
size: { type: integer }
total: { type: integer }
summary:
type: object
properties:
totalFiles: { type: integer }
activeCount: { type: integer }
expiredCount: { type: integer }
textCount: { type: integer }
fileCount: { type: integer }
chunkedCount: { type: integer }
storageUsed: { type: integer, format: int64 }
usedCount: { type: integer, format: int64 }
data:
type: array
items: { $ref: '#/components/schemas/AdminFileItem' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/file/detail:
get:
tags: [管理后台]
summary: 文件详情(GET
operationId: adminFileDetailGet
security: [{ bearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/FileIDQuery'
responses:
'200':
description: 文件条目(文本分享额外含 content 全文)
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/AdminFileItem' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
post:
tags: [管理后台]
summary: 文件详情(POST
operationId: adminFileDetailPost
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema:
type: object
required: [id]
properties:
id: { type: integer, format: int64 }
responses:
'200':
description: 同 GET 版本
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/AdminFileItem' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/file/update:
patch:
tags: [管理后台]
summary: 更新分享字段(主名)
operationId: adminFileUpdate
description: code 冲突 400POST /admin/file/update 为兼容别名。
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema: { $ref: '#/components/schemas/FileUpdateInput' }
responses:
'200':
description: 更新成功(data 为字符串“更新成功”)
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
post:
tags: [管理后台]
summary: 更新分享字段(POST 别名)
operationId: adminFileUpdateAlias
deprecated: true
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema: { $ref: '#/components/schemas/FileUpdateInput' }
responses:
'200':
description: 更新成功
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/file/delete:
delete:
tags: [管理后台]
summary: 删除单个分享(主名)
operationId: adminFileDelete
description: 连带删除存储文件;POST /admin/file/delete 为兼容别名。
security: [{ bearerAuth: [] }]
parameters:
- { name: id, in: query, schema: { type: integer, format: int64 }, description: 亦可放请求体 }
requestBody:
content:
application/json:
schema:
type: object
properties:
id: { type: integer, format: int64 }
responses:
'200':
description: 删除成功(data 为 null
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
post:
tags: [管理后台]
summary: 删除单个分享(POST 别名)
operationId: adminFileDeleteAlias
deprecated: true
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema:
type: object
properties:
id: { type: integer, format: int64 }
responses:
'200':
description: 删除成功
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/file/batch-delete:
post:
tags: [管理后台]
summary: 批量删除
operationId: adminFileBatchDelete
description: DELETE 方法亦可。
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema:
type: object
required: [ids]
properties:
ids: { type: array, items: { type: integer, format: int64 } }
responses:
'200':
description: 批量统计
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/BatchStats' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
delete:
tags: [管理后台]
summary: 批量删除(DELETE 别名)
operationId: adminFileBatchDeleteAlias
deprecated: true
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema:
type: object
required: [ids]
properties:
ids: { type: array, items: { type: integer, format: int64 } }
responses:
'200':
description: 批量统计
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/BatchStats' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/file/batch-update:
patch:
tags: [管理后台]
summary: 批量更新过期策略
operationId: adminFileBatchUpdate
description: expired_at / expired_count / clearExpiredAt 三选一;POST 亦可。
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema:
type: object
required: [ids]
properties:
ids: { type: array, items: { type: integer, format: int64 } }
expired_at: { type: string, description: ISO 8601 }
expired_count: { type: integer }
clearExpiredAt: { type: boolean, description: 清空过期时间并置 expired_count=-1(永久) }
clear_expired_at: { type: boolean, description: 同 clearExpiredAt }
responses:
'200':
description: 批量统计
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/BatchStats' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
post:
tags: [管理后台]
summary: 批量更新(POST 别名)
operationId: adminFileBatchUpdateAlias
deprecated: true
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema:
type: object
required: [ids]
properties:
ids: { type: array, items: { type: integer, format: int64 } }
expired_at: { type: string }
expired_count: { type: integer }
clearExpiredAt: { type: boolean }
responses:
'200':
description: 批量统计
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/BatchStats' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/file/policy-action:
patch:
tags: [管理后台]
summary: 单文件过期策略动作
operationId: adminFilePolicyAction
description: 动作:extend_24h / extend_7d / make_permanent / reset_download_limitPOST 亦可。
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema: { $ref: '#/components/schemas/PolicyInput' }
responses:
'200':
description: 执行结果
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/PolicyResult' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
post:
tags: [管理后台]
summary: 单文件策略动作(POST 别名)
operationId: adminFilePolicyActionAlias
deprecated: true
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema: { $ref: '#/components/schemas/PolicyInput' }
responses:
'200':
description: 执行结果
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/PolicyResult' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/file/batch-policy-action:
patch:
tags: [管理后台]
summary: 批量过期策略动作
operationId: adminFileBatchPolicyAction
description: 响应结构同批量更新;POST 亦可。
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema:
allOf:
- type: object
required: [ids]
properties:
ids: { type: array, items: { type: integer, format: int64 } }
- $ref: '#/components/schemas/PolicyInput'
responses:
'200':
description: 批量统计
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/BatchStats' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
post:
tags: [管理后台]
summary: 批量策略动作(POST 别名)
operationId: adminFileBatchPolicyActionAlias
deprecated: true
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema:
allOf:
- type: object
required: [ids]
properties:
ids: { type: array, items: { type: integer, format: int64 } }
- $ref: '#/components/schemas/PolicyInput'
responses:
'200':
description: 批量统计
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/BatchStats' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/file/download:
get:
tags: [管理后台]
summary: 管理员下载原文件
operationId: adminFileDownload
description: 不消耗取件次数;文件返回二进制流(支持 Range),文本分享返回 JSON。
security: [{ bearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/FileIDQuery'
responses:
'200':
description: 文件流或文本 JSON
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
application/octet-stream:
schema: { type: string, format: binary }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/file/preview:
get:
tags: [管理后台]
summary: 文本预览
operationId: adminFilePreview
description: 仅文本分享可用(文件分享 400);maxChars 默认 40001~20000)。
security: [{ bearerAuth: [] }]
parameters:
- $ref: '#/components/parameters/FileIDQuery'
- { name: maxChars, in: query, schema: { type: integer, default: 4000, minimum: 1, maximum: 20000 } }
responses:
'200':
description: 预览内容
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
properties:
id: { type: integer, format: int64 }
code: { type: string }
name: { type: string }
type: { type: string, example: text }
content: { type: string }
length: { type: integer }
previewLength: { type: integer }
truncated: { type: boolean }
maxChars: { type: integer }
createdAt: { type: string }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/config/get:
get:
tags: [管理后台]
summary: 读取运行时配置
operationId: adminConfigGet
description: admin_token 屏蔽(恒空串)、jwt_secret 不下发;_engine_hint 提示引擎配置需重启。
security: [{ bearerAuth: [] }]
responses:
'200':
description: 配置 KV
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
additionalProperties: true
properties:
site_name: { type: string }
logo_url: { type: string }
favicon_url: { type: string }
background_url: { type: string }
footer_text: { type: string }
footer_beian: { type: string }
notify_enabled: { type: integer, enum: [0, 1] }
openUpload: { type: integer }
uploadSize: { type: integer, format: int64 }
max_file_size: { type: integer, format: int64, description: '0=回落 uploadSize' }
max_save_count: { type: integer, description: '0=不限制' }
max_save_seconds: { type: integer, format: int64 }
allowed_file_types: { type: array, items: { type: string } }
expireStyle: { type: array, items: { type: string } }
storageLimit: { type: integer, format: int64 }
uploadCount: { type: integer }
uploadMinute: { type: integer }
admin_token: { type: string, example: '' }
_engine_hint:
type: object
properties:
storage_backend: { type: string }
note: { type: string }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/config/update:
patch:
tags: [管理后台]
summary: 部分更新配置(主名)
operationId: adminConfigUpdate
description: |
JSON 对象部分更新,未知键忽略;数值键自动转型;v2 起新键按 settings.KVSchema 校验
(整型边界/字符串长度/列表非空,越界 400 中文错误);background_url 协议白名单
http(s)/data:image/站内相对路径);admin_token 明文自动哈希并轮换 jwt_secret
(全部令牌立即失效);adminSessionExpire 须 1~365 整天(默认 604800 即 7 天);notify_content 服务端白名单净化(仅保留文本与 <a href=http(s)|/|#>);策略与限流修改立即生效,引擎配置需重启。
POST /admin/config/update 为兼容别名。
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema:
type: object
additionalProperties: true
properties:
site_name: { type: string }
logo_url: { type: string }
favicon_url: { type: string }
background_url:
type: string
maxLength: 2048
description: '需求 ① 背景图;仅 http(s)、data:image/* 与站内相对路径(/开头),空串清除;非法协议 400'
footer_text: { type: string, maxLength: 2000, description: '需求 ② 页脚自定义内容' }
footer_beian: { type: string, maxLength: 128, description: '需求 ② 备案号' }
notify_enabled: { type: integer, enum: [0, 1], description: '需求 ③ 通知开关' }
notify_title: { type: string, maxLength: 128 }
notify_content: { type: string, maxLength: 2000 }
openUpload: { type: integer, enum: [0, 1] }
uploadSize: { type: integer, format: int64, minimum: 1024, maximum: 10737418240 }
max_file_size: { type: integer, format: int64, minimum: 0, maximum: 10737418240, description: '需求 ⑩ 存储策略-单文件上限字节;0=回落 uploadSize' }
max_save_count: { type: integer, minimum: 0, maximum: 100000, description: '需求 ④ 单次分享最大可取次数上限;0=不限制' }
max_save_seconds: { type: integer, format: int64, minimum: 0, maximum: 31536000, description: '需求 ④ 最长保存秒数上限;0=默认 7 天兜底' }
allowed_file_types:
oneOf:
- type: array
items: { type: string }
minItems: 1
- type: string
description: 逗号分隔亦可;"*" 不限制
expireStyle:
oneOf:
- type: array
items: { type: string }
minItems: 1
- type: string
description: 允许的过期方式白名单(day/hour/minute/forever/count 子集)
storageLimit: { type: integer, format: int64, minimum: 0 }
uploadCount: { type: integer, minimum: 1, maximum: 10000 }
uploadMinute: { type: integer, minimum: 1, maximum: 1440 }
errorCount: { type: integer }
errorMinute: { type: integer }
loginCount: { type: integer }
loginMinute: { type: integer }
admin_token: { type: string, description: 明文密码自动哈希并轮换 jwt_secret;空串忽略 }
adminSessionExpire: { type: integer, default: 604800, description: 秒,须 86400 的 1~365 整数倍(默认 7 天) }
responses:
'200':
description: 保存成功
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/OkData' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
post:
tags: [管理后台]
summary: 部分更新配置(POST 别名)
operationId: adminConfigUpdateAlias
deprecated: true
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
'200':
description: 保存成功
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/OkData' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/storage/switch:
post:
tags: [admin]
summary: 存储引擎热切换(v3
description: |
运行时切换存储引擎(local|s3|webdav)。构建新引擎并做健康检查,
通过才生效;失败返回 503 且保持原引擎。成功后持久化 storage_engine KV
重启后沿用。
operationId: adminStorageSwitch
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [engine]
properties:
engine:
type: string
enum: [local, s3, webdav]
responses:
"200":
description: 切换成功
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Envelope"
- type: object
properties:
data:
type: object
properties:
ok: { type: boolean }
engine: { type: string }
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthorized" }
"503": { $ref: "#/components/responses/ServiceUnavailable" }
/admin/settings/password:
patch:
tags: [管理后台]
summary: 修改管理员密码(主名)
operationId: adminChangePassword
description: 新密码 ≥8 位;成功后轮换 jwt_secret,全部旧令牌失效(401)。POST 亦可。
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema:
type: object
required: [old_password, new_password]
properties:
old_password: { type: string }
new_password: { type: string, minLength: 8 }
responses:
'200':
description: 修改成功
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/OkData' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
post:
tags: [管理后台]
summary: 修改管理员密码(POST 别名)
operationId: adminChangePasswordAlias
deprecated: true
security: [{ bearerAuth: [] }]
requestBody:
content:
application/json:
schema:
type: object
required: [old_password, new_password]
properties:
old_password: { type: string }
new_password: { type: string, minLength: 8 }
responses:
'200':
description: 修改成功
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data: { $ref: '#/components/schemas/OkData' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/audit/list:
get:
tags: [审计日志]
summary: 分页查询审计日志(主名)
operationId: adminAuditList
description: |
需求 ③ 审计查询。行字段 snake_case 为主(camelCase 兼容双份)。
action: upload|downloadresult: success|denied|failed;时间参数 ISO 8601。
security: [{ bearerAuth: [] }]
parameters:
- { name: page, in: query, schema: { type: integer, default: 1, minimum: 1 } }
- { name: size, in: query, schema: { type: integer, default: 20, minimum: 1, maximum: 200 }, description: 兼容 pageSize }
- { name: action, in: query, schema: { type: string, enum: [upload, download] } }
- { name: result, in: query, schema: { type: string, enum: [success, denied, failed] } }
- { name: ip, in: query, schema: { type: string } }
- { name: start_time, in: query, schema: { type: string, example: '2025-06-01T00:00:00+08:00' } }
- { name: end_time, in: query, schema: { type: string, example: '2025-06-02T00:00:00+08:00' } }
responses:
'200':
description: 审计日志分页
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
properties:
data:
type: array
items: { $ref: '#/components/schemas/AuditItem' }
total: { type: integer }
page: { type: integer }
size: { type: integer }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
/admin/audit/logs:
get:
tags: [审计日志]
summary: 分页查询审计日志(别名)
operationId: adminAuditLogs
deprecated: true
description: 与 /admin/audit/list 完全相同(同一 handler 的兼容别名)。
security: [{ bearerAuth: [] }]
parameters:
- { name: page, in: query, schema: { type: integer, default: 1 } }
- { name: size, in: query, schema: { type: integer, default: 20 } }
- { name: action, in: query, schema: { type: string, enum: [upload, download] } }
- { name: result, in: query, schema: { type: string, enum: [success, denied, failed] } }
- { name: ip, in: query, schema: { type: string } }
- { name: start_time, in: query, schema: { type: string } }
- { name: end_time, in: query, schema: { type: string } }
responses:
'200':
description: 审计日志分页
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: object
properties:
data:
type: array
items: { $ref: '#/components/schemas/AuditItem' }
total: { type: integer }
page: { type: integer }
size: { type: integer }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'428': { $ref: '#/components/responses/NotInitialized' }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: POST /admin/login 获取
parameters:
CodeQuery:
name: code
in: query
required: true
schema: { type: string }
description: 取件码
UploadID:
name: uploadID
in: path
required: true
schema: { type: string }
description: 上传会话 ID
FileIDQuery:
name: id
in: query
required: true
schema: { type: integer, format: int64 }
description: 文件记录 ID
responses:
BadRequest:
description: 参数/格式错误
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
example: { code: 400, msg: '过期时间类型错误' }
Unauthorized:
description: 未认证
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
example: { code: 401, msg: '密码错误' }
Forbidden:
description: 拒绝(类型/鉴权/上限)
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
example: { code: 403, msg: '不允许上传该类型文件' }
NotFound:
description: 不存在/已过期
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
example: { code: 404, msg: '文件不存在' }
RangeNotSatisfiable:
description: Range 越界
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
example: { code: 416, msg: '请求范围超出文件大小' }
RateLimited:
description: 限流超限
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
example: { code: 423, msg: '请求次数过多,请稍后再试' }
NotInitialized:
description: 系统未初始化(除 /setup 与 /api/v1/health 外)
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
example: { code: 428, msg: '系统未初始化,请先完成初始化' }
NotImplemented:
description: 引擎不支持
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
example: { code: 501, msg: '当前存储引擎不支持该操作' }
ServiceUnavailable:
description: 存储不可用
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
example: { code: 503, msg: '存储服务不可用,请稍后再试' }
InsufficientStorage:
description: 容量超限
content:
application/json:
schema: { $ref: '#/components/schemas/Envelope' }
example: { code: 507, msg: '存储空间已达到管理员设置的容量上限' }
schemas:
Envelope:
type: object
description: 统一响应封装(HTTP 状态码 = code;失败时 data 缺省)
required: [code, msg]
properties:
code: { type: integer, example: 200 }
msg: { type: string, example: ok }
data: { description: 业务数据,失败时缺省 }
OkData:
type: object
properties:
ok: { type: boolean, example: true }
ShareMetadata:
type: object
properties:
code: { type: string }
name: { type: string }
size: { type: integer, format: int64 }
type: { type: string, enum: [text, file] }
is_text: { type: boolean }
created_at: { type: string }
expired_at: { type: string, nullable: true }
expires_at: { type: string, nullable: true }
expired_count: { type: integer, description: '-1 按时间/永久;>0 次数型限额' }
used_count: { type: integer }
remaining_downloads: { type: integer, nullable: true }
ShareDetail:
allOf:
- $ref: '#/components/schemas/ShareMetadata'
- type: object
properties:
text: { type: string, description: 文本内容或下载地址 }
content: { type: string, description: 文本内容(仅文本分享) }
download_url: { type: string, nullable: true, description: 代理地址或 S3 预签名直链 }
ShareFileData:
type: object
properties:
code: { type: string }
name: { type: string }
ChunkInitData:
type: object
properties:
existed: { type: boolean, example: false }
upload_id: { type: string }
chunk_size: { type: integer, format: int64 }
total_chunks: { type: integer }
uploaded_chunks: { type: array, items: { type: integer } }
ChunkUploadData:
type: object
properties:
chunk_hash: { type: string }
chunk_index: { type: integer }
skipped: { type: boolean, description: 幂等命中已传分片时为 true }
ChunkStatusData:
type: object
properties:
upload_id: { type: string }
file_name: { type: string }
file_size: { type: integer, format: int64 }
chunk_size: { type: integer, format: int64 }
total_chunks: { type: integer }
uploaded_chunks: { type: array, items: { type: integer } }
progress: { type: number }
PresignInitData:
type: object
properties:
upload_id: { type: string }
upload_url: { type: string, description: 'direct=S3 预签名 PUT URLproxy=/presign/upload/proxy/{id}' }
mode: { type: string, enum: [direct, proxy] }
expires_in: { type: integer, example: 900 }
file_path: { type: string }
proxy_upload_url: { type: string, description: '仅 proxy 模式,与 upload_url 等价' }
legacy_proxy_upload_url: { type: string, description: '仅 proxy 模式;/api 前缀兼容别名,已废弃' }
LoginData:
type: object
properties:
id: { type: string, example: admin }
username: { type: string, example: admin }
token: { type: string }
token_type: { type: string, example: Bearer }
expires_at: { type: integer, format: int64, description: Unix 秒 }
expires_in: { type: integer, description: 秒 }
AdminFileItem:
type: object
description: snake_case 与 camelCase 双份输出(文档以 snake_case 为准)
properties:
id: { type: integer, format: int64 }
code: { type: string }
name: { type: string }
prefix: { type: string }
suffix: { type: string }
size: { type: integer, format: int64 }
is_text: { type: boolean }
is_chunked: { type: boolean }
is_expired: { type: boolean }
expired_at: { type: string, nullable: true }
expired_count: { type: integer }
used_count: { type: integer }
created_at: { type: string }
has_download_limit: { type: boolean }
is_permanent: { type: boolean }
remaining_downloads: { type: integer, nullable: true }
file_hash: { type: string, nullable: true }
text: { type: boolean, description: true 表示为文本分享 }
FileUpdateInput:
type: object
required: [id]
properties:
id: { type: integer, format: int64 }
code: { type: string }
prefix: { type: string }
suffix: { type: string }
expired_at: { type: string, description: ISO 8601 }
expired_count: { type: integer }
PolicyInput:
type: object
required: [action]
properties:
id: { type: integer, format: int64 }
action: { type: string, enum: [extend_24h, extend_7d, make_permanent, reset_download_limit] }
downloadLimit: { type: integer, minimum: 1, description: 仅 reset_download_limit;默认 5 }
PolicyResult:
type: object
properties:
id: { type: integer, format: int64 }
action: { type: string }
BatchStats:
type: object
description: 请求/成功/缺失/失败统计(snake_case 与 camelCase 双份)
properties:
requestedCount: { type: integer }
deletedCount: { type: integer, description: 仅批量删除 }
updatedCount: { type: integer, description: 仅批量更新 }
missingCount: { type: integer }
failedCount: { type: integer }
deleted: { type: array, items: { type: integer, format: int64 } }
missing: { type: array, items: { type: integer, format: int64 } }
failed:
type: array
items:
type: object
properties:
id: { type: integer, format: int64 }
reason: { type: string }
AuditItem:
type: object
description: 需求 ③ 审计行(snake_case 与 camelCase 双份)
properties:
id: { type: integer, format: int64 }
action: { type: string, enum: [upload, download] }
file_code: { type: string }
file_name: { type: string }
size_bytes: { type: integer, format: int64 }
transferred_bytes: { type: integer, format: int64 }
ip: { type: string }
user_agent: { type: string }
device_os: { type: string }
device_browser: { type: string }
device_type: { type: string }
actor: { type: string, enum: [admin, guest] }
result: { type: string, enum: [success, denied, failed] }
error_msg: { type: string }
duration_ms: { type: integer, format: int64 }
created_at: { type: string }
security: []
`,S=Object.assign({"../../../docs/api/00-overview.md":p,"../../../docs/api/01-auth.md":d,"../../../docs/api/02-text-share.md":c,"../../../docs/api/03-file-share.md":l,"../../../docs/api/04-pickup.md":m,"../../../docs/api/05-chunk-upload.md":u,"../../../docs/api/06-presign.md":g,"../../../docs/api/07-admin.md":_,"../../../docs/api/08-audit.md":y,"../../../docs/api/09-storage.md":h,"../../../docs/api/10-config.md":f,"../../../docs/api/11-errors.md":x,"../../../docs/api/12-logo.md":b}),v=["README","share","chunk","presign","admin","audit","errors","config","logo","webdav","env"];function j(n){return(n.split("/").pop()??n).replace(/\.md$/,"")}function E(n,e){const o=/^#\s+(.+)$/m.exec(n);return o?o[1].trim():e}const r=Object.entries(S).map(([n,e])=>{const o=j(n),s=v.indexOf(o);return{slug:o,title:E(e,o),embedded:e,order:s>=0?s:100+o.localeCompare("")}}).sort((n,e)=>n.order-e.order||n.slug.localeCompare(e.slug));r.length>0;const k=Object.assign({"../../../docs/openapi.yaml":T}),O=Object.values(k)[0]??null;async function C(n){if(n.embedded!==null)return n.embedded;const{API_BASE:e}=await i(async()=>{const{API_BASE:t}=await import("./index-BKnWAKao.js").then(a=>a.aD);return{API_BASE:t}},__vite__mapDeps([0,1])),o=`${e}/docs/api/${encodeURIComponent(n.slug)}.md`,s=await fetch(o);if(!s.ok)throw new Error(`文档加载失败(HTTP ${s.status}`);return s.text()}async function I(){try{const{API_BASE:n}=await i(async()=>{const{API_BASE:t}=await import("./index-BKnWAKao.js").then(a=>a.aD);return{API_BASE:t}},__vite__mapDeps([0,1])),e=await fetch(`${n}/docs/api/index.json`);if(!e.ok)return[];const o=await e.json(),s=new Set(r.map(t=>t.slug));return o.filter(t=>t.slug&&!s.has(t.slug)).map(t=>({slug:t.slug,title:t.title??t.slug,embedded:null,order:200}))}catch{return[]}}export{I as a,r as d,O as e,C as l};