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:
2026-09-05 04:22:41 +08:00
commit 9686fe887a
173 changed files with 32455 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
// Package storage 定义存储引擎统一契约。
//
// 本文件是 go-storage 并行开发的接口契约:签名一经定义不再改动。
// 三种引擎(local/s3/webdav)都要实现该接口;工厂按 FCB_STORAGE_ENGINE 选择。
package storage
import (
"context"
"errors"
"io"
)
// 错误定义:实现方应返回这些哨兵错误(可用 %w 包装),便于 API 层映射 HTTP 状态码。
var (
// ErrNotFound 文件不存在(HTTP 404)。
ErrNotFound = errors.New("storage: 文件不存在")
// ErrInvalidPath 非法路径(路径穿越等,HTTP 400)。
ErrInvalidPath = errors.New("storage: 非法文件路径")
// ErrUnavailable 存储服务不可用(连接失败等,HTTP 503)。
ErrUnavailable = errors.New("storage: 存储服务不可用")
)
// FileMeta 文件元信息(大小等)。
type FileMeta struct {
Size int64 // 字节数
ContentType string // MIME 类型,可为空
AcceptRanges bool // 是否支持 Range 请求
}
// Download 流式下载句柄。调用方负责 Close。
type Download struct {
// ReadCloser 文件内容流(已按 Range 重定位)。
io.ReadCloser
// Meta 文件元信息。
Meta FileMeta
// Start 当前流的起始字节偏移(Range 请求时为 rangeStart)。
Start int64
// End 流的结束字节偏移(含);未知为 -1。
End int64
// Total 文件总大小(字节);未知为 -1。
Total int64
}
// Range 字节范围(对齐 HTTP Range 语义)。
// nil 指针表示完整文件。
type Range struct {
Start int64 // 起始字节(含)
End int64 // 结束字节(含);-1 表示到文件末尾
}
// Storage 存储引擎统一接口。
//
// 约定:
// - savePath 为存储侧相对路径(引擎内部负责安全解析,拒绝 .. 穿越);
// - 所有方法必须是并发安全的;
// - 实现方遇到不可恢复错误时返回本包哨兵错误(或用 %w 包装)。
type Storage interface {
// SaveFile 流式保存文件:r 读取到 EOF 即完成,返回实际写入字节数。
// 引擎必须按 256KB 级别分块读取,不得将整个文件读入内存。
SaveFile(ctx context.Context, r io.Reader, savePath string) (int64, error)
// DeleteFile 删除文件;文件不存在时返回 ErrNotFound 或 nil 均可接受。
DeleteFile(ctx context.Context, savePath string) error
// Open 以下载模式打开文件,支持 HTTP Range 请求语义:
// - rng 为 nil:返回完整文件流(Start=0End=Total-1);
// - rng 非 nil:返回 [Start, End] 区间流。
// 引擎应尽量透传 RangeWebDAV/S3)或按块 seeklocal)。
Open(ctx context.Context, savePath string, rng *Range) (*Download, error)
// Stat 获取文件元信息;不存在返回 ErrNotFound。
Stat(ctx context.Context, savePath string) (*FileMeta, error)
// SaveChunk 保存一个分片到临时区(upload_id 隔离),返回分片字节数。
SaveChunk(ctx context.Context, uploadID string, chunkIndex int, r io.Reader, savePath string) (int64, error)
// MergeChunks 按索引 0..total-1 有序合并分片并落为正式文件。
// verifyHash 为nil 时不校验;否则为分片 SHA256 校验函数(输入索引,输出期望哈希,空串表示跳过)。
// 返回 (最终文件大小, 整个文件 SHA256)。
MergeChunks(ctx context.Context, uploadID string, total int, verifyHash func(index int) (string, error), savePath string) (int64, string, error)
// CleanChunks 清理分片临时区;不存在时静默成功。
CleanChunks(ctx context.Context, uploadID string, savePath string) error
// FileExists 检查文件是否存在。
FileExists(ctx context.Context, savePath string) (bool, error)
// HeadMeta 读取对象元信息与头部字节(可选能力,供直传 confirm 校验实际
// 大小与内容;不支持时返回 ErrNotSupported)。
// meta 允许为 nil(仅取头部);head 为对象前 headBytes 字节(不足时取实际长度)。
HeadMeta(ctx context.Context, savePath string, headBytes int64) (*FileMeta, []byte, error)
// PresignGetURL 生成限时直链(下载);不支持直链的引擎返回 ErrNotSupported。
PresignGetURL(ctx context.Context, savePath string, expires int64) (string, error)
// PresignPutURL 生成限时直传(上传)URL;不支持直传的引擎返回 ErrNotSupported。
PresignPutURL(ctx context.Context, savePath string, expires int64) (string, error)
// HealthCheck 引擎健康检查(启动时与 /health 使用)。
HealthCheck(ctx context.Context) error
}
// ErrNotSupported 当前引擎不支持该能力(如本地引擎不支持预签名)。
var ErrNotSupported = errors.New("storage: 当前引擎不支持该操作")
// ChunkPath 返回分片临时路径(约定统一为 <dir>/chunks/<upload_id>/<index>.part)。
// 引擎可使用 ChunkDir 拼接自身路径。
type PathBuilder interface {
// ChunkDir 分片临时目录(相对 savePath 所在目录)。
ChunkDir(savePath, uploadID string) string
}