This commit is contained in:
toom1996
2026-09-07 00:04:01 +08:00
parent 6a5a4378ab
commit 10d8a96e8c
29 changed files with 5349 additions and 0 deletions

View File

@ -0,0 +1,71 @@
// 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
}