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 全绿;二进制端到端冒烟通过
112 lines
4.9 KiB
Go
112 lines
4.9 KiB
Go
// 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=0,End=Total-1);
|
||
// - rng 非 nil:返回 [Start, End] 区间流。
|
||
// 引擎应尽量透传 Range(WebDAV/S3)或按块 seek(local)。
|
||
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
|
||
}
|