- 对象存储直链:S3 引擎 302 到限时预签名 URL(有效期钳位分享剩余时效),失败自动回落代理 - 过期回收:janitor 定时扫描 + 取件惰性回收 + 管理端手动触发(POST /admin/recycle/run), retention_days 最长存储时长;删除走引用计数(去重对象安全) - SHA512 内容去重:三条上传链路落库后计算哈希,命中即复用旧对象并删除本次副本 - 下载防盗链:Referer 白名单(同源/空 Referer/通配域名放行),挂 /share/download - 取件页图片/音频内联预览(下载地址直连,加载失败回退下载按钮) - 文件夹上传:拖拽目录明确提示"建议压缩后上传"(webkitGetAsEntry 探测) - 管理端设置卡「回收与下载安全」8 个新配置键(KVSchema + configKeys + UI + i18n) - 前端产物重建并同步 server/web/dist 与 web-embed - 文档:10-config 配置表、03-file-share 直链/防盗链/文件夹章节、07-admin 回收端点、openapi
文件快传 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 # 26.9 新配置键 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内部分支处理。
26.9 新增配置键(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"]) |
upload_rate / download_rate |
int64 | 0 / 0 |
26.9 带宽限速:上下行字节/秒(0=不限速,≤1 GiB/s)。管理端改后立即生效(每请求动态读 KV);上传侧 middleware 包裹 Request.Body,下载侧在 serveFile 包裹 storage.ReadCloser;S3 预签名直传(客户端→S3)服务端无法限速。算法为时间窗对齐 sleep(middleware/bandwidth.go) |
管理端与文档(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 三引擎(接口签名已冻结):
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 因缺配置失败):
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 层)
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 等),可运行时调整
本地开发
# 默认 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/...)却极易误判为代码问题。 正确写法:cd server export GOCACHE=$(pwd)/../.gocache GOMODCACHE=$(pwd)/../.gomodcache GOSUMDB=off go build ./... && go vet ./... && go test ./... # 后续命令也能继承 export 的变量