FileCodeBox Go 重写版 v2.5.6(安全审计修复版)
Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务: - 安全审计全部修复(docs/security-audit-2026-09-05.md): bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、 全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、 管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、 会话默认 7 天、限流缓存故障降级、robots.txt 端点等 - 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固 - Redis 支持库号(FCB_REDIS_DB / redis://…/db URL) - 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、 提码 5 位起、chunk 32MiB 上限、admin 审计动作等) 验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# API 概述
|
||||
|
||||
文件快传 Go 版(v2.5.6)对外提供一套 REST API,覆盖文本/文件分享、分片上传、
|
||||
预签名直传、管理后台与审计日志查询。本文档与 `server/internal/api/` 实际实现逐一对齐,
|
||||
交互式规范见站内 `/openapi`(源文件 `docs/openapi.yaml`)。
|
||||
|
||||
## Base URL
|
||||
|
||||
- 服务默认监听 `:8466`,Base URL 为 `http://<host>:8466`(下文示例统一用 `http://localhost:8466`)。
|
||||
- **业务路由挂根路径**(与参考实现一致):`/share/*`、`/chunk/*`、`/presign/*`、`/admin/*`、`/setup`。
|
||||
- 仅两个公共接口带 `/api/v1` 前缀:`/api/v1/health`、`/api/v1/config`。
|
||||
|
||||
## 统一响应封装
|
||||
|
||||
所有 JSON 接口返回统一结构,HTTP 状态码与 `code` 一致;失败时 `data` 缺省:
|
||||
|
||||
```json
|
||||
{ "code": 200, "msg": "ok", "data": { } }
|
||||
```
|
||||
|
||||
失败示例(404):
|
||||
|
||||
```json
|
||||
{ "code": 404, "msg": "文件不存在" }
|
||||
```
|
||||
|
||||
个别端点直接返回原始内容而非 JSON 封装(文档中已单独标注):
|
||||
|
||||
| 端点 | 响应形式 |
|
||||
|---|---|
|
||||
| `GET /share/select?code=`(文本分享) | `text/plain; charset=utf-8` 正文 |
|
||||
| `GET /share/select?code=`(文件分享) | 文件二进制流(200/206,支持 Range) |
|
||||
| `GET /share/download?key=&code=`(文件分享) | 文件二进制流(200/206,支持 Range) |
|
||||
| `GET /admin/file/download?id=`(文件分享) | 文件二进制流 |
|
||||
| `GET /setup` / `POST /setup`(表单) | HTML 向导/成功页 |
|
||||
|
||||
## 字段命名约定
|
||||
|
||||
- 接口字段以 **snake_case** 为主(`file_code`、`size_bytes`)。
|
||||
- 文件列表与审计日志的行字段同时输出 **snake_case 与 camelCase 双份**(如 `expired_at` 与 `expiredAt`),文档以 snake_case 为准,camelCase 仅为前端兼容保留。
|
||||
|
||||
## 认证
|
||||
|
||||
- 游客接口无需认证;是否允许游客上传由配置 `openUpload` 控制(关闭时上传类接口要求管理员 `Authorization: Bearer <token>`,否则 403)。
|
||||
- 管理接口(`/admin/login` 除外)一律要求 `Authorization: Bearer <JWT>`,无效/缺失返回 401。
|
||||
- 详情见《认证与限流》。
|
||||
|
||||
## 限流
|
||||
|
||||
按 IP(可信代理场景解析 `X-Forwarded-For`)维度限流,超限返回 **423**:
|
||||
|
||||
| 规则 | 计数时机 | 默认(次/窗口) | 相关配置 |
|
||||
|---|---|---|---|
|
||||
| `upload` | **上传成功**后计数 | 10 次 / 1 分钟 | `uploadCount` / `uploadMinute` |
|
||||
| `error` | 取件失败(404/过期)时计数 | 10 次 / 1 分钟 | `errorCount` / `errorMinute` |
|
||||
| `login` | 登录失败时计数 | 5 次 / 15 分钟 | `loginCount` / `loginMinute` |
|
||||
| `metadata` | 每次访问即计数 | 同 `error` | `errorCount` / `errorMinute` |
|
||||
|
||||
## 初始化守卫
|
||||
|
||||
系统未初始化(未设置管理员密码)时,除 `GET|POST /setup` 与 `GET /api/v1/health` 外,
|
||||
**所有接口一律返回 428**:
|
||||
|
||||
```json
|
||||
{ "code": 428, "msg": "系统未初始化,请先完成初始化" }
|
||||
```
|
||||
|
||||
首次部署请先访问 `GET /setup` 获取 HTML 向导,或直接 `POST /setup` 完成初始化(见《管理后台 API》初始化章节)。
|
||||
|
||||
## 审计
|
||||
|
||||
所有上传/下载端点经审计中间件自动落库(操作时间/IP/UA/设备解析/动作/结果/字节数/耗时/角色),
|
||||
失败与被拒绝的请求同样记录;管理端经 `GET /admin/audit/list` 查询,详见《审计日志》。
|
||||
|
||||
## 端点总览
|
||||
|
||||
| 模块 | 端点 |
|
||||
|---|---|
|
||||
| 公共 | `GET /api/v1/health` · `GET /api/v1/config` · `GET /robots.txt`(输出 `robotsText` 配置) |
|
||||
| 初始化 | `GET /setup` · `POST /setup` |
|
||||
| 文本分享 | `POST /share/text` |
|
||||
| 文件分享 | `POST /share/file` |
|
||||
| 查询与取件 | `GET/POST /share/metadata` · `GET /share/select` · `POST /share/select` · `GET /share/download` |
|
||||
| 分片上传 | `POST /chunk/upload/init` · `POST /chunk/upload/{uploadID}/{chunkIndex}` · `GET /chunk/upload/status/{uploadID}` · `POST /chunk/upload/complete/{uploadID}` · `DELETE /chunk/upload/{uploadID}` |
|
||||
| 预签名直传 | `POST /presign/upload/init` · `PUT /presign/upload/proxy/{uploadID}` · `POST /presign/upload/confirm/{uploadID}` · `GET /presign/upload/status/{uploadID}` · `DELETE /presign/upload/{uploadID}` |
|
||||
| 管理后台 | `POST /admin/login` · `GET /admin/verify` · `POST /admin/logout` · `GET /admin/dashboard` · 文件管理 `/admin/file/*` · 配置 `/admin/config/*` · 密码 `/admin/settings/password` |
|
||||
| 审计日志 | `GET /admin/audit/list`(别名 `/admin/audit/logs`) |
|
||||
|
||||
## 时间与编码
|
||||
|
||||
- 时间字段一律 RFC 3339(如 `2025-06-01T12:00:00+08:00`);管理员会话过期时间为 Unix 秒。
|
||||
- 请求体支持 `application/json` 与 `application/x-www-form-urlencoded`(上传类为 `multipart/form-data`),文档示例以 JSON/curl 为主。
|
||||
- CORS:公开接口放开(Bearer 认证,无 Cookie CSRF 面);**管理端 `/admin/*` 已收紧**——携带 Origin 且既不同源也不在 `site_domain` 白名单时不下发 CORS 头(浏览器拦截跨域读取)。
|
||||
|
||||
## 交互式文档
|
||||
|
||||
- 站内文档页:`/docs`(渲染本目录 markdown,构建时内嵌)。
|
||||
- Swagger UI:`/openapi`(渲染 `docs/openapi.yaml`,构建时内嵌)。
|
||||
- OpenAPI 规范源文件为仓库内 `docs/openapi.yaml`;如需经后端直接下载,
|
||||
需在部署时把它拷贝进前端静态产物 `web/dist/`(未拷贝时该路径按 SPA 回退返回页面)。
|
||||
@@ -0,0 +1,105 @@
|
||||
# 认证与限流
|
||||
|
||||
## 角色
|
||||
|
||||
| 角色 | 能力 |
|
||||
|---|---|
|
||||
| 游客(无 Authorization 头) | 取件、查询元信息;`openUpload=1` 时可上传 |
|
||||
| 管理员(`Authorization: Bearer <JWT>`) | 全部能力 + `/admin/*` 管理接口 |
|
||||
|
||||
## 管理员令牌
|
||||
|
||||
- 由 `POST /admin/login` 用管理员密码换取,HS256 JWT,默认有效期 **7 天**(`adminSessionExpire`,1~365 整天,v2.5.6 起由 30 天缩短)。
|
||||
- 请求头格式:`Authorization: Bearer <token>`。
|
||||
- **改密/重置管理员密码会轮换 `jwt_secret`,所有已签发令牌立即失效**(401)。
|
||||
- 密码存储为 bcrypt(cost 12);历史 `sha256$`/明文格式在登录成功后自动升级重哈希,无需手动迁移。
|
||||
- 游客上传关闭(`openUpload=0`)时,上传类接口也可用管理员 Bearer 令牌通过鉴权。
|
||||
|
||||
## 认证失败语义
|
||||
|
||||
| 场景 | 状态码 |
|
||||
|---|---|
|
||||
| `/admin/*` 缺失/无效令牌 | 401 |
|
||||
| `POST /admin/login` 密码错误 | 401(并计入 login 限流) |
|
||||
| 游客上传被关闭且未携带有效令牌 | 403 |
|
||||
| 代理下载 `key` 校验失败 | 403 |
|
||||
|
||||
## 未初始化(428)
|
||||
|
||||
管理员密码未设置(`admin_token` 为空)时,除 `GET|POST /setup` 与 `GET /api/v1/health` 外全部接口返回 428。
|
||||
完成 `POST /setup` 初始化后自动解除。
|
||||
|
||||
## 限流规则
|
||||
|
||||
限流按 **客户端 IP** 维度(配置 `FCB_TRUSTED_PROXIES` 声明可信代理 CIDR,命中时解析 `X-Forwarded-For` 取真实 IP),
|
||||
窗口计数原子化存储于缓存(未配置 Redis 时为进程内存)。**超限一律返回 423**:
|
||||
|
||||
```json
|
||||
{ "code": 423, "msg": "请求次数过多,请稍后再试" }
|
||||
```
|
||||
|
||||
| 规则 | 生效端点 | 计数时机 | 默认 | 配置键 |
|
||||
|---|---|---|---|---|
|
||||
| `upload` | `/share/text`、`/share/file`、`/chunk/upload/*`、`/presign/upload/*` | **成功后**计数(进入时仅检查) | 10 次 / 1 分钟 | `uploadCount`、`uploadMinute` |
|
||||
| `error` | `/share/select`、`/share/download` | 取件失败(不存在/过期/鉴权失败)时计数 | 10 次 / 1 分钟 | `errorCount`、`errorMinute` |
|
||||
| `login` | `/admin/login` | 登录失败时计数 | 5 次 / 15 分钟 | `loginCount`、`loginMinute` |
|
||||
| `metadata` | `/share/metadata` | **每次访问即计数**(含失败) | 同 `error` | `errorCount`、`errorMinute` |
|
||||
|
||||
- 规则值可由管理端 `PATCH /admin/config/update` 运行时修改,立即生效(无需重启)。
|
||||
- 取件成功(`/share/select`、`/share/download`)不计入 `error` 限流。
|
||||
|
||||
## 代理下载令牌(key)
|
||||
|
||||
`GET /share/download` 的 `key` 由服务端按窗口生成:
|
||||
`sha256(code + timeFactor + "000" + jwt_secret)`,`timeFactor = unix秒 / 1000`(约 16.7 分钟一个窗口)。
|
||||
服务端**同时接受当前与上一窗口**的令牌,避免窗口边界竞态。令牌通过 `POST /share/select` 的响应
|
||||
`download_url` 下发,客户端不应自行构造。
|
||||
|
||||
## 示例
|
||||
|
||||
登录获取令牌:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8466/admin/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"password":"your-admin-password"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": {
|
||||
"id": "admin", "username": "admin",
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"token_type": "Bearer",
|
||||
"expires_at": 1750000000,
|
||||
"expires_in": 604800
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
携带令牌调用管理接口:
|
||||
|
||||
```bash
|
||||
TOKEN="eyJhbGciOiJIUzI1NiIs..."
|
||||
curl -s http://localhost:8466/admin/dashboard -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
校验令牌是否有效:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8466/admin/verify -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": { "id": "admin", "username": "admin", "token": "eyJhbGciOiJIUzI1NiIs...", "token_type": "Bearer", "expires_at": 1750000000 }
|
||||
}
|
||||
```
|
||||
|
||||
令牌失效时:
|
||||
|
||||
```json
|
||||
{ "code": 401, "msg": "令牌无效或已过期" }
|
||||
```
|
||||
@@ -0,0 +1,91 @@
|
||||
# 文本分享
|
||||
|
||||
创建纯文本分享,返回取件码。文本大小上限 **222KB**(超限建议改用文件分享);请求体全局上限 1MiB,`Content-Length` >441KB 时读前直接 403。
|
||||
经审计中间件落库(action=upload)。
|
||||
|
||||
## POST /share/text
|
||||
|
||||
**请求参数**(`application/x-www-form-urlencoded`,亦支持 multipart;`text` 为必需):
|
||||
|
||||
| 参数 | 类型 | 必需 | 默认 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `code` | string,可选;自定义提取码,5-8 位字母或数字(空=随机生成;占用 400「该提取码已被占用」) |
|
||||
| `text` | string | ✅ | - | 文本内容(≤222KB,按 UTF-8 字节数) |
|
||||
| `expire_value` | int | ❌ | `1` | 过期值(配合 `expire_style`) |
|
||||
| `expire_style` | string | ❌ | `day` | `day`/`hour`/`minute`/`count`/`forever`(须在站点允许列表内) |
|
||||
|
||||
过期语义:
|
||||
|
||||
- `day`/`hour`/`minute`:按时间过期,`expired_count = -1`。
|
||||
- `count`:按次数过期,取件 `expire_value` 次后失效(`expired_count = expire_value`);
|
||||
**v2 需求 ④**:`max_save_count>0` 时 `expire_value` 不得超出该上限,超限 403。
|
||||
- `forever`:永久(需站点允许;`max_save_seconds>0` 时其他方式受最长保存上限约束,超限 403)。
|
||||
|
||||
> 可选值与上限来自公开配置 `GET /api/v1/config`(`expireStyle`、`max_save_seconds`、
|
||||
> `max_save_count`),上传页动态读取并在范围内选择;管理端改策略后立即生效。
|
||||
|
||||
**curl 示例**:
|
||||
|
||||
```bash
|
||||
# 自定义提取码(可选):-d 'code=MYCODE1'
|
||||
curl -s -X POST http://localhost:8466/share/text \
|
||||
-d 'text=你好,文件快传' \
|
||||
-d 'expire_value=1' \
|
||||
-d 'expire_style=day'
|
||||
```
|
||||
|
||||
**成功响应**(200):
|
||||
|
||||
```json
|
||||
{ "code": 200, "msg": "ok", "data": { "code": "8XQ2M" } }
|
||||
```
|
||||
|
||||
`data.code` 为 5 位取件码(数字或大写字母+数字,取决于 `code_generate_type`)。
|
||||
|
||||
**错误响应**:
|
||||
|
||||
```json
|
||||
{ "code": 400, "msg": "过期时间类型错误" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 400, "msg": "过期时间值必须大于 0" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 403, "msg": "内容过多,建议采用文件形式" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 403, "msg": "限制最长时间为 7天,可换用其他方式" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 403, "msg": "限制次数最多为 5 次" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 423, "msg": "请求次数过多,请稍后再试" }
|
||||
```
|
||||
|
||||
> 游客上传关闭(`openUpload=0`)时需携带管理员令牌,否则 403:
|
||||
> `{"code":403,"msg":"本站未开启游客上传,如需上传请先登录后台"}`
|
||||
|
||||
## 取回文本
|
||||
|
||||
文本分享的取回走统一的取件接口(消耗次数):
|
||||
|
||||
- `GET /share/select?code=<code>` → `text/plain` 正文即文本内容(响应头 `Content-Disposition` 带文件名,无扩展名时为 `<prefix>.txt`)。
|
||||
- `POST /share/select`(`{"code":"8XQ2M"}`)→ JSON,`data.text` / `data.content` 为文本内容。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
curl -s "http://localhost:8466/share/select?code=8XQ2M"
|
||||
```
|
||||
|
||||
```text
|
||||
你好,文件快传
|
||||
```
|
||||
|
||||
**v3.1 变更**:① 支持 JSON 提交(`Content-Type: application/json`,字段同名);② 空文本 400「分享内容不能为空」;③ 可选 `code` 自定义提取码(5-8 位字母数字,占用 400)。
|
||||
@@ -0,0 +1,103 @@
|
||||
# 文件分享
|
||||
|
||||
上传单个文件并创建分享。支持扩展名/MIME 白名单 + **magic bytes 防伪**(读文件前 64 字节校验,
|
||||
伪造类型返回 403)。经审计中间件落库(action=upload,记录文件总大小与实际传输字节)。
|
||||
|
||||
## POST /share/file
|
||||
|
||||
**请求参数**(`multipart/form-data`):
|
||||
|
||||
| 参数 | 类型 | 必需 | 默认 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `code` | string,可选;自定义提取码,5-8 位字母或数字(空=随机生成;占用 400) |
|
||||
| `file` | file | ✅ | - | 上传的文件(大小 ≤ 生效上限:`max_file_size>0` 时为其,否则 `uploadSize`) |
|
||||
| `expire_value` | int | ❌ | `1` | 过期值(配合 `expire_style`;`count` 型受 `max_save_count` 约束) |
|
||||
| `expire_style` | string | ❌ | `day` | `day`/`hour`/`minute`/`count`/`forever`(须在 `expireStyle` 白名单内) |
|
||||
|
||||
**curl 示例**:
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8466/share/file \
|
||||
-F 'file=@./report.pdf;type=application/pdf' \
|
||||
-F 'expire_value=7' \
|
||||
-F 'expire_style=day'
|
||||
```
|
||||
|
||||
**成功响应**(200):
|
||||
|
||||
```json
|
||||
{ "code": 200, "msg": "ok", "data": { "code": "K3P9W", "name": "report.pdf" } }
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
```json
|
||||
{ "code": 400, "msg": "缺少上传文件 file 字段" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 403, "msg": "大小超过限制,最大为10.00 MB" }
|
||||
```
|
||||
|
||||
> 大小上限为动态策略(v2 需求 ④⑩):管理端改 `max_file_size`(0=回落 `uploadSize`)后
|
||||
> **下一次上传立即按新上限执行**,无需重启;上限值可经 `GET /api/v1/config` 的
|
||||
> `max_file_size`/`maxFileSize` 字段读取。
|
||||
|
||||
```json
|
||||
{ "code": 403, "msg": "不允许上传该类型文件" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 403, "msg": "文件内容与扩展名不匹配,疑似伪造类型" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 403, "msg": "限制最长时间为 7天,可换用其他方式" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 403, "msg": "限制次数最多为 5 次" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 403, "msg": "请求次数过多,请稍后再试" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 507, "msg": "存储空间已达到管理员设置的容量上限" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 503, "msg": "存储服务不可用,请稍后再试" }
|
||||
```
|
||||
|
||||
## 文件类型白名单
|
||||
|
||||
由配置 `allowed_file_types` 控制(管理端可改):
|
||||
|
||||
- `*`:不限制(默认)。
|
||||
- 扩展名规则:`.png`、`pdf`(自动补点)等,按文件名后缀匹配。
|
||||
- MIME 规则:`image/*`、`application/pdf` 等,按请求 `Content-Type` 通配匹配。
|
||||
|
||||
已知类型(png/jpg/gif/webp/bmp/pdf/zip/rar/7z/gz/mp3/mp4/exe/elf)会做 **magic bytes 交叉校验**:
|
||||
扩展名或 Content-Type 声明了已知类型,但文件头不匹配时拒绝(403「疑似伪造类型」)。
|
||||
|
||||
## 下载取件
|
||||
|
||||
- `GET /share/select?code=<code>`:消耗 1 次取件,返回文件流(`200` 全量 / `206` 区间,
|
||||
支持 `Range` 请求头;响应含 `Accept-Ranges: bytes`、`Content-Disposition: attachment; filename*=UTF-8''...`)。
|
||||
- `POST /share/select`:返回详情 JSON,`download_url` 为代理下载地址(见下)。
|
||||
- `GET /share/download?key=<token>&code=<code>`:代理下载,消耗 1 次,同样支持 Range。
|
||||
|
||||
Range 示例(取前 1024 字节):
|
||||
|
||||
```bash
|
||||
curl -s -H 'Range: bytes=0-1023' -o part.bin \
|
||||
"http://localhost:8466/share/select?code=K3P9W"
|
||||
```
|
||||
|
||||
区间越界返回:
|
||||
|
||||
```json
|
||||
{ "code": 416, "msg": "请求范围超出文件大小" }
|
||||
```
|
||||
@@ -0,0 +1,173 @@
|
||||
# 分享查询与取件
|
||||
|
||||
查询分享元信息(不消耗次数)与真正取件(消耗次数)的完整接口。
|
||||
除 `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` 为文本内容。
|
||||
@@ -0,0 +1,212 @@
|
||||
# 分片上传
|
||||
|
||||
大文件分片上传:客户端把文件切成固定大小的分片逐个上传,服务端按索引合并并做 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": "上传会话不存在" }
|
||||
```
|
||||
@@ -0,0 +1,210 @@
|
||||
# 预签名直传
|
||||
|
||||
服务端预生成上传地址,客户端直接向存储引擎(或服务端代理)上传文件,最后确认建分享。
|
||||
两种模式:
|
||||
|
||||
| 模式 | 引擎 | 上传方式 |
|
||||
|---|---|---|
|
||||
| `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/filecodebox/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": "上传会话已过期" }
|
||||
```
|
||||
@@ -0,0 +1,434 @@
|
||||
# 管理后台 API
|
||||
|
||||
管理端接口:除 `POST /admin/login` 与初始化向导 `/setup` 外,一律要求
|
||||
`Authorization: Bearer <token>`(见《认证与限流》),无效令牌 401。
|
||||
|
||||
## 初始化向导:GET /setup
|
||||
|
||||
未初始化时返回 HTML 配置页(站点名称、管理员密码、上传/限流/保存策略);
|
||||
已初始化时 `303` 重定向到 `/`。
|
||||
|
||||
```bash
|
||||
curl -i http://localhost:8466/setup
|
||||
```
|
||||
|
||||
## 初始化提交:POST /setup
|
||||
|
||||
表单(浏览器向导)或 JSON 均可;成功后写入库配置 KV 并生成密码哈希与 `jwt_secret`。
|
||||
表单提交返回成功 HTML 页;JSON 提交返回 JSON。
|
||||
|
||||
**主要字段**:
|
||||
|
||||
| 字段 | 必需 | 默认 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `admin_password` | ✅ | - | 管理员密码(≥8 位) |
|
||||
| `confirm_password` | ✅ | - | 确认密码(须一致) |
|
||||
| `site_name` | ❌ | 文件快传 | 站点名称 |
|
||||
| `upload_size_value` / `upload_size_unit` | ❌ | 10 / MB | 单文件大小限制(单位 KB/MB/GB) |
|
||||
| `save_time_value` / `save_time_unit` | ❌ | 0 / day | 最长保存秒数(0=不限) |
|
||||
| `expireStyle` | ❌ | day,hour,minute,forever,count | 过期方式(可多值/逗号分隔) |
|
||||
| `code_generate_type` | ❌ | secret | 取件码类型 `number`/`secret` |
|
||||
| `errorCount` / `errorMinute` | ❌ | 10 / 1 | 取件错误限流 |
|
||||
| `loginCount` / `loginMinute` | ❌ | 5 / 15 | 登录失败限流 |
|
||||
| `uploadCount` / `uploadMinute` | ❌ | 10 / 1 | 上传限流 |
|
||||
| `allowed_file_types` | ❌ | `*` | 逗号分隔白名单 |
|
||||
| `openUpload` / `enableChunk` | ❌ | 1 / 0 | 游客上传 / 分片开关(`1`/`true`/`on`/`yes`) |
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8466/setup \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"admin_password":"admin12345","confirm_password":"admin12345","site_name":"我的文件柜","upload_size_value":10,"upload_size_unit":"MB"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 200, "msg": "ok", "data": { "ok": true, "admin": "/#/admin" } }
|
||||
```
|
||||
|
||||
**错误响应**(400,HTML 表单时内嵌错误提示):
|
||||
|
||||
```json
|
||||
{ "code": 400, "msg": "管理员密码至少 8 位" }
|
||||
```
|
||||
|
||||
## 登录:POST /admin/login
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8466/admin/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"password":"admin12345"}'
|
||||
```
|
||||
|
||||
**成功响应**(200):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": {
|
||||
"id": "admin", "username": "admin",
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"token_type": "Bearer",
|
||||
"expires_at": 1750000000,
|
||||
"expires_in": 604800
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
```json
|
||||
{ "code": 401, "msg": "密码错误" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 423, "msg": "请求次数过多,请稍后再试" }
|
||||
```
|
||||
|
||||
## 校验会话:GET /admin/verify
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8466/admin/verify -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 200, "msg": "ok", "data": { "id": "admin", "username": "admin", "token": "eyJ…", "token_type": "Bearer", "expires_at": 1750000000 } }
|
||||
```
|
||||
|
||||
## 登出:POST /admin/logout
|
||||
|
||||
无状态 JWT,服务端仅返回确认(客户端应丢弃令牌):
|
||||
|
||||
```json
|
||||
{ "code": 200, "msg": "ok", "data": { "ok": true } }
|
||||
```
|
||||
|
||||
## 仪表盘:GET /admin/dashboard
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8466/admin/dashboard -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": {
|
||||
"totalFiles": 42,
|
||||
"storageUsed": "123456789",
|
||||
"sysUptime": 1750000000000,
|
||||
"yesterdayCount": 5, "yesterdaySize": "1048576",
|
||||
"todayCount": 12, "todaySize": "5242880",
|
||||
"activeCount": 40, "expiredCount": 2,
|
||||
"textCount": 10, "fileCount": 32, "chunkedCount": 3,
|
||||
"usedCount": 156,
|
||||
"storageBackend": "local",
|
||||
"uploadSizeLimit": 10485760,
|
||||
"openUpload": 1, "enableChunk": 1,
|
||||
"maxSaveSeconds": 0,
|
||||
"topSuffixes": [ { "suffix": ".pdf", "count": 12 }, { "suffix": "Text", "count": 10 } ],
|
||||
"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 } ],
|
||||
"recentActivities": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> `storageUsed`/`todaySize`/`yesterdaySize` 为字符串字节数;`sysUptime` 为服务启动时刻的 Unix 毫秒。
|
||||
|
||||
## 文件列表:GET /admin/file/list
|
||||
|
||||
**参数**:
|
||||
|
||||
| 参数 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `page` / `size` | 1 / 10 | 分页(size 1~100) |
|
||||
| `keyword` | - | 模糊匹配取件码/文件名/哈希/文本内容 |
|
||||
| `status` | - | `active` / `expired` |
|
||||
| `type` | - | `text` / `file` / `chunked` |
|
||||
| `sortBy` | `created_at` | `created_at`/`expired_at`/`name`/`size`/`used_count`/`code` |
|
||||
| `sortOrder` | `desc` | `asc` / `desc` |
|
||||
|
||||
```bash
|
||||
curl -s "http://localhost:8466/admin/file/list?page=1&size=10&status=active&sortBy=size&sortOrder=desc" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": {
|
||||
"page": 1, "size": 10, "total": 40,
|
||||
"summary": { "totalFiles": 42, "activeCount": 40, "expiredCount": 2, "textCount": 10, "fileCount": 32, "chunkedCount": 3, "storageUsed": 123456789, "usedCount": 156 },
|
||||
"data": [
|
||||
{ "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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 文件详情:GET /admin/file/detail
|
||||
|
||||
`GET ?id=42` 或 `POST {"id":42}`。返回列表条目字段;文本分享额外含 `content`(全文)。
|
||||
|
||||
```json
|
||||
{ "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 } }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 404, "msg": "文件不存在" }
|
||||
```
|
||||
|
||||
## 更新文件:PATCH /admin/file/update
|
||||
|
||||
更新取件码/文件名(前后缀)/过期策略。**PATCH 为主名,POST 为兼容别名**。
|
||||
|
||||
**请求体**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `id` | int | 必需 |
|
||||
| `code` | string | 新取件码(冲突 400「code已存在」) |
|
||||
| `prefix` / `suffix` | string | 文件名前后缀 |
|
||||
| `expired_at` | string | 过期时间(ISO 8601,如 `2025-07-01T00:00:00+08:00`) |
|
||||
| `expired_count` | int | 取件次数上限(`-1` 按时间/永久) |
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH http://localhost:8466/admin/file/update \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"id":42,"expired_at":"2025-07-01T00:00:00+08:00","expired_count":10}'
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 200, "msg": "ok", "data": "更新成功" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 400, "msg": "code已存在" }
|
||||
```
|
||||
|
||||
## 删除文件:DELETE /admin/file/delete
|
||||
|
||||
删除分享记录并连带删除存储文件(文本分享无存储文件)。`DELETE` 或 `POST`,请求体 `{"id":42}`。
|
||||
|
||||
```bash
|
||||
curl -s -X DELETE http://localhost:8466/admin/file/delete \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"id":42}'
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 200, "msg": "ok", "data": null }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 400, "msg": "请选择要删除的文件" }
|
||||
```
|
||||
|
||||
## 批量删除:POST /admin/file/batch-delete
|
||||
|
||||
`POST` 或 `DELETE`,请求体 `{"ids":[41,42,43]}`。返回逐条统计:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": {
|
||||
"requestedCount": 3, "deletedCount": 2, "missingCount": 1, "failedCount": 0,
|
||||
"deleted": [41, 42], "missing": [43], "failed": [],
|
||||
"requested_count": 3, "deleted_count": 2, "missing_count": 1, "failed_count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 批量更新:PATCH /admin/file/batch-update
|
||||
|
||||
`PATCH` 或 `POST`。请求体:`ids[]` 必需;`expired_at`(ISO 8601)/`expired_count` 二选一;
|
||||
`clearExpiredAt: true`(或 `clear_expired_at`)= 清空过期时间并置 `expired_count=-1`(永久)。
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH http://localhost:8466/admin/file/batch-update \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"ids":[41,42],"clearExpiredAt":true}'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200, "msg": "ok",
|
||||
"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 }
|
||||
}
|
||||
```
|
||||
|
||||
## 过期策略动作:PATCH /admin/file/policy-action
|
||||
|
||||
对单个文件执行快捷策略。`PATCH` 或 `POST`。批量版为 `/admin/file/batch-policy-action`(`{"ids":[…]}`),响应结构同批量更新。
|
||||
|
||||
**请求体**:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `id` | 文件 ID |
|
||||
| `action` | `extend_24h` / `extend_7d` / `make_permanent` / `reset_download_limit` |
|
||||
| `downloadLimit` | 仅 `reset_download_limit` 用:新取件次数(默认 5,须 >0) |
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH http://localhost:8466/admin/file/policy-action \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"id":42,"action":"reset_download_limit","downloadLimit":3}'
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 200, "msg": "ok", "data": { "id": 42, "action": "reset_download_limit" } }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 400, "msg": "不支持的策略动作" }
|
||||
```
|
||||
|
||||
> `extend_24h`/`extend_7d` 在当前过期时间(未过期时)基础上顺延;`make_permanent` 清空过期时间并置次数 -1。
|
||||
|
||||
## 管理员下载:GET /admin/file/download?id=
|
||||
|
||||
下载原文件(**不消耗取件次数**);文件返回二进制流(支持 Range),文本分享返回 JSON(`data` 为文本内容)。
|
||||
|
||||
```bash
|
||||
curl -s -OJ "http://localhost:8466/admin/file/download?id=42" -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## 文本预览:GET /admin/file/preview
|
||||
|
||||
仅文本分享可用(文件分享返回 400)。`maxChars` 截断长度(默认 4000,1~20000)。
|
||||
|
||||
```bash
|
||||
curl -s "http://localhost:8466/admin/file/preview?id=41&maxChars=100" -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": {
|
||||
"id": 41, "code": "8XQ2M", "name": "Text.txt", "type": "text",
|
||||
"content": "你好,文件快传",
|
||||
"length": 24, "previewLength": 24, "truncated": false,
|
||||
"maxChars": 100, "max_chars": 100,
|
||||
"created_at": "2025-06-01T12:00:00+08:00", "createdAt": "2025-06-01T12:00:00+08:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 400, "msg": "仅文本分享支持预览" }
|
||||
```
|
||||
|
||||
## 读取配置:GET /admin/config/get
|
||||
|
||||
返回运行时配置 KV(含默认值与管理端修改)。`admin_token` 恒返回空串(屏蔽);
|
||||
`jwt_secret` 不下发;存储引擎为进程级单例,`_engine_hint` 提示引擎配置修改需重启。
|
||||
v2 新增键(需求 ①②③④⑩)一并返回:`background_url`、`footer_text`、`footer_beian`、
|
||||
`notify_enabled`、`max_save_count`、`max_file_size` 等。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": {
|
||||
"site_name": "文件快传",
|
||||
"name": "文件快传",
|
||||
"description": "开箱即用的文件快传系统",
|
||||
"page_explain": "…", "keywords": "…",
|
||||
"notify_title": "系统通知", "notify_content": "…", "notify_enabled": 1,
|
||||
"logo_url": "",
|
||||
"favicon_url": "",
|
||||
"background_url": "", "footer_text": "", "footer_beian": "",
|
||||
"openUpload": 1, "uploadSize": 10485760,
|
||||
"max_file_size": 0, "max_save_count": 0,
|
||||
"allowed_file_types": ["*"], "expireStyle": ["day","hour","minute","forever","count"],
|
||||
"code_generate_type": "secret", "enableChunk": 1,
|
||||
"uploadMinute": 1, "uploadCount": 10,
|
||||
"errorMinute": 1, "errorCount": 10,
|
||||
"loginCount": 5, "loginMinute": 15,
|
||||
"max_save_seconds": 0, "storageLimit": 0,
|
||||
"opacity": 0.9, "background": "", "showAdminAddr": 0, "robotsText": "User-agent: *\nDisallow: /",
|
||||
"adminSessionExpire": 604800,
|
||||
"storage_path": "", "local_storage_path": "/app/data",
|
||||
"file_storage": "local",
|
||||
"admin_token": "",
|
||||
"_engine_hint": { "storage_backend": "local", "note": "存储引擎为进程级单例,修改存储引擎相关配置后需重启服务生效" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 存储引擎热切换:POST /admin/storage/switch(v3)
|
||||
|
||||
运行时切换存储引擎,**无需重启**:
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8466/admin/storage/switch \
|
||||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"engine":"s3"}'
|
||||
# 成功:{"code":200,"msg":"ok","data":{"ok":true,"engine":"s3"}}
|
||||
# 失败:503 {"code":503,"msg":"存储引擎切换失败,已保持原引擎: …"}
|
||||
```
|
||||
|
||||
- `engine` 仅接受 `local|s3|webdav`(400 中文错误)。
|
||||
- 切换流程:用最新 KV 参数构建新引擎 → 健康检查 → 通过才替换当前引擎并持久化 `storage_engine`。
|
||||
- 失败(构建/健康检查不过)返回 503,**原引擎与 KV 均保持不变**。
|
||||
- 切到当前引擎为幂等操作(直接返回 200)。
|
||||
- 旧文件按归属引擎(`file_codes.engine`)取回,切换后旧引擎文件仍可下载。
|
||||
|
||||
## 更新配置:PATCH /admin/config/update
|
||||
|
||||
部分更新(JSON 对象,未提供的键不变;表单亦可)。**PATCH 为主名,POST 为兼容别名**。
|
||||
|
||||
- 仅接受管理端可见键(见上响应键集合),未知键忽略。
|
||||
- 数值型键自动转型:`openUpload`、`enableChunk`、`uploadSize`、`storageLimit`、限流四组、`max_save_seconds`、`adminSessionExpire`、`showAdminAddr`;v2 新增 `max_save_count`、`max_file_size`、`notify_enabled`;`opacity` 为浮点。
|
||||
- **v3.1**:`site_domain`(站点对外域名)可经本端点设置,非法格式 400(仅 http/https、主机+端口、不带路径)。
|
||||
- **v3 引擎键**:`storage_engine` 不经本端点修改(走 `POST /admin/storage/switch`);引擎参数键
|
||||
(`local_storage_path`、`webdav_url`、`webdav_root_path`、`webdav_username`、`webdav_password`、
|
||||
`s3_endpoint_url`、`s3_region_name`、`s3_bucket_name`、`s3_access_key_id`、`s3_secret_access_key`、
|
||||
`aws_session_token`、`s3_addressing_style`)可经本端点保存——保存后对应引擎实例缓存失效,
|
||||
下次切换/构建生效;敏感键空串或 `******` 表示不修改。
|
||||
- **v2 schema 校验**(`settings.KVSchema`,越界一律 400,中文错误信息):
|
||||
- 整型边界:`max_file_size` ≤ 10GiB(10737418240)、`max_save_count` ≤ 100000、`max_save_seconds` ≤ 31536000(365 天)、`notify_enabled` ∈ {0,1}、`uploadCount` 1~10000、`uploadMinute` 1~1440 等;
|
||||
- 字符串长度:`background_url` ≤ 2048、`footer_text` ≤ 2000、`footer_beian` ≤ 128、`notify_title` ≤ 128、`notify_content` ≤ 2000 字符;
|
||||
- 列表键 `expireStyle` / `allowed_file_types`:须为字符串数组(或逗号分隔串)且至少保留一项;
|
||||
- 错误示例:`{"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"}`。
|
||||
- `background_url` 协议白名单(需求 ①,防 `javascript:` 注入):仅 `http(s)://`、`data:image/*` 与站内相对路径(`/`开头);空串=清除背景。非法值 400:
|
||||
`{"code":400,"msg":"background_url 仅支持 http(s) 地址、data:image 图片或站内相对路径"}`
|
||||
- `admin_token`:明文密码自动哈希,**并轮换 `jwt_secret`(全部管理员令牌立即失效)**;空串忽略;已是哈希格式则原样保存。
|
||||
- `adminSessionExpire` 须为 1~365 的整天秒数(86400 的整数倍),否则 400。
|
||||
- `storageLimit` 不能小于 0。
|
||||
- 修改限流/策略配置**立即生效**(无需重启:限流规则运行时同步,策略由上传链路每次实时读取);引擎相关(`file_storage`/`s3_*`/`webdav_*`/`storage_path`/`local_storage_path`)需重启。
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH http://localhost:8466/admin/config/update \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"site_name":"我的文件柜","uploadSize":52428800,"openUpload":1,"footer_beian":"京ICP备20240001号","max_file_size":10485760,"max_save_count":5}'
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 200, "msg": "ok", "data": { "ok": true } }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 400, "msg": "adminSessionExpire 必须是 1 到 365 个整天" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 400, "msg": "background_url 仅支持 http(s) 地址、data:image 图片或站内相对路径" }
|
||||
```
|
||||
|
||||
## 修改管理员密码:PATCH /admin/settings/password
|
||||
|
||||
`PATCH` 或 `POST`。新密码 ≥8 位;成功后哈希保存并**轮换 `jwt_secret`,所有旧令牌失效(401)**,需重新登录。
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH http://localhost:8466/admin/settings/password \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"old_password":"admin12345","new_password":"new-pass-6789"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 200, "msg": "ok", "data": { "ok": true } }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 400, "msg": "新密码长度至少 8 位" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 401, "msg": "旧密码错误" }
|
||||
```
|
||||
@@ -0,0 +1,121 @@
|
||||
# 审计日志查询
|
||||
|
||||
所有上传/下载请求由审计中间件自动落库(需求 ③),管理端分页查询。
|
||||
认证:`Authorization: Bearer <token>`。
|
||||
|
||||
## 审计记录内容
|
||||
|
||||
每条审计日志覆盖以下维度(需求 ③):
|
||||
|
||||
| 维度 | 字段 | 说明 |
|
||||
|---|---|---|
|
||||
| 操作时间 | `created_at` | RFC 3339 |
|
||||
| 客户端 | `ip` | 可信代理场景解析 XFF 后的真实 IP |
|
||||
| 终端信息 | `user_agent` | 原始 UA |
|
||||
| 设备解析 | `device_os` / `device_browser` / `device_type` | 由 UA 解析(如 Windows/Chrome/desktop) |
|
||||
| 动作 | `action` | `upload`(上传类) / `download`(取件/下载类) / `admin`(管理端敏感操作,v2.5.6 新增) |
|
||||
| 结果 | `result` | `success` / `denied`(拒绝:401/403/423/428)/ `failed`(失败:其余 4xx/5xx 或业务报错) |
|
||||
| 字节数 | `size_bytes` | 文件总大小;`transferred_bytes` 实际传输(**Range 下载只计实际区间字节**;下载由中间件自动统计,上传由各 handler 填充) |
|
||||
| 耗时 | `duration_ms` | 毫秒 |
|
||||
| 角色 | `actor` | `admin`(有效管理员令牌)/ `guest` |
|
||||
| 业务 | `file_code` / `file_name` | 取件码 / 文件名(分片上传时 `file_code` 为 `upload_id`) |
|
||||
| 错误 | `error_msg` | 失败/拒绝原因 |
|
||||
|
||||
**命中审计的端点**:上传类 `POST /share/text`、`POST /share/file`、`/chunk/upload*`、`/presign*`(POST/PUT);
|
||||
下载类 `GET /share/download`、`GET /share/select`、`GET /share/metadata`;
|
||||
管理类(`action=admin`)`POST /admin/login`、`POST /admin/logout`、`PATCH|POST /admin/config/update`、
|
||||
`PATCH|POST /admin/settings/password`、`POST /admin/storage/switch`、`PATCH|DELETE /admin/file/update|delete|batch-delete|batch-update|policy-action|batch-policy-action`。
|
||||
管理类动作未显式填结果时按 HTTP 状态兜底落库(401/403/423 → `denied`,5xx → `failed`,其余 → `success`)。
|
||||
失败与被拒绝的请求同样落库。
|
||||
|
||||
## 查询接口:GET /admin/audit/list
|
||||
|
||||
**参数**:
|
||||
|
||||
| 参数 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `page` | 1 | 页码(≥1) |
|
||||
| `size` | 20 | 每页条数(1~200;兼容 `pageSize`) |
|
||||
| `action` | - | `upload` / `download` / `admin` |
|
||||
| `result` | - | `success` / `denied` / `failed` |
|
||||
| `ip` | - | 按客户端 IP 过滤 |
|
||||
| `start_time` / `end_time` | - | 时间范围,ISO 8601(如 `2025-06-01T00:00:00+08:00`;也接受 `2006-01-02 15:04:05` / 日期) |
|
||||
|
||||
```bash
|
||||
curl -s "http://localhost:8466/admin/audit/list?page=1&size=20&action=download&result=success&start_time=2025-06-01T00:00:00%2B08:00" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**成功响应**(200):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"id": 318,
|
||||
"action": "download",
|
||||
"file_code": "K3P9W",
|
||||
"file_name": "report.pdf",
|
||||
"size_bytes": 1048576,
|
||||
"transferred_bytes": 524288,
|
||||
"ip": "203.0.113.7",
|
||||
"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",
|
||||
"device_os": "Windows",
|
||||
"device_browser": "Chrome",
|
||||
"device_type": "desktop",
|
||||
"actor": "guest",
|
||||
"result": "success",
|
||||
"error_msg": "",
|
||||
"duration_ms": 128,
|
||||
"created_at": "2025-06-01T12:03:45+08:00",
|
||||
"fileCode": "K3P9W",
|
||||
"fileName": "report.pdf",
|
||||
"sizeBytes": 1048576,
|
||||
"transferredBytes": 524288,
|
||||
"userAgent": "Mozilla/5.0 …",
|
||||
"deviceOs": "Windows",
|
||||
"deviceBrowser": "Chrome",
|
||||
"deviceType": "desktop",
|
||||
"errorMsg": "",
|
||||
"durationMs": 128,
|
||||
"createdAt": "2025-06-01T12:03:45+08:00"
|
||||
}
|
||||
],
|
||||
"total": 1180,
|
||||
"page": 1,
|
||||
"size": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 行字段以 snake_case 为准;camelCase 为兼容双份输出(文档不再重复列出)。
|
||||
|
||||
**错误响应**:
|
||||
|
||||
```json
|
||||
{ "code": 400, "msg": "start_time 时间格式错误" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 401, "msg": "令牌无效或已过期" }
|
||||
```
|
||||
|
||||
## 别名:GET /admin/audit/logs
|
||||
|
||||
与 `/admin/audit/list` 完全相同(同一 handler 的兼容别名),参数与响应一致。
|
||||
|
||||
## 典型查询
|
||||
|
||||
```bash
|
||||
# 最近的下载行为
|
||||
curl -s "http://localhost:8466/admin/audit/list?action=download&size=50" -H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 某 IP 的全部被拒请求(限流/鉴权失败)
|
||||
curl -s "http://localhost:8466/admin/audit/list?ip=203.0.113.7&result=denied" -H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 今天 0 点以来的上传失败
|
||||
curl -s "http://localhost:8466/admin/audit/list?action=upload&result=failed&start_time=2025-06-01T00:00:00%2B08:00" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
@@ -0,0 +1,119 @@
|
||||
# 存储引擎配置
|
||||
|
||||
存储引擎只支持三种:**本地磁盘 / S3 / WebDAV**,由进程级环境变量 `FCB_STORAGE_ENGINE` 选择。
|
||||
引擎与引擎相关配置在启动时一次性读取(`storage.SetEngineOptions` → `NewEngine`),
|
||||
**运行时修改引擎 KV 需重启服务**(管理端 `GET /admin/config/get` 的 `_engine_hint` 亦有提示)。
|
||||
|
||||
## 引擎选择
|
||||
|
||||
```bash
|
||||
FCB_STORAGE_ENGINE=local # 启动默认(KV storage_engine 为空时生效)
|
||||
FCB_STORAGE_ENGINE=s3
|
||||
FCB_STORAGE_ENGINE=webdav
|
||||
```
|
||||
|
||||
非法值直接启动失败:`FCB_STORAGE_ENGINE 无效值 "xxx",仅支持 local|s3|webdav`。
|
||||
|
||||
**v3 运行时热切换**:管理端 `POST /admin/storage/switch`(或后台设置页「存储引擎」卡)可在不重启的情况下切换引擎——
|
||||
先构建新引擎并健康检查,通过才生效;失败 503 保持原引擎。当前引擎持久化在 settings KV `storage_engine`(空=回落启动值)。
|
||||
各引擎参数(存储目录/服务地址/存储桶/密钥)同样在后台设置页运行时可改;保存后对应引擎实例缓存失效,下次切换/构建生效。
|
||||
|
||||
## 文件归属引擎(v3)
|
||||
|
||||
每条分享记录(`file_codes.engine`)与上传会话(`upload_chunks.engine` / `presign_upload_sessions.engine`)
|
||||
在创建时戳记当时的引擎名。下载、分片合并、删除按**归属引擎**操作——切换引擎后,旧引擎里的文件仍可正常下载与删除
|
||||
(空戳为历史数据,回落当前引擎)。
|
||||
|
||||
## 按日期目录存储
|
||||
|
||||
文件落盘路径:`[storage_path/]share/data/YYYY/MM/DD/<uuid>/<文件名>`(如 `share/data/2026/09/04/…`)。
|
||||
按日嵌套目录自然排序、无日月歧义、避免单日海量文件挤在单目录;三种引擎一致适用;历史路径记录在
|
||||
`file_codes.file_path`,不受路径规则调整影响。
|
||||
|
||||
## 配置键与环境变量
|
||||
|
||||
引擎相关配置键(DB settings KV)可由环境变量种子注入(优先级:默认 < 环境变量 < DB KV):
|
||||
|
||||
| KV 键 | 环境变量 | 引擎 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `local_storage_path` | `FCB_LOCAL_STORAGE_PATH` | local | 本地存储根目录(容器内默认 `/app/data`) |
|
||||
| `storage_path` | `FCB_STORAGE_PATH` | 全部 | 存储相对路径前缀(空 = `share/data/…`) |
|
||||
| `s3_access_key_id` | `FCB_S3_ACCESS_KEY_ID` | s3 | 访问密钥 |
|
||||
| `s3_secret_access_key` | `FCB_S3_SECRET_ACCESS_KEY` | s3 | 私有密钥 |
|
||||
| `aws_session_token` | `FCB_AWS_SESSION_TOKEN` | s3 | 可选临时会话令牌 |
|
||||
| `s3_bucket_name` | `FCB_S3_BUCKET_NAME` | s3 | 桶名 |
|
||||
| `s3_endpoint_url` | `FCB_S3_ENDPOINT_URL` | s3 | S3 兼容端点(MinIO/R2 等;AWS 原生可空) |
|
||||
| `s3_region_name` | `FCB_S3_REGION_NAME` | s3 | 区域(默认 `auto`) |
|
||||
| `s3_addressing_style` | `FCB_S3_ADDRESSING_STYLE` | s3 | `auto`/`path`/`virtual` |
|
||||
| `webdav_url` | `FCB_WEBDAV_URL` | webdav | WebDAV 服务地址(如 `http://webdav:5000`) |
|
||||
| `webdav_username` | `FCB_WEBDAV_USERNAME` | webdav | 用户名 |
|
||||
| `webdav_password` | `FCB_WEBDAV_PASSWORD` | webdav | 密码 |
|
||||
| `webdav_root_path` | `FCB_WEBDAV_ROOT_PATH` | webdav | 根目录(默认 `filebox_storage`,不存在自动逐级创建) |
|
||||
|
||||
`FCB_STORAGE_ENGINE` 本身不落库(`GET /admin/config/get` 中的 `file_storage` 为 KV 记忆键,
|
||||
进程实际引擎以 `FCB_STORAGE_ENGINE` 与 `_engine_hint.storage_backend` 为准)。
|
||||
|
||||
## 各引擎要点
|
||||
|
||||
### 本地引擎(local)
|
||||
|
||||
- 原子写:临时文件 + fsync + rename,避免半写文件。
|
||||
- 路径安全:`SanitizePath` + 符号链接逃逸双重防穿越。
|
||||
- Range 下载基于 `SectionReader`;分片按索引有序合并 + SHA256 校验,合并后清理分片目录。
|
||||
|
||||
### S3 引擎(s3)
|
||||
|
||||
- 原生 multipart 流式合并(失败自动 Abort),分片落临时文件保证精确 Content-Length 与可重放。
|
||||
- 预签名 GET/PUT 直链(预签名直传唯一 `direct` 模式引擎)。
|
||||
- SDK 内置 5xx 指数退避重试;`when_required` 校验模式兼容 MinIO/R2 与纯流式转发。
|
||||
|
||||
### WebDAV 引擎(webdav,重点优化)
|
||||
|
||||
- **连接复用**:池化 Transport,连接复用(实测 25 次请求仅 1 条 TCP 连接)。
|
||||
- **认证**:Basic + Digest(RFC 2617 `qop=auth`,MD5/SHA-256)自动协商,401 挑战驱动。
|
||||
- **Range**:`Range` 头透传 + `206` 解析,支持分块/断点下载。
|
||||
- **重试**:5xx/429/408 指数退避(封顶 2s ± 20% 抖动,尊重 `Retry-After`)。
|
||||
- **流式**:下载经 `io.Pipe` 流式转发不落盘;上下文取消挂到响应体读完之后,防止提前断连。
|
||||
- **目录**:按需逐级 `MKCOL` + 目录缓存,避免重复建目录。
|
||||
- **超时**:可配置(`webdav_url` 同级暂无独立超时键,引擎默认值内置)。
|
||||
|
||||
## 健康检查
|
||||
|
||||
三引擎均实现 `HealthCheck`:local 写探针、s3 `ListObjectsV2`、webdav `PROPFIND`(根目录不存在时自建)。
|
||||
服务启动时预检失败仅告警不阻断;运行状态可经 `GET /api/v1/health` 的 `data.storage` 查看当前引擎名。
|
||||
|
||||
## 预签名直传支持矩阵
|
||||
|
||||
| 引擎 | `PresignPutURL` / `PresignGetURL` | init 返回 mode |
|
||||
|---|---|---|
|
||||
| s3 | ✅ | `direct` |
|
||||
| local / webdav | ❌(`ErrNotSupported`) | `proxy`(走服务端代理上传) |
|
||||
|
||||
引擎不支持的操作经统一映射返回 501:
|
||||
|
||||
```json
|
||||
{ "code": 501, "msg": "当前存储引擎不支持该操作" }
|
||||
```
|
||||
|
||||
## Docker Compose 冒烟编排
|
||||
|
||||
`deploy/docker-compose.yml` 提供可选 profile(详见 deploy/README.md):
|
||||
|
||||
```bash
|
||||
docker compose --profile minio up -d --build # MinIO(含 mc 自动建桶)+ FCB_STORAGE_ENGINE=s3
|
||||
docker compose --profile webdav up -d --build # dufs WebDAV 冒烟(admin/admin123)+ FCB_STORAGE_ENGINE=webdav
|
||||
docker compose --profile redis up -d --build # Redis 缓存增强(非引擎)
|
||||
```
|
||||
|
||||
## 存储哨兵错误 → HTTP 状态
|
||||
|
||||
| 哨兵错误 | HTTP | 文案 |
|
||||
|---|---|---|
|
||||
| `ErrNotFound` | 404 | 文件不存在 |
|
||||
| `ErrInvalidPath` | 400 | 非法文件路径 |
|
||||
| `ErrUnavailable` | 503 | 存储服务不可用,请稍后再试 |
|
||||
| `ErrNotSupported` | 501 | 当前存储引擎不支持该操作 |
|
||||
| `ErrRangeNotSatisfiable` | 416 | 请求范围超出文件大小 |
|
||||
| `ErrHashMismatch` | 400 | 分片哈希校验失败,请重新上传 |
|
||||
|
||||
容量超限(`storageLimit`,经容量预留判定)返回 507:`存储空间已达到管理员设置的容量上限`。
|
||||
@@ -0,0 +1,190 @@
|
||||
# 环境变量与配置项
|
||||
|
||||
配置分三层:**默认值 → `FCB_*` 环境变量 → 数据库 settings KV(管理端运行时修改)**。
|
||||
v2 起进程必需的环境变量为空集:数据库默认 **SQLite**(modernc.org/sqlite 纯 Go 驱动,零外部依赖,
|
||||
DSN 缺省落 `./data/filecodebox.db`);`FCB_DB_DRIVER=postgres` 时 `FCB_DB_DSN` 必需(需求 ⑧)。
|
||||
|
||||
## 环境变量(进程级)
|
||||
|
||||
| 变量 | 必需 | 默认 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `FCB_DB_DRIVER` | ❌ | `sqlite` | 数据库驱动:`sqlite` / `postgres`(需求 ⑧) |
|
||||
| `FCB_DB_DSN` | 视驱动 | `./data/filecodebox.db` | postgres:连接串(**必需**,如 `postgres://user:pass@host:5432/filecodebox?sslmode=disable`);sqlite:数据库文件路径(可空,父目录自动创建) |
|
||||
| `FCB_REDIS_ADDR` | ❌ | 空 | Redis 地址(如 `redis:6379`),也支持 `redis://[:password@]host:port[/db]` / `rediss://`(TLS)URL 形式;**空则缓存降级为进程内存实现**(缓存故障时自动降级为进程内限流计数) |
|
||||
| `FCB_REDIS_DB` | ❌ | `0` | Redis 逻辑库号 0-15;URL 形式地址显式携带 `/N` 时以 URL 为准 |
|
||||
| `FCB_ADMIN_PASSWORD` | ❌ | 空 | 设置后服务首次启动即自动初始化管理员(≥8 位,不足告警跳过),消除 `/setup` 被抢占窗口;初始化完成后建议移除 |
|
||||
| `FCB_LISTEN` | ❌ | `:8466` | HTTP 监听地址 |
|
||||
| `FCB_STORAGE_ENGINE` | ❌ | `local` | 存储引擎:`local` / `s3` / `webdav` |
|
||||
| `FCB_TRUSTED_PROXIES` | ❌ | 空 | 可信代理 CIDR(逗号分隔),命中时从 `X-Forwarded-For` 解析真实客户端 IP |
|
||||
|
||||
- SQLite 连接参数(驱动自动注入):`busy_timeout=10s` + `WAL` 日志模式 + `foreign_keys=1`;连接池 8/4。
|
||||
- Postgres 连接池沿用 v1 参数(32/8,1h 轮换);`FCB_DB_DRIVER=postgres` 且未设 `FCB_DB_DSN` 时**启动直接报错**。
|
||||
|
||||
引擎相关环境变量(种子注入 settings KV,见《存储引擎配置》):`FCB_LOCAL_STORAGE_PATH`、
|
||||
`FCB_STORAGE_PATH`、`FCB_S3_ACCESS_KEY_ID`、`FCB_S3_SECRET_ACCESS_KEY`、`FCB_AWS_SESSION_TOKEN`、
|
||||
`FCB_S3_BUCKET_NAME`、`FCB_S3_ENDPOINT_URL`、`FCB_S3_REGION_NAME`、`FCB_S3_ADDRESSING_STYLE`、
|
||||
`FCB_WEBDAV_URL`、`FCB_WEBDAV_USERNAME`、`FCB_WEBDAV_PASSWORD`、`FCB_WEBDAV_ROOT_PATH`。
|
||||
|
||||
部署用编排变量(`deploy/.env.example`):`WEB_PORT`(默认 8466)、
|
||||
`POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB`(默认 filecodebox,仅 `--profile postgres` 时使用)。
|
||||
|
||||
## 配置项(settings KV,默认值对齐参考实现)
|
||||
|
||||
> 键名/类型/默认值/边界以 `server/internal/config/schema.go` 的 `KVSchema()` 为单一事实来源
|
||||
> (schema 同步测试保证与 defaults() 逐键一致);v2 新增键统一 snake_case。
|
||||
|
||||
### 站点信息与展示(需求 ①②③)
|
||||
|
||||
| 键 | 类型/边界 | 默认 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `site_name` / `name` | string | 文件快传 | 站点名称(`site_name` 优先) |
|
||||
| `site_domain` | string,≤256 | 空 | **v3.1**:站点对外域名(`http(s)://host[:port]`,不带路径;裸主机自动补 `http://`)。配置后分享链接(结果卡/管理端复制)用该域名生成——内网部署也能把公网链接发出去;留空=用当前访问地址 |
|
||||
| `description` | string | 开箱即用的文件快传系统 | 站点描述 |
|
||||
| `page_explain` | string | (合规声明) | 页面说明文案 |
|
||||
| `keywords` | string | 文件快传, 文件分享… | SEO 关键词 |
|
||||
| `logo_url` | string | 空(前端回落本地打包 `/assets/logo-*.svg`,需求 ⑤) | 页面导航 Logo,管理端可设任意 URL |
|
||||
| `favicon_url` | string | 空(前端回落本地打包 `/assets/favicon-*.png`,需求 ⑤) | favicon / 备用 Logo |
|
||||
| `opacity` | float | 0.9 | 界面不透明度 |
|
||||
| `background` | string | 空 | 背景图 URL(参考实现既有键,v1 兼容保留) |
|
||||
| `background_url` | string,≤2048 字符 | 空 | **v2 需求 ①**:背景图 URL 或上传后地址(空=主题默认;取值时 legacy `background` 键兜底)。管理端保存时校验协议白名单:仅 `http(s)`、`data:image/*` 与站内相对路径(防 `javascript:` 注入,非法 400) |
|
||||
| `footer_text` | string,≤2000 字符 | 空 | **v2 需求 ②**:页脚自定义内容(纯文本或受控 HTML 片段) |
|
||||
| `footer_beian` | string,≤128 字符 | 空 | **v2 需求 ②**:备案号(如 `京ICP备2024xxxxxx号-1`),展示于页脚 |
|
||||
| `notify_enabled` | int(0/1) | 1 | **v2 需求 ③**:通知开关(1=前台右上角悬浮窗展示 / 0=关闭) |
|
||||
| `notify_title` | string,≤128 字符 | 系统通知 | 通知标题 |
|
||||
| `notify_content` | string,≤2000 字符 | 欢迎使用… | 通知正文(**服务端白名单净化**:仅保留纯文本与 `<a href>` 为 http(s)/站内相对/`#` 锚点的链接,其余标签与事件属性剥离,保存与读取双侧生效) |
|
||||
| `showAdminAddr` | int(0/1) | 0 | 是否展示后台入口 |
|
||||
| `robotsText` | string | `User-agent: *\nDisallow: /` | robots.txt 内容(由公开端点 `GET /robots.txt` 输出) |
|
||||
|
||||
### 保存策略(需求 ④,上传页动态读取并在范围内选择)
|
||||
|
||||
| 键 | 类型/边界 | 默认 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `max_save_seconds` | int64,0~31536000 | 0 | 最长保存秒数上限(0=仅默认 7 天兜底;>0 时按时间过期超限 403「限制最长时间为 X,可换用其他方式」)。**v3**:管理界面以「小时/天」下拉单位编辑(≥1 天自动显示天),提交时前端换算为秒——canonical 单位保持秒,接口语义不变 |
|
||||
| `max_save_count` | int,0~100000 | 0 | **v2 新增**:单次分享最大可取(保存)次数上限(0=不限制;`expire_style=count` 且 `expire_value` 超上限时 403「限制次数最多为 N 次」) |
|
||||
| `expireStyle` | []string | `["day","hour","minute","forever","count"]` | 允许的过期方式白名单(上传时不在白名单 400「过期时间类型错误」) |
|
||||
|
||||
### 存储策略(需求 ④⑩)
|
||||
|
||||
| 键 | 类型/边界 | 默认 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `uploadSize` | int64,1024~10GiB | 10485760(10MB) | 单文件大小上限(字节),参考实现语义;`max_file_size=0` 时作为生效上限 |
|
||||
| `max_file_size` | int64,0~10GiB | 0 | **v2 新增**:存储策略-单文件上限(字节),0=回落 `uploadSize`;超出 403(文案 humanSize 自适应 B/KB/MB/GB)。**v3**:管理界面以「MB/GB」下拉单位编辑(≥1 GiB 自动显示 GB),提交时前端换算为字节 |
|
||||
| `allowed_file_types` | []string | `["*"]` | 允许类型白名单(扩展名/MIME 通配,`*` 不限制;非白名单 403「不允许上传该类型文件」) |
|
||||
| `storageLimit` | int64,≥0 | 0 | 站点总容量(字节),0=不限制(超限 507) |
|
||||
| `openUpload` | int(0/1) | 1 | 游客上传开关(0 时上传接口要求管理员令牌 403) |
|
||||
| `enableChunk` | int(0/1) | 0 | 启用分片上传 |
|
||||
|
||||
### 上传频率限制(需求 ④,既有键对齐参考 ip_limit["upload"])
|
||||
|
||||
| 键 | 类型/边界 | 默认 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `uploadCount` / `uploadMinute` | int(1~10000 / 1~1440) | 10 / 1 | 窗口内允许上传次数 / 窗口分钟(上传成功才计数,超限 423;管理端修改后运行时同步限流规则,立即生效) |
|
||||
| `errorCount` / `errorMinute` | int | 10 / 1 | 取件错误(失败计数)+ metadata 每次计数 |
|
||||
| `loginCount` / `loginMinute` | int | 5 / 15 | 登录失败计数 |
|
||||
|
||||
### 安全与会话
|
||||
|
||||
| 键 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `admin_token` | 空(未初始化) | 管理员密码哈希(`sha256$salt$hash`);GET 配置时屏蔽为空串 |
|
||||
| `jwt_secret` | 空 | JWT 签名密钥(初始化/改密时自动生成轮换;不下发;`settings.SensitiveKeys` 双模式下一致屏蔽) |
|
||||
| `adminSessionExpire` | 604800(7 天) | 管理员会话秒数(须 1~365 整天) |
|
||||
|
||||
### 存储引擎(v3 运行时可配 + 热切换)
|
||||
|
||||
**`storage_engine`**(v3 新增键):string,`local|s3|webdav`,默认空=回落启动值 `FCB_STORAGE_ENGINE`。
|
||||
运行时切换走 **`POST /admin/storage/switch`**(JWT 保护):构建新引擎 → 健康检查通过才生效;
|
||||
失败返回 503「存储引擎切换失败,已保持原引擎: …」且不改 KV。成功后持久化 `storage_engine`,重启沿用。
|
||||
`GET /api/v1/config` 公开下发 `storage_engine` 当前名(仅名称,任何引擎参数/凭据不下发)。
|
||||
|
||||
引擎参数键(管理端可改;保存后对应引擎实例缓存失效,下次切换/构建生效):
|
||||
|
||||
| 键 | 默认 |
|
||||
|---|---|
|
||||
| `file_storage` | `local` |
|
||||
| `storage_path` | 空 |
|
||||
| `local_storage_path` | 空(容器内由 `FCB_LOCAL_STORAGE_PATH=/app/data` 注入) |
|
||||
| `s3_access_key_id` / `s3_secret_access_key` / `aws_session_token` | 空 |
|
||||
| `s3_bucket_name` / `s3_endpoint_url` / `s3_hostname` | 空 |
|
||||
| `s3_region_name` | `auto` |
|
||||
| `s3_signature_version` | `s3v4` |
|
||||
| `s3_addressing_style` | `auto` |
|
||||
| `s3_proxy` | 0 |
|
||||
| `webdav_url` / `webdav_username` / `webdav_password` | 空 |
|
||||
| `webdav_root_path` | `filebox_storage` |
|
||||
| `webdav_proxy` | 0 |
|
||||
|
||||
> 敏感键 `webdav_password` / `s3_secret_access_key` / `aws_session_token`(v3 加入 `settings.SensitiveKeys`):
|
||||
> 管理端 GET 返回掩码 `******`;PATCH 时空串或 `******` 表示不修改。直接写库 settings KV 后重启同样生效。
|
||||
|
||||
## 策略动态生效机制(v2 需求 ④⑩)
|
||||
|
||||
上传页通过 `GET /api/v1/config` 的 `config` 字段读取**当前策略快照**并在范围内渲染选项;
|
||||
上传链路(`/share/file`、`/chunk/*`、`/presign/*`)**每次请求实时读取** settings KV 同一组值校验:
|
||||
|
||||
- 管理端改策略(`PATCH /admin/config/update`)→ 公开 config 即时反映 → 后续上传立即按新策略执行(含 403/400 拒绝与恢复放行)。
|
||||
- 生效上限:`max_file_size > 0` 时为 `max_file_size`,否则回落 `uploadSize`。
|
||||
- 校验点覆盖:单文件(`/share/file`)、分片 init 按分片数上限、分片上传累计、分片 complete 累计、预签名 init 声明大小,五处口径一致(`api.UploadPolicy.CheckSize`)。
|
||||
|
||||
## 公共配置接口
|
||||
|
||||
前端启动时经 `GET /api/v1/config` 获取站点公开配置(无需认证;v2 扩展需求 ①②③④⑩):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": {
|
||||
"config": {
|
||||
"name": "文件快传",
|
||||
"description": "开箱即用的文件快传系统",
|
||||
"explain": "请勿上传或分享违法内容…",
|
||||
"logo_url": "",
|
||||
"favicon_url": "",
|
||||
"background_url": "",
|
||||
"footer_text": "自定义页脚内容",
|
||||
"footer_beian": "京ICP备2024xxxxxx号-1",
|
||||
"notify_enabled": 1,
|
||||
"notify_title": "系统通知",
|
||||
"notify_content": "欢迎使用文件快传…",
|
||||
"uploadSize": 10485760,
|
||||
"max_file_size": 10485760,
|
||||
"maxFileSize": 10485760,
|
||||
"allowedFileTypes": ["*"],
|
||||
"expireStyle": ["day", "hour", "minute", "forever", "count"],
|
||||
"max_save_seconds": 0,
|
||||
"maxSaveSeconds": 0,
|
||||
"max_save_count": 0,
|
||||
"maxSaveCount": 0,
|
||||
"uploadCount": 10,
|
||||
"uploadMinute": 1,
|
||||
"enableChunk": false,
|
||||
"openUpload": true
|
||||
},
|
||||
"meta": {
|
||||
"version": "2.5.6",
|
||||
"features": { "chunkUpload": false, "guestUpload": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 策略字段 snake_case 与 camelCase 双份下发(前端宽松解析);响应为白名单显式构造,
|
||||
> 任何敏感键(`admin_token`/`jwt_secret`)均不会出现。
|
||||
|
||||
## 健康检查
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8466/api/v1/health
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": {
|
||||
"status": "ok",
|
||||
"version": "2.5.6",
|
||||
"storage": "local",
|
||||
"time": "2025-06-01T12:00:00+08:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,62 @@
|
||||
# 错误码
|
||||
|
||||
## 响应结构
|
||||
|
||||
```json
|
||||
{ "code": 404, "msg": "文件不存在" }
|
||||
```
|
||||
|
||||
- `code` 与 HTTP 状态码一致;失败时无 `data` 字段。
|
||||
- `msg` 为中文可读信息,可直接展示给用户。
|
||||
|
||||
## 业务状态码
|
||||
|
||||
| 状态码 | 语义 | 典型场景 |
|
||||
|---|---|---|
|
||||
| 200 | 成功 | 全部正常响应 |
|
||||
| 400 | 参数/格式错误 | 缺字段、过期策略非法、时间格式错误、分片哈希不匹配、code 冲突、`chunk_size` 超 32MiB 上限、presign 实际大小与声明不符、请求体超过大小上限 |
|
||||
| 401 | 未认证 | 管理端令牌缺失/无效;登录密码错误 |
|
||||
| 403 | 拒绝 | 类型白名单拒绝、magic bytes 防伪、游客上传未开启、**分片上传未启用**(enableChunk=0)、presign 直传对象超限(服务端删除对象并释放预留)、下载 `key` 鉴权失败、超过大小/时长限制 |
|
||||
| 404 | 不存在/已过期 | 取件码不存在、文件已过期、上传会话不存在、`/api/*` 未命中路由 |
|
||||
| 409 | 冲突 | 上传容量预留信息不一致 |
|
||||
| 416 | Range 越界 | `Range: bytes=…` 超出文件大小 |
|
||||
| 423 | 限流 | upload/error/login/metadata 任一规则超限 |
|
||||
| 428 | 未初始化 | 系统未初始化时访问除 `/setup`、`/api/v1/health` 外的接口 |
|
||||
| 500 | 服务器错误 | 数据库/内部异常 |
|
||||
| 501 | 引擎不支持 | 引擎不支持预签名等操作(local/webdav 的 `PresignGetURL/PutURL`) |
|
||||
| 503 | 存储不可用 | 存储引擎连接失败/健康检查不通过时的操作 |
|
||||
| 507 | 容量超限 | 达到 `storageLimit` 上限(含上传预留判定) |
|
||||
|
||||
## 存储哨兵错误映射
|
||||
|
||||
存储层哨兵错误统一映射(支持错误包装链判定):
|
||||
|
||||
| 哨兵错误 | HTTP | 响应 msg |
|
||||
|---|---|---|
|
||||
| `ErrNotFound` | 404 | 文件不存在 |
|
||||
| `ErrInvalidPath` | 400 | 非法文件路径 |
|
||||
| `ErrUnavailable` | 503 | 存储服务不可用,请稍后再试 |
|
||||
| `ErrNotSupported` | 501 | 当前存储引擎不支持该操作 |
|
||||
| `ErrRangeNotSatisfiable` | 416 | 请求范围超出文件大小 |
|
||||
| `ErrHashMismatch` | 400 | 分片哈希校验失败,请重新上传 |
|
||||
|
||||
未识别的存储错误归入 500(`存储操作失败: …`)。
|
||||
|
||||
## 错误结果的审计归类
|
||||
|
||||
错误响应同时写入审计日志(需求 ③):
|
||||
|
||||
- `denied`:401 / 403 / 423 / 429 / 428(拒绝类)。
|
||||
- `failed`:其余 4xx / 5xx 及业务显式报错。
|
||||
|
||||
## 常见排障
|
||||
|
||||
| 现象 | 原因与处理 |
|
||||
|---|---|
|
||||
| 全部接口 428 | 未初始化:访问 `GET /setup` 或 `POST /setup` 完成向导 |
|
||||
| 上传 403「本站未开启游客上传」 | `openUpload=0`,携带管理员 Bearer 令牌或后台开启 |
|
||||
| 上传 423 | 触发 upload 限流,等待窗口或调大 `uploadCount/uploadMinute` |
|
||||
| 取件 404「文件已过期」 | 分享过期/次数耗尽;管理员可 `PATCH /admin/file/update` 调整 |
|
||||
| 下载 403「下载鉴权失败」 | `key` 窗口令牌过期/伪造:重新 `POST /share/select` 获取新地址 |
|
||||
| 预签名 init 返回 proxy | local/webdav 引擎不支持直链,按 proxy 流程走服务端代理上传 |
|
||||
| 503 存储服务不可用 | 检查引擎配置与远端服务(S3/WebDAV)连通性;`GET /api/v1/health` 的 `storage` 字段确认引擎 |
|
||||
@@ -0,0 +1,61 @@
|
||||
# Logo 自定义
|
||||
|
||||
## 默认 Logo(内置,v2 需求 ⑤)
|
||||
|
||||
| 项 | 默认值 | 用途 |
|
||||
|---|---|---|
|
||||
| 页面导航 Logo | 前端打包本地资源 `/assets/logo-*.svg`(源:`web/src/assets/brand/logo.svg`) | 导航栏 `<img>`;`config.logo_url` 为空时回落使用 |
|
||||
| favicon / 备用 Logo | 前端打包本地资源 `/assets/favicon-*.png`(源:`web/src/assets/brand/favicon.png`) | `index.html` `<link rel="icon">` + 动态 favicon 回落 |
|
||||
|
||||
v2 起默认不再引用远程 URL:`GET /api/v1/config` 中 `logo_url`/`favicon_url` 默认下发空串,
|
||||
前端 `displayLogoUrl`/`displayFaviconUrl` 判空后回落到打包的本地资源。
|
||||
管理端仍可设置任意 URL 全站替换(三步如下)。
|
||||
|
||||
## 管理端自定义(三步)
|
||||
|
||||
1. **登录后台**:`POST /admin/login` 获取 Bearer 令牌。
|
||||
2. **保存配置**:`PATCH /admin/config/update` 更新 `logo_url`(与可选 `favicon_url`),值为图片 URL 或经管理端上传后得到的地址。
|
||||
3. **全站生效**:保存即写入 settings KV 并热更新,前端读取公共配置立即换新 Logo,无需重启。
|
||||
|
||||
curl 示例:
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH http://localhost:8466/admin/config/update \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"logo_url":"https://cdn.example.com/logo.svg","favicon_url":"https://cdn.example.com/favicon.png"}'
|
||||
```
|
||||
|
||||
```json
|
||||
{ "code": 200, "msg": "ok", "data": { "ok": true } }
|
||||
```
|
||||
|
||||
> 也可以在管理界面「系统设置」页操作(上传图片或填写 URL),效果相同。
|
||||
|
||||
## 校验生效
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8466/api/v1/config
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": { "config": { "logo_url": "https://cdn.example.com/logo.svg", "favicon_url": "https://cdn.example.com/favicon.png" } }
|
||||
}
|
||||
```
|
||||
|
||||
## 恢复默认
|
||||
|
||||
把 `logo_url` / `favicon_url` 置回默认值(空串,前端回落本地打包资源)即可:
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH http://localhost:8466/admin/config/update \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"logo_url":"","favicon_url":""}'
|
||||
```
|
||||
|
||||
## 相关行为
|
||||
|
||||
- 前端运行时优先读取配置值;空值回退前端打包的本地资源(`web/src/assets/brand/logo.svg` + `favicon.png`,经 `displayLogoUrl`/`displayFaviconUrl` 判空回落)。
|
||||
- `site_name` 同样支持运行时自定义(`PATCH /admin/config/update` 的 `site_name` 键)。
|
||||
- Logo/favicon 仅涉及展示层,修改不影响会话与令牌(不轮换 `jwt_secret`)。
|
||||
Reference in New Issue
Block a user