Files
SKYMirror 84df9996cb
CI 测试 / go vet + go test (push) Successful in 49s
26.9:版本号统一 + CI 精简 + 前端产物重建
- 全项目版本号统一:v3.x 迭代号(26.9/26.9/26.9/26.9 及裸 v2/v3)→ 26.9,
  覆盖 Go 注释 / 文档 / openapi.yaml / README×4 / 前端源码(80+ 处)
- v31_test.go 更名 custom_code_test.go;TestV2AccessorDefaults → TestKVAccessorDefaults
- docs/api/00-overview.md 更新日志合并为单条 26.9 条目(修复错位拼接)
- .goreleaser.yaml 头部注释与实际一致(Pro 2.18.1 / GITEA_TOKEN / semver tag 要求)
- CI:release-image.yml → ci.yml,仅保留 vet+test 门禁;
  镜像发布移交 GoReleaser Pro(原 build-push 的 tag 校验与 26.9 版本方案冲突,历史 9 次失败)
- 前端重建:server/web/dist 与 web-embed 同步(docs 文案嵌入更新)
2026-09-08 03:16:51 +08:00

10 KiB
Raw Permalink Blame History

文件快传 Go 后端(server/

Go 1.27.1 + Gin + GORM 重写的文件快传后端。数据库默认 SQLitemodernc.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.dbFCB_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:运行时配置 KVsettings / 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→failed401/403/423/429/428→denied

下载响应字节数由中间件自动统计;上传字节数由 handler 填 TransferredBytes

限流语义(对齐参考实现)

  • error(取件错误)/login(登录失败):仅在失败时计数handler 调用 limiter.Add(c, kind)
  • upload成功上传才计数(先 Check 放行,成功后 Add
  • metadata:每次访问即计数,可用 RequireRateLimit 中间件
  • 超限返回 HTTP 423;规则来自 settings KVerrorCount/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 的变量