为异步生图任务增加 S3 兼容对象存储支持,任务结果不再把大图内联存进 Redis: - 新增可插拔接口 service.ImageStorage(Save -> url),适配别的厂商只需实现它 - S3 实现 S3ImageStorage(AWS S3 / R2 / 阿里云 OSS / MinIO),与备份共用 S3 客户端构造 - 新增 image_storage 配置(config.yaml + IMAGE_STORAGE_* 环境变量),默认关闭 - enabled 同时作为总开关:关闭或未配置对象存储时,异步生图接口返回 404 且不写 Redis,从根上避免几 MB 的 b64_json 结果撑爆 Redis - 完成时把图片上传对象存储并把结果改写为短链接(公开直链或 presigned), 上传失败则任务标记为失败 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SM1tf3CFVRzC7guuhBXvMd
81 lines
2.2 KiB
Go
81 lines
2.2 KiB
Go
package repository
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/servertiming"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
)
|
|
|
|
// S3ImageStorage 用 S3 兼容对象存储实现 service.ImageStorage。
|
|
type S3ImageStorage struct {
|
|
client *s3.Client
|
|
bucket string
|
|
publicBaseURL string
|
|
presignExpiry time.Duration
|
|
}
|
|
|
|
var _ service.ImageStorage = (*S3ImageStorage)(nil)
|
|
|
|
// NewS3ImageStorage 依据配置构造 S3 图片存储(调用方应先确认 cfg.Active())。
|
|
func NewS3ImageStorage(ctx context.Context, cfg *config.ImageStorageConfig) (*S3ImageStorage, error) {
|
|
client, err := newS3Client(ctx, s3ClientParams{
|
|
Endpoint: cfg.Endpoint,
|
|
Region: cfg.Region,
|
|
AccessKeyID: cfg.AccessKeyID,
|
|
SecretAccessKey: cfg.SecretAccessKey,
|
|
ForcePathStyle: cfg.ForcePathStyle,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
expiry := time.Duration(cfg.PresignExpiry) * time.Hour
|
|
if expiry <= 0 {
|
|
expiry = 24 * time.Hour
|
|
}
|
|
|
|
return &S3ImageStorage{
|
|
client: client,
|
|
bucket: cfg.Bucket,
|
|
publicBaseURL: strings.TrimRight(cfg.PublicBaseURL, "/"),
|
|
presignExpiry: expiry,
|
|
}, nil
|
|
}
|
|
|
|
// Save 上传图片字节,返回可访问 URL:配了 public_base_url 则返回公开直链,否则返回 presigned 临时链接。
|
|
func (s *S3ImageStorage) Save(ctx context.Context, key, contentType string, data []byte) (string, error) {
|
|
finish := servertiming.ObserveDependency(ctx, "s3")
|
|
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
|
|
Bucket: &s.bucket,
|
|
Key: &key,
|
|
Body: bytes.NewReader(data),
|
|
ContentType: &contentType,
|
|
})
|
|
finish()
|
|
if err != nil {
|
|
return "", fmt.Errorf("S3 PutObject: %w", err)
|
|
}
|
|
|
|
if s.publicBaseURL != "" {
|
|
return s.publicBaseURL + "/" + strings.TrimLeft(key, "/"), nil
|
|
}
|
|
|
|
presignClient := s3.NewPresignClient(s.client)
|
|
result, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &s.bucket,
|
|
Key: &key,
|
|
}, s3.WithPresignExpires(s.presignExpiry))
|
|
if err != nil {
|
|
return "", fmt.Errorf("presign url: %w", err)
|
|
}
|
|
return result.URL, nil
|
|
}
|