- 数据库默认文件 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 清理)
2518 lines
158 KiB
JavaScript
2518 lines
158 KiB
JavaScript
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- 密码存储为 bcrypt(cost 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_url(S3 预签名 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"}'
|
||
\`\`\`
|
||
|
||
**S3(direct)成功响应**(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/WebDAV(proxy)成功响应**(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**错误响应**(400,HTML 表单时内嵌错误提示):\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` 截断长度(默认 4000,1~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/switch(v3)\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` ≤ 10GiB(10737418240)、`max_save_count` ≤ 100000、`max_save_seconds` ≤ 31536000(365 天)、`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 + Digest(RFC 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://`(TLS)URL 形式;**空则缓存降级为进程内存实现**(缓存故障时自动降级为进程内限流计数) |\n| `FCB_REDIS_DB` | ❌ | `0` | Redis 逻辑库号 0-15;URL 形式地址显式携带 `/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/8,1h 轮换);`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` | int(0/1) | 1 | **v2 需求 ③**:通知开关(1=前台右上角悬浮窗展示 / 0=关闭) |\n| `notify_title` | string,≤128 字符 | 系统通知 | 通知标题 |\n| `notify_content` | string,≤2000 字符 | 欢迎使用… | 通知正文(**服务端白名单净化**:仅保留纯文本与 `<a href>` 为 http(s)/站内相对/`#` 锚点的链接,其余标签与事件属性剥离,保存与读取双侧生效) |\n| `showAdminAddr` | int(0/1) | 0 | 是否展示后台入口 |\n| `robotsText` | string | `User-agent: *\\nDisallow: /` | robots.txt 内容(由公开端点 `GET /robots.txt` 输出) |\n\n### 保存策略(需求 ④,上传页动态读取并在范围内选择)\n\n| 键 | 类型/边界 | 默认 | 说明 |\n|---|---|---|---|\n| `max_save_seconds` | int64,0~31536000 | 0 | 最长保存秒数上限(0=仅默认 7 天兜底;>0 时按时间过期超限 403「限制最长时间为 X,可换用其他方式」)。**v3**:管理界面以「小时/天」下拉单位编辑(≥1 天自动显示天),提交时前端换算为秒——canonical 单位保持秒,接口语义不变 |\n| `max_save_count` | int,0~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` | int64,1024~10GiB | 10485760(10MB) | 单文件大小上限(字节),参考实现语义;`max_file_size=0` 时作为生效上限 |\n| `max_file_size` | int64,0~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` | int(0/1) | 1 | 游客上传开关(0 时上传接口要求管理员令牌 403) |\n| `enableChunk` | int(0/1) | 0 | 启用分片上传 |\n\n### 上传频率限制(需求 ④,既有键对齐参考 ip_limit["upload"])\n\n| 键 | 类型/边界 | 默认 | 说明 |\n|---|---|---|---|\n| `uploadCount` / `uploadMinute` | int(1~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` | 604800(7 天) | 管理员会话秒数(须 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。表单提交返回成功 HTML,JSON 提交返回 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(请求体全局上限 1MiB,Content-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/text(403/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 区间,支持 Range,Accept-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 时为其,否则 uploadSize;403);类型白名单 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/text(403/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=direct(upload_url 为预签名 PUT URL);local/webdav 返回 mode=proxy
|
||
(upload_url=/presign/upload/proxy/{uploadID})。会话 900 秒有效。
|
||
file_size 受动态策略上限(max_file_size>0 时为其,否则 uploadSize;403);保存策略同 /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 冲突 400;POST /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_limit;POST 亦可。
|
||
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 默认 4000(1~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|download;result: 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 URL;proxy=/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};
|