Files
kefu_cloud/server/internal/storage/s3.go
T
yml2213 ec2f12c6ed 实现 MinIO 对象存储图片流水线(可迁移 OSS)
- 新增 S3 兼容 Storage 抽象与 MinIO 实现,compose 启动 MinIO
- 上传接口:校验 → 缩放 → WebP → 主图/缩略图入库
- 消息图片 content 改为对象 URL,拒绝 base64
- 工作台/访客端改为先上传再发消息
2026-07-15 12:21:31 +08:00

105 lines
2.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}