// Package storage 是文件上传的存储抽象。R2(S3 兼容 API)是第一个实现, // 本地磁盘是零配置兜底;以后接其他存储端 = 再写一个 BlobStore 实现, // 从 FromEnv 的选择逻辑里加一个分支,调用方(admin API / 公开路由)不变。 // // URL 不在存储层决定:文件行里记 store 来源,读取时按「来源 + PublicBase」 // 解析直链或后端代理路由,因此切换存储端不破坏存量链接。 package storage import ( "context" "io" "net/url" "strings" ) // BlobStore 是对象存储的最小面。size 只是提示(S3 PutObject 需要预知长度, // 用临时文件或内存缓冲满足),实现方不得依赖它做校验——类型与大小上限 // 在上传 handler 里统一把关。 type BlobStore interface { Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) error Open(ctx context.Context, key string) (io.ReadCloser, int64, error) Delete(ctx context.Context, key string) error } // FileURL 解析文件的公开访问地址:R2 上的文件且配了公开域名(PublicBase, // 尾部不带 /)走 CDN 直链;其余(本地兜底、或没配公开域名)走后端的 // /uploads/key 流式路由。调用方传入文件行里的 store 来源。 func FileURL(storeName, key, publicBase string) string { if storeName == "r2" && publicBase != "" { return publicBase + "/" + key } return "/uploads/" + key } // EndpointKeyPrefix 提取端点 URL 里的路径段(去首尾斜杠)作为上传 key 的 // 前缀:S3Api 写成 https://host/oss 时,对象 key 会带上 oss/ 前缀, // 数据库 / 直链 / 存储端三方都用这个前缀对齐。没有路径段返回空串。 func EndpointKeyPrefix(endpoint string) string { u, err := url.Parse(strings.TrimSpace(endpoint)) if err != nil { return "" } return strings.Trim(u.Path, "/") } // StripEndpointPath 去掉端点 URL 的路径,只留 scheme://host[:port]: // SDK 的 BaseEndpoint 用它(路径段已经折进 key 前缀,见 EndpointKeyPrefix)。 func StripEndpointPath(endpoint string) string { u, err := url.Parse(strings.TrimSpace(endpoint)) if err != nil { return endpoint } return u.Scheme + "://" + u.Host }