实现 MinIO 对象存储图片流水线(可迁移 OSS)
- 新增 S3 兼容 Storage 抽象与 MinIO 实现,compose 启动 MinIO - 上传接口:校验 → 缩放 → WebP → 主图/缩略图入库 - 消息图片 content 改为对象 URL,拒绝 base64 - 工作台/访客端改为先上传再发消息
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"kefu-sys/server/internal/config"
|
||||
)
|
||||
|
||||
// S3Storage 基于 minio-go 的 S3 兼容实现(MinIO / OSS / COS / AWS S3)
|
||||
type S3Storage struct {
|
||||
client *minio.Client
|
||||
bucket string
|
||||
base string
|
||||
}
|
||||
|
||||
func NewS3(cfg config.StorageConfig) (*S3Storage, error) {
|
||||
endpoint := strings.TrimPrefix(strings.TrimPrefix(cfg.Endpoint, "https://"), "http://")
|
||||
client, err := minio.New(endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
|
||||
Secure: cfg.UseSSL,
|
||||
Region: cfg.Region,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init s3 client: %w", err)
|
||||
}
|
||||
base := NormalizePublicBase(cfg.PublicBase)
|
||||
if base == "" {
|
||||
scheme := "http"
|
||||
if cfg.UseSSL {
|
||||
scheme = "https"
|
||||
}
|
||||
base = fmt.Sprintf("%s://%s/%s", scheme, endpoint, cfg.Bucket)
|
||||
}
|
||||
return &S3Storage{client: client, bucket: cfg.Bucket, base: base}, nil
|
||||
}
|
||||
|
||||
func (s *S3Storage) EnsureBucket(ctx context.Context) error {
|
||||
exists, err := s.client.BucketExists(ctx, s.bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if err := s.client.MakeBucket(ctx, s.bucket, minio.MakeBucketOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 开发环境尽量开放公共读;生产用 CDN/桶策略配置
|
||||
policy := fmt.Sprintf(`{
|
||||
"Version":"2012-10-17",
|
||||
"Statement":[{
|
||||
"Effect":"Allow",
|
||||
"Principal":{"AWS":["*"]},
|
||||
"Action":["s3:GetObject"],
|
||||
"Resource":["arn:aws:s3:::%s/*"]
|
||||
}]
|
||||
}`, s.bucket)
|
||||
_ = s.client.SetBucketPolicy(ctx, s.bucket, policy)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *S3Storage) Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) (string, error) {
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
_, err := s.client.PutObject(ctx, s.bucket, key, r, size, minio.PutObjectOptions{
|
||||
ContentType: contentType,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return s.objectURL(key), nil
|
||||
}
|
||||
|
||||
func (s *S3Storage) Delete(ctx context.Context, key string) error {
|
||||
return s.client.RemoveObject(ctx, s.bucket, key, minio.RemoveObjectOptions{})
|
||||
}
|
||||
|
||||
func (s *S3Storage) PublicBase() string { return s.base }
|
||||
|
||||
func (s *S3Storage) objectURL(key string) string {
|
||||
// PublicBase 形如 http://localhost:9000/kefu
|
||||
return s.base + "/" + strings.TrimPrefix(key, "/")
|
||||
}
|
||||
|
||||
// ParseKeyFromURL 从公网 URL 还原 object key(删除时用)
|
||||
func (s *S3Storage) ParseKeyFromURL(publicURL string) (string, error) {
|
||||
if !strings.HasPrefix(publicURL, s.base+"/") {
|
||||
return "", fmt.Errorf("url not in bucket")
|
||||
}
|
||||
key := strings.TrimPrefix(publicURL, s.base+"/")
|
||||
if key == "" {
|
||||
return "", fmt.Errorf("empty key")
|
||||
}
|
||||
if u, err := url.PathUnescape(key); err == nil {
|
||||
return u, nil
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
Reference in New Issue
Block a user