// Package storage 抽象「把一段图片字节存成可访问地址」的实现。 // // 两种实现: // - LocalUploader:落到本地磁盘,对外以静态路径暴露(兜底/默认实现)。 // - QiniuUploader:直传七牛云对象存储(标准库实现,无第三方 SDK 依赖)。 // // 入库 worker 主用 Qiniu(若启用),单张失败时兜底写本地,避免图片完全丢失。 package storage import ( "context" "os" "path/filepath" "strings" ) // Uploader 抽象图片上传。 // Upload 返回写入数据库的图片访问地址: // - Qiniu 返回完整 https URL(base_url + key) // - 本地实现返回 Upload.URLPrefix + "/" + key 的相对路径 type Uploader interface { // Upload 上传 data 到 key 指定的对象,返回访问 URL。 Upload(ctx context.Context, key string, data []byte, contentType string) (string, error) // Enabled 表示该实现是否真正启用(用于日志/兜底判断)。 Enabled() bool } // Deleter 抽象图片删除(清理孤儿文件用)。 // QiniuUploader / LocalUploader 都实现本接口;仅当某 key 在所有图集/草稿表中 // 引用计数归零时才由调用方触发,避免误删仍被引用的文件。 type Deleter interface { // Delete 删除 key 指定的对象/文件。key 不存在时按成功处理(幂等)。 Delete(ctx context.Context, key string) error } // LocalUploader 把文件落到本地磁盘并对外以静态路径暴露(兜底/默认实现)。 type LocalUploader struct { Dir string URLPrefix string } // NewLocalUploader 构造本地上传器。 func NewLocalUploader(dir, urlPrefix string) *LocalUploader { return &LocalUploader{Dir: dir, URLPrefix: strings.TrimRight(urlPrefix, "/")} } // Enabled 本地上传始终可用。 func (l *LocalUploader) Enabled() bool { return true } // Upload 写本地文件并返回相对静态路径(如 /uploads/runway/xxx.jpg)。 func (l *LocalUploader) Upload(ctx context.Context, key string, data []byte, contentType string) (string, error) { abs := filepath.Join(l.Dir, filepath.FromSlash(key)) if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil { return "", err } if err := os.WriteFile(abs, data, 0o644); err != nil { return "", err } return l.URLPrefix + "/" + strings.TrimLeft(key, "/"), nil } // Delete 删除本地文件(清理孤儿图用)。key 可能带 URLPrefix 前缀,先剥掉再定位磁盘文件。 func (l *LocalUploader) Delete(ctx context.Context, key string) error { rel := strings.TrimPrefix(key, l.URLPrefix) rel = strings.TrimPrefix(rel, "/") abs := filepath.Join(l.Dir, filepath.FromSlash(rel)) if err := os.Remove(abs); err != nil && !os.IsNotExist(err) { return err } return nil }