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) HealthCheck(ctx context.Context) error { _, err := s.client.BucketExists(ctx, s.bucket) return err } 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:9100/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 }