Documentation
¶
Overview ¶
backends.go 存储后端注册表:类型 → Operator 构建器。
此前 buildOperator 是 14 后端巨型 switch,ProbeConfig 另持一份"远端类型" case 清单——新增后端须同步改两处,漏列 ProbeConfig 的坏配置会静默按 local 探测通过(2026-10-05 215 实测事故:不存在的桶 Probe 照样通过,上传全挂)。 现收口为单一注册表:ProbeConfig/BuildAndProbe 的远端判定直接由注册表派生, 未注册且非 local 的类型 fail-loud(未知类型不再静默当 local)。
新增后端只需:① 本表注册构建器;② opendal 子包实现 Scheme 驱动; ③ StorageConfig/ConfigFromConf 补配置段。探测链路自动生效。
chunks.go 分片上传:chunk key、保存/合并/清理,与远端合并用的链式顺序读器。
config.go 存储配置映射:StorageConfig 与 conf.StorageConfig 的互转(bootstrap 与管理端切换共用)。
diskusage_unix.go 本地磁盘容量查询(linux/darwin:Statfs)。
local.go 本地后端路径语义:逃逸收口(localContain)与历史双层布局迁移 (MigrateLegacyLayout)。
presign.go 真预签名直传/直下 URL 生成与 s3 直传核实(HeadObject)。
probe.go 存储后端探测:构造 Operator 并做认证级验证(管理端「测试连接」/切换保存前调用)。
providers.go 云厂商对象存储预设(S3 兼容协议)。
腾讯云 COS / 阿里云 OSS / 百度云 BOS / 金山云 KS3 / 华为云 OBS / AWS S3 均提供 S3 兼容端点,统一经 s3 驱动(minio-go SigV4)读写; 本表负责三件事:
- endpoint 推导:用户只填 region(endpoint 留空时按厂商模板生成)
- path_style 缺省:各厂商均为 virtual-host 风格(MinIO/自建才用 path style)
- type 白名单与展示名
Index ¶
- Constants
- Variables
- func BuildAndProbe(ctx context.Context, cfg *StorageConfig) error
- func DiskUsage(path string) (total, free uint64, err error)
- func IsCloudProviderType(t StorageType) bool
- func MigrateLegacyLayout(dataPath string) (int, int, error)
- func ProbeConfig(ctx context.Context, cfg *StorageConfig) error
- func ResolveCloudProvider(t StorageType, region, bucket, accessKey, secretKey, endpoint string, ...) (map[string]string, error)
- type FileOperationResult
- type ProviderPreset
- type RemoteObjectInfo
- type StorageConfig
- type StorageInterface
- type StorageService
- func (s *StorageService) CleanChunks(ctx context.Context, uploadID string) error
- func (s *StorageService) DataPath() string
- func (s *StorageService) DeleteFile(ctx context.Context, filePath string) error
- func (s *StorageService) EffectiveType() StorageType
- func (s *StorageService) FileExists(ctx context.Context, filePath string) bool
- func (s *StorageService) GetFile(ctx context.Context, filePath string) ([]byte, error)
- func (s *StorageService) GetFileReader(ctx context.Context, filePath string) (io.ReadCloser, int64, error)
- func (s *StorageService) GetFileReaderRange(ctx context.Context, filePath string, start, length int64) (io.ReadCloser, int64, error)
- func (s *StorageService) GetFileSize(ctx context.Context, filePath string) (int64, error)
- func (s *StorageService) GetFileURL(ctx context.Context, filePath string) (string, error)
- func (s *StorageService) HeadObject(ctx context.Context, objectKey string) (int64, string, error)
- func (s *StorageService) InitError() error
- func (s *StorageService) ListRemoteObjects(ctx context.Context, prefix string) ([]RemoteObjectInfo, error)
- func (s *StorageService) LocalAbsPath(rel string) string
- func (s *StorageService) MergeChunks(ctx context.Context, uploadID string, totalChunks int, savePath string) error
- func (s *StorageService) PresignGetURL(ctx context.Context, objectKey string, expire time.Duration, ...) (string, error)
- func (s *StorageService) PresignPutURL(ctx context.Context, objectKey string, expire time.Duration) (string, error)
- func (s *StorageService) Reload(config *StorageConfig) error
- func (s *StorageService) SaveBytes(ctx context.Context, savePath string, data []byte) error
- func (s *StorageService) SaveChunk(ctx context.Context, uploadID string, chunkIndex int, data []byte) error
- func (s *StorageService) SaveFile(ctx context.Context, file *multipart.FileHeader, savePath string) (*FileOperationResult, error)
- func (s *StorageService) SaveStream(ctx context.Context, savePath string, r io.Reader, expectedSize int64) (int64, error)
- func (s *StorageService) StatFile(ctx context.Context, filePath string) (int64, error)
- type StorageType
- type StoredFileEntry
Constants ¶
const ( StorageTypeOSS = StorageType("oss") StorageTypeCOS = StorageType("cos") StorageTypeBOS = StorageType("bos") StorageTypeKS3 = StorageType("ks3") StorageTypeOBS = StorageType("obs") )
Variables ¶
var ErrPresignUnsupported = errors.New("presign direct transfer not supported by current storage backend")
ErrPresignUnsupported 当前存储后端不支持真预签名直传/直下(local/webdav 走服务端中转)。
var ErrRangeUnsupported = opendal.ErrRangeUnsupported
ErrRangeUnsupported 当前存储后端不支持按字节区间读取(Range 下载/断点续传)。 转发 opendal.ErrRangeUnsupported,调用方用 errors.Is 判断后回退全量流。
Functions ¶
func BuildAndProbe ¶
func BuildAndProbe(ctx context.Context, cfg *StorageConfig) error
BuildAndProbe 构造 Operator 并做认证级探测(管理端「测试连接」用)。 local 返回 nil operator(无需探测)。
func DiskUsage ¶ added in v0.15.1
DiskUsage 返回 path 所在文件系统的 (总容量, 可用容量) 字节数。 可用口径 = Bavail(非特权用户实际可用,不含 root 保留块)。
func IsCloudProviderType ¶
func IsCloudProviderType(t StorageType) bool
IsCloudProviderType type 是否为受支持的云厂商(S3 兼容)
func MigrateLegacyLayout ¶ added in v0.32.14
MigrateLegacyLayout 迁移历史双层布局(幂等,可重复调用)。
历史形态:config 的 storage.storage_path 写成 "./data/uploads"(带了 uploads 段), 而存储相对路径本身又以 "uploads/" 开头 ⇒ 物理路径变成 <DataPath>/uploads/uploads/… 多了一层。当前形态:DataPath 是存储根,相对路径 "uploads/2026/…" 解析为 <DataPath>/uploads/2026/…(单层)。
迁移动作:若 <DataPath>/uploads/uploads 存在且非空,把它的子项上提到 <DataPath>/uploads/(同盘 rename,瞬时完成,不复制文件内容)。上提后旧布局目录 变空即删除。冲突(目标同名已存在)时跳过该项并告警,绝不覆盖已有文件。
返回 (上提的条目数, 跳过的条目数, error)。无可迁移内容时返回 (0,0,nil)。
调用时机:bootstrap 拿到存储实例后、对外服务前(读路径必须先于迁移完成)。
func ProbeConfig ¶
func ProbeConfig(ctx context.Context, cfg *StorageConfig) error
ProbeConfig 认证级验证存储配置(管理端切换/保存前调用)。 s3/webdav/ftp/sftp/云厂商:凭据+可达性;local:路径可创建可写。 远端/本地分流由 backends.go 注册表派生——此前是手写 case 清单,云厂商类型 漏列即落 default 被当 local 只探本地路径,坏配置静默上线(2026-10-05 215 实测:不存在的桶 Probe 照样通过,上传全挂);现新后端注册即自动被远端探测, 未知类型显式报错而非静默按 local 放行。
func ResolveCloudProvider ¶
func ResolveCloudProvider(t StorageType, region, bucket, accessKey, secretKey, endpoint string, useSSL, pathStyle *bool) (map[string]string, error)
ResolveCloudProvider 把厂商配置归一为 S3 驱动 options。 返回 (options, error);endpoint 由显式配置或 region 推导得出。
Types ¶
type FileOperationResult ¶
type FileOperationResult struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Error error `json:"-"`
FilePath string `json:"file_path,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
FileHash string `json:"file_hash,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
FileOperationResult 文件操作结果
type ProviderPreset ¶
type ProviderPreset struct {
// DeriveEndpoint 按 region 推导 S3 兼容端点(不带协议前缀;返回空表示必须显式配置)
DeriveEndpoint func(region string) string
// DefaultPathStyle 各厂商均为 virtual-host;MinIO/自建 S3 才默认 path style
DefaultPathStyle bool
// DisplayName 运维展示名
DisplayName string
}
ProviderPreset 厂商预设
type RemoteObjectInfo ¶
RemoteObjectInfo 远端对象列举项(presign 孤儿清理用)。
type StorageConfig ¶
type StorageConfig struct {
Type StorageType
DataPath string // 本地存储路径
BaseURL string // 基础URL
// S3 配置
Endpoint string
AccessKey string
SecretKey string
Bucket string
Region string
UseSSL bool
PathStyle bool // 路径风格寻址(MinIO/Ceph 自建场景)
// WebDAV 配置
WebDAVURL string
WebDAVUsername string
WebDAVPassword string
// Root 远端根目录(webdav:所有对象挂其下,避免绝对路径写入;空 = "pigeonbox")
Root string
// FTP/SFTP/Azure Blob/HDFS/OneDrive 配置(存储驱动扩展;指针复用 conf 结构)
FTP *conf.FTPConfig
SFTP *conf.SFTPConfig
Azure *conf.AzureBlobConfig
HDFS *conf.HDFSConfig
OneDrive *conf.OneDriveConfig
}
StorageConfig 存储配置
func ConfigFromConf ¶
func ConfigFromConf(c *conf.StorageConfig, baseURL string) *StorageConfig
ConfigFromConf 把 conf 的存储配置映射为 StorageConfig(bootstrap 与管理端切换共用)。
func (*StorageConfig) ToConf ¶
func (c *StorageConfig) ToConf() *conf.StorageConfig
ToConf 反向映射(运行时切换后持久化回 conf.StorageConfig 形态)。
type StorageInterface ¶
type StorageInterface interface {
// 基础操作
SaveFile(ctx context.Context, file *multipart.FileHeader, savePath string) (*FileOperationResult, error)
DeleteFile(ctx context.Context, filePath string) error
GetFile(ctx context.Context, filePath string) ([]byte, error)
FileExists(ctx context.Context, filePath string) bool
// 流式写入(本地导入/服务端拷贝用;返回写入字节数)
SaveStream(ctx context.Context, savePath string, r io.Reader, expectedSize int64) (int64, error)
// 分片操作
SaveChunk(ctx context.Context, uploadID string, chunkIndex int, data []byte) error
MergeChunks(ctx context.Context, uploadID string, totalChunks int, savePath string) error
CleanChunks(ctx context.Context, uploadID string) error
// 工具方法
GetFileSize(ctx context.Context, filePath string) (int64, error)
GetFileURL(ctx context.Context, filePath string) (string, error)
// 流式下载方法
GetFileReader(ctx context.Context, filePath string) (io.ReadCloser, int64, error)
}
StorageInterface 存储接口(简化版)
type StorageService ¶
type StorageService struct {
// contains filtered or unexported fields
}
StorageService 存储服务。 local 走本地文件系统(实现与拆分前完全一致);s3/webdav 经 opendal.Operator 分派到对应驱动。管理端在线切换后端时通过 Reload 热替换(在途请求持旧后端完成)。
func NewLocalFallback ¶ added in v0.15.1
func NewLocalFallback(dataPath, baseURL string) *StorageService
NewLocalFallback 构造本地后端兜底实例(未注入统一存储时的测试/降级路径; share/chunk 两域各自懒加载缓存一份,DataPath 沿用各自历史约定—— ./data/uploads 与 ./data 的差异是 2026-10 修复后的刻意参数,勿"统一")。 构造收口此处:兜底语义演进(如 BaseURL 策略)只改一点。
func NewStorageService ¶
func NewStorageService(config *StorageConfig) *StorageService
NewStorageService 创建存储服务(兼容入口)。 远端后端配置非法时降级 local 并记录原因(InitError 可查),与拆分前行为兼容。
func NewStorageServiceE ¶
func NewStorageServiceE(config *StorageConfig) (*StorageService, error)
NewStorageServiceE 创建存储服务;远端后端配置非法时返回错误(bootstrap 用, 失败要显式暴露而不是静默落本地盘)。
func (*StorageService) CleanChunks ¶
func (s *StorageService) CleanChunks(ctx context.Context, uploadID string) error
CleanChunks 清理分片
func (*StorageService) DataPath ¶
func (s *StorageService) DataPath() string
DataPath 返回本地存储根目录(分片临时区;远端模式下仍承载合并副本)。
⚠️ **必须持读锁**(2026-10-31 修):Reload(在线切换存储后端)在写锁下替换 s.config 指针,而本方法此前**无锁读**该指针 ⇒ data race。 调用点都在运行期并发路径上:/admin/diagnostics(pb doctor 消费)、admin 系统信息、后台 janitor 任务(周期性跑,与在线切换并发)。
与私有的 dataPath() 同款:都走读锁取当前快照。
func (*StorageService) DeleteFile ¶
func (s *StorageService) DeleteFile(ctx context.Context, filePath string) error
DeleteFile 删除文件
func (*StorageService) EffectiveType ¶
func (s *StorageService) EffectiveType() StorageType
EffectiveType 实际生效的存储类型(远端构造失败降级 local 时如实返回 local)。
func (*StorageService) FileExists ¶
func (s *StorageService) FileExists(ctx context.Context, filePath string) bool
FileExists 检查文件是否存在
func (*StorageService) GetFileReader ¶
func (s *StorageService) GetFileReader(ctx context.Context, filePath string) (io.ReadCloser, int64, error)
GetFileReader 获取文件读取器(用于流式下载)
func (*StorageService) GetFileReaderRange ¶
func (s *StorageService) GetFileReaderRange(ctx context.Context, filePath string, start, length int64) (io.ReadCloser, int64, error)
GetFileReaderRange 区间读取文件(Range 下载/断点续传)。 返回 (区间读器, 对象总大小)。当前后端不支持区间读时返回 opendal.ErrRangeUnsupported,调用方应回退全量流式(200)。 start 越界(>= 文件大小)返回 os.ErrNotExist 语义错误(调用方 416)。
func (*StorageService) GetFileSize ¶
GetFileSize 获取文件大小
func (*StorageService) GetFileURL ¶
GetFileURL 获取文件URL
func (*StorageService) HeadObject ¶
HeadObject 返回对象大小与 ETag(s3 直传 Complete 时的存在性/大小核实)。
func (*StorageService) InitError ¶
func (s *StorageService) InitError() error
InitError 返回构造降级原因(无降级时为 nil)。
func (*StorageService) ListRemoteObjects ¶
func (s *StorageService) ListRemoteObjects(ctx context.Context, prefix string) ([]RemoteObjectInfo, error)
ListRemoteObjects 列举远端后端 prefix 下对象(递归)。 仅远端后端支持;本地后端返回错误(本地对账走 ReconcileOrphans 磁盘遍历)。
func (*StorageService) LocalAbsPath ¶
func (s *StorageService) LocalAbsPath(rel string) string
LocalAbsPath 本地后端下解析存储相对路径为绝对路径(仅 local 后端;远端返回空)。 供下载链路走 c.File(原生 Range/断点续传/MIME 推断);路径不存在返回空。
func (*StorageService) MergeChunks ¶
func (s *StorageService) MergeChunks(ctx context.Context, uploadID string, totalChunks int, savePath string) error
MergeChunks 合并分片。 local:顺序拼接落盘;远端:懒打开分片的链式读器 + 按总大小流式上传, 不整文件进内存。
func (*StorageService) PresignGetURL ¶
func (s *StorageService) PresignGetURL(ctx context.Context, objectKey string, expire time.Duration, disposition ...string) (string, error)
PresignGetURL 生成真预签名直下 GET URL(短时效,下载 302 用)。 disposition 可选(首项生效):非空时作为 response-content-disposition 签入 URL——对象存储按此回 Content-Disposition,直下 302 客户端才能拿到文件名 (2026-10-07 修复:直下 302 曾无文件名,浏览器按对象 UUID key 命名)。
func (*StorageService) PresignPutURL ¶
func (s *StorageService) PresignPutURL(ctx context.Context, objectKey string, expire time.Duration) (string, error)
PresignPutURL 生成真预签名直传 PUT URL(客户端直传对象存储,服务器不过流量)。 仅 s3 等支持离线签名的后端可用;local/webdav 返回 ErrPresignUnsupported, 调用方(presign 域)据此回退自家中转。
func (*StorageService) Reload ¶
func (s *StorageService) Reload(config *StorageConfig) error
Reload 热切换存储后端(管理端切换存储时调用)。构造失败时保留原后端并返回错误。
func (*StorageService) SaveBytes ¶
SaveBytes 写入内存数据到相对路径(presign 直传使用)。 路径防御收敛在此处:相对 DataPath 计算清洗后的偏移,逃逸(../)即拒绝。
func (*StorageService) SaveChunk ¶
func (s *StorageService) SaveChunk(ctx context.Context, uploadID string, chunkIndex int, data []byte) error
SaveChunk 保存分片
func (*StorageService) SaveFile ¶
func (s *StorageService) SaveFile(ctx context.Context, file *multipart.FileHeader, savePath string) (*FileOperationResult, error)
SaveFile 保存文件。 落盘的同时用 io.TeeReader 流式计算 SHA-256(不额外占用整文件内存), 哈希写入 result.FileHash——这是秒传(按哈希秒传)与完整性的地基。
type StorageType ¶
type StorageType string
StorageType 存储类型
const ( StorageTypeLocal StorageType = "local" StorageTypeS3 StorageType = "s3" StorageTypeWebDAV StorageType = "webdav" StorageTypeFTP StorageType = "ftp" StorageTypeSFTP StorageType = "sftp" StorageTypeGCS StorageType = "gcs" StorageTypeAzBlob StorageType = "azureblob" StorageTypeHDFS StorageType = "hdfs" StorageTypeOneDrv StorageType = "onedrive" )
type StoredFileEntry ¶
type StoredFileEntry struct {
// RelPath 存储相对路径(含唯一文件名;local 下与合并写入路径一致)
RelPath string
// FileName 原始文件名(消毒后,仅展示)
FileName string
// Size 实际大小(handler 已复核)
Size int64
// FileHash SHA-256(可空:哈希失败不阻断分享)
FileHash string
}
StoredFileEntry 已落存储的文件项(多文件分享/访客投递等跨域流转的统一载体)。 语义约定:传输层保证物理文件已就位(直传保存/分片合并/presign 核实), 业务层只做装配。定义在本包使 request 等域引用它时无需依赖具体业务域。