- 数据库默认文件 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 清理)
177 lines
9.9 KiB
Markdown
177 lines
9.9 KiB
Markdown
# 文件快传 Go 后端(server/)
|
||
|
||
Go 1.27.1 + Gin + GORM 重写的文件快传后端。数据库**默认 SQLite**(modernc.org/sqlite
|
||
纯 Go 驱动,CGO_ENABLED=0 交叉编译友好,零外部依赖),配置 `FCB_DB_DRIVER=postgres` 后
|
||
切换 Postgres(需求 ⑧);Redis 为**可选**增强(未配置 `FCB_REDIS_ADDR` 时自动降级为进程内存缓存)。
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
server/
|
||
├── cmd/server/main.go # 入口:装配 配置→DB→缓存→设置→审计→中间件→路由
|
||
└── internal/
|
||
├── config/ # 配置:FCB_* 环境变量基线 + DB settings KV 运行时覆盖
|
||
│ └── schema.go # v2 新配置键 schema(键名/类型/默认值/边界)
|
||
├── model/ # GORM 模型 + 双方言 AutoMigrate
|
||
├── cache/ # 缓存统一接口:redis.go / memory.go 双实现
|
||
├── database/ # 双方言连接与迁移(sqlite 默认 / postgres 可选)
|
||
├── settings/ # settings KV 读写、密码哈希(sha256$salt$hash)、键 schema re-export
|
||
├── audit/ # 审计服务(Sink 抽象、失败重试队列)+ UA 设备解析
|
||
├── middleware/ # JWT 认证、IP 限流、审计中间件、CORS、IP 解析
|
||
├── storage/ # 存储引擎契约(interface.go 为 go-storage 的实现契约)
|
||
└── response/ # 统一响应 {"code":200,"msg":"","data":...}
|
||
```
|
||
|
||
## 环境变量
|
||
|
||
| 变量 | 必需 | 默认 | 说明 |
|
||
|---|---|---|---|
|
||
| `FCB_DB_DRIVER` | ❌ | `sqlite` | 数据库驱动:`sqlite` \| `postgres`(需求 ⑧) |
|
||
| `FCB_DB_DSN` | 视驱动 | `./data/fileshare.db` | postgres:连接串(**必需**),如 `postgres://user:pass@host:5432/fileshare?sslmode=disable`;sqlite:数据库文件路径(可空,自动创建 `data/` 目录) |
|
||
| `FCB_REDIS_ADDR` | ❌ | 空 | 为空时缓存降级为内存实现 |
|
||
| `FCB_LISTEN` | ❌ | `:8466` | 监听地址 |
|
||
| `FCB_STORAGE_ENGINE` | ❌ | `local` | `local` \| `s3` \| `webdav` |
|
||
| `FCB_TRUSTED_PROXIES` | ❌ | 空 | 可信代理 CIDR(逗号分隔),用于解析真实客户端 IP |
|
||
|
||
### 数据库模式(需求 ⑧)
|
||
|
||
- **SQLite(默认)**:`FCB_DB_DRIVER=sqlite`(或缺省)。零 DSN 零依赖启动,数据库文件
|
||
默认 `./data/fileshare.db`(`FCB_DB_DSN` 可覆盖路径;父目录自动创建)。
|
||
连接参数:`busy_timeout=10s` + `WAL` 日志模式 + `foreign_keys=1`(通过 DSN pragma 注入)。
|
||
- **Postgres(可选)**:`FCB_DB_DRIVER=postgres` 且必须提供 `FCB_DB_DSN`,否则启动报错。
|
||
连接池沿用 v1 参数(32/8、1h 轮换)。
|
||
- 双方言共用 GORM 抽象:AutoMigrate、settings KV、全部业务查询方言无关;
|
||
唯一原生 DDL(migrates 台账表)在 `database.createMigratesTable` 内部分支处理。
|
||
|
||
## v2 新增配置键(internal/config/schema.go 为单一事实来源)
|
||
|
||
| 键 | 类型 | 默认 | 说明 |
|
||
|---|---|---|---|
|
||
| `background_url` | string | `""` | 需求 ①:背景图 URL/上传地址(空=主题默认;legacy `background` 键兜底) |
|
||
| `footer_text` | string | `""` | 需求 ②:页脚自定义内容(≤2000 字符) |
|
||
| `footer_beian` | string | `""` | 需求 ②:备案号(≤128 字符) |
|
||
| `notify_enabled` | int | `1` | 需求 ③:通知开关(1 开 / 0 关) |
|
||
| `notify_title` / `notify_content` | string | 见 defaults | 需求 ③:通知标题/内容(沿用参考实现语义) |
|
||
| `max_save_seconds` | int64 | `0` | 需求 ④:最长保存秒数上限(0=仅默认 7 天兜底,≤365 天) |
|
||
| `max_save_count` | int | `0` | 需求 ④:单次分享最大可取次数上限(0=不限制,≤100000) |
|
||
| `max_file_size` | int64 | `0` | 需求 ⑩:存储策略-单文件上限字节(0=回落 `uploadSize`,≤10GiB) |
|
||
| `uploadSize` / `allowed_file_types` / `storageLimit` / `openUpload` | 既有 | - | 存储策略既有键(语义不变) |
|
||
| `uploadCount` / `uploadMinute` | 既有 | `10` / `1` | 上传频率限制(对齐参考 `ip_limit["upload"]`) |
|
||
|
||
管理端与文档(t2/t4)以 `config.KVSchema()`(`settings.KVSchema()` re-export)为元数据源;
|
||
schema 同步测试保证 `KVSchema()` 与 `defaults()` 逐键一致。
|
||
|
||
## 数据模型
|
||
|
||
- `file_codes`:文件/文本分享(对齐参考 `apps/base/models.py::FileCodes`)
|
||
- `upload_chunks`:分片上传记录
|
||
- `key_values`:运行时配置 KV(`settings` / `sys_start` 键)
|
||
- `presign_upload_sessions`:预签名直传会话
|
||
- `storage_reservations`:上传容量预留
|
||
- `audit_logs`:上传/下载审计(时间/IP/UA/设备/动作/结果/字节数/耗时),需求 ③
|
||
- `migrates`:迁移台账表(双方言 DDL 分支,见 `database.createMigratesTable`)
|
||
|
||
## 存储引擎契约(internal/storage/interface.go)
|
||
|
||
`go-storage` 按此契约实现 local/s3/webdav 三引擎(**接口签名已冻结**):
|
||
|
||
```go
|
||
type Storage interface {
|
||
SaveFile(ctx, r io.Reader, savePath) (int64, error) // 流式保存
|
||
DeleteFile(ctx, savePath) error
|
||
Open(ctx, savePath, rng *Range) (*Download, error) // Range 下载
|
||
Stat(ctx, savePath) (*FileMeta, error)
|
||
SaveChunk(ctx, uploadID, chunkIndex, r io.Reader, savePath) (int64, error)
|
||
MergeChunks(ctx, uploadID, total, verifyHash, savePath) (int64, string, error)
|
||
CleanChunks(ctx, uploadID, savePath) error
|
||
FileExists(ctx, savePath) (bool, error)
|
||
PresignGetURL(ctx, savePath, expires) (string, error) // 不支持→ErrNotSupported
|
||
PresignPutURL(ctx, savePath, expires) (string, error)
|
||
HealthCheck(ctx) error
|
||
}
|
||
```
|
||
|
||
错误映射约定:`ErrNotFound`→404、`ErrInvalidPath`→400、`ErrUnavailable`→503、`ErrNotSupported`→501、`ErrRangeNotSatisfiable`→416。
|
||
|
||
引擎通过 `storage.RegisterEngine("local"|"s3"|"webdav", factory)` 注册,
|
||
`storage.NewEngine(ctx, name)` 构造。分片路径约定:`<父目录>/chunks/<uploadID>/<index>.part`。
|
||
|
||
### 引擎构造选项(t3 API 层接入)
|
||
|
||
构造引擎前必须先注入选项(否则 local 退到系统临时目录、s3/webdav 因缺配置失败):
|
||
|
||
```go
|
||
storage.SetEngineOptions(storage.EngineOptions{
|
||
Local: storage.LocalOptions{Root: cfg.GetString("local_storage_path")},
|
||
S3: storage.S3Options{
|
||
AccessKeyID: cfg.GetString("s3_access_key_id"), SecretAccessKey: cfg.GetString("s3_secret_access_key"),
|
||
Bucket: cfg.GetString("s3_bucket_name"), Endpoint: cfg.GetString("s3_endpoint_url"),
|
||
Region: cfg.GetString("s3_region_name"), AddressingStyle: cfg.GetString("s3_addressing_style"),
|
||
},
|
||
WebDAV: storage.WebDAVOptions{
|
||
BaseURL: cfg.GetString("webdav_url"), Username: cfg.GetString("webdav_username"),
|
||
Password: cfg.GetString("webdav_password"), RootPath: cfg.GetString("webdav_root_path"),
|
||
},
|
||
})
|
||
st, err := storage.NewEngine(ctx, cfg.Engine()) // 构造后调 st.HealthCheck(ctx) 完成启动自检
|
||
```
|
||
|
||
引擎要点:local 原子写(临时文件+fsync+rename)、防穿越+符号链接逃逸校验;
|
||
s3 原生 multipart 流式合并、预签名直链、SDK 内置 5xx 重试;webdav 连接池复用、
|
||
Basic/Digest 自动协商、Range 透传、按需逐级 MKCOL(带缓存)、5xx/429 指数退避重试、
|
||
下载 io.Pipe 流式不落盘。三引擎均支持分片上传/合并(SHA256 校验)与 Range 下载。
|
||
|
||
## 审计埋点用法(API 层)
|
||
|
||
```go
|
||
r.Use(middleware.Audit(auditSvc, nil)) // nil=默认按路由前缀分类 upload/download
|
||
|
||
// handler 内填充业务字段并显式落库(推荐):
|
||
middleware.AuditSet(c, func(e *audit.Entry) { e.FileCode = code; e.FileName = name })
|
||
middleware.AuditRecordRequest(c, auditSvc, model.AuditResultSuccess, "")
|
||
// 或不显式落库:中间件按 HTTP 状态兜底(4xx/5xx→failed,401/403/423/429/428→denied)
|
||
```
|
||
|
||
下载响应字节数由中间件自动统计;上传字节数由 handler 填 `TransferredBytes`。
|
||
|
||
## 限流语义(对齐参考实现)
|
||
|
||
- `error`(取件错误)/`login`(登录失败):**仅在失败时计数**,handler 调用 `limiter.Add(c, kind)`
|
||
- `upload`:**成功上传才计数**(先 `Check` 放行,成功后 `Add`)
|
||
- `metadata`:每次访问即计数,可用 `RequireRateLimit` 中间件
|
||
- 超限返回 HTTP 423;规则来自 settings KV(errorCount/errorMinute 等),可运行时调整
|
||
|
||
## 本地开发
|
||
|
||
```bash
|
||
# 默认 SQLite 模式:零依赖,数据库落 ./data/fileshare.db
|
||
go run ./cmd/server # 启动于 :8466
|
||
curl localhost:8466/api/v1/health
|
||
|
||
# Postgres 模式(可选)
|
||
export FCB_DB_DRIVER=postgres
|
||
export FCB_DB_DSN='postgres://postgres:postgres@localhost:5432/fileshare?sslmode=disable'
|
||
go run ./cmd/server
|
||
|
||
# 双方言单测:sqlite 始终执行;postgres 需真实实例(FCB_TEST_PG_DSN 指向测试库)
|
||
FCB_TEST_PG_DSN='postgres://postgres:postgres@localhost:5432/fcb_test?sslmode=disable' go test ./internal/database/ ./internal/settings/
|
||
go test ./... # 全量单测(未设置 FCB_TEST_PG_DSN 时自动跳过 PG 用例)
|
||
```
|
||
|
||
系统未初始化时除 `/setup` 与 `/api/v1/health` 外一律返回 428,初始化路由由 API 层任务接入。
|
||
|
||
## 开发注意
|
||
|
||
- **GOCACHE/GOMODCACHE 必须用 `export` 设置**(沙箱环境):本地与 CI 沙箱通常禁止写默认 Go 缓存
|
||
目录(`~/Library/Caches/go-build`)。注意一个隐蔽的坑——用**内联前缀变量**的方式
|
||
(`GOCACHE=... GOMODCACHE=... go build ./... && go vet ./... && go test ./...`)时,
|
||
只有第一条命令继承这些变量,`&&` 后续命令(vet/test)会丢失前缀、落回默认缓存路径,
|
||
报错 `operation not permitted`(指向 `~/Library/Caches/...`)却极易误判为代码问题。
|
||
正确写法:
|
||
|
||
```bash
|
||
cd server
|
||
export GOCACHE=$(pwd)/../.gocache GOMODCACHE=$(pwd)/../.gomodcache GOSUMDB=off
|
||
go build ./... && go vet ./... && go test ./... # 后续命令也能继承 export 的变量
|
||
```
|