# 文件快传 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//.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 的变量 ```