package storage import ( "context" "fmt" "io" "strings" "time" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/sundynix/pets-be/internal/config" ) // Storage MinIO 对象存储封装 type Storage struct { client *minio.Client bucket string publicBaseURL string } // New 初始化 MinIO 客户端,并确保 bucket 存在且可公开下载 func New(cfg config.MinIOConfig) (*Storage, error) { client, err := minio.New(cfg.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""), Secure: cfg.UseSSL, }) if err != nil { return nil, fmt.Errorf("minio new: %w", err) } s := &Storage{ client: client, bucket: cfg.Bucket, publicBaseURL: strings.TrimRight(cfg.PublicBaseURL, "/"), } if err := s.ensureBucket(); err != nil { return nil, err } return s, nil } func (s *Storage) ensureBucket() error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() exists, err := s.client.BucketExists(ctx, s.bucket) if err != nil { return fmt.Errorf("bucket exists: %w", err) } if !exists { if err := s.client.MakeBucket(ctx, s.bucket, minio.MakeBucketOptions{}); err != nil { return fmt.Errorf("make bucket: %w", err) } } // dev:设置公开只读策略,便于直接以 URL 访问对象 policy := fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"AWS": ["*"]}, "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::%s/*"] }] }`, s.bucket) if err := s.client.SetBucketPolicy(ctx, s.bucket, policy); err != nil { return fmt.Errorf("set bucket policy: %w", err) } return nil } // Upload 上传对象,返回可公开访问的 URL func (s *Storage) Upload(objectName string, reader io.Reader, size int64, contentType string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() _, err := s.client.PutObject(ctx, s.bucket, objectName, reader, size, minio.PutObjectOptions{ ContentType: contentType, }) if err != nil { return "", fmt.Errorf("put object: %w", err) } return s.PublicURL(objectName), nil } // PublicURL 由对象名拼出可访问 URL(用当前配置的 public_base_url,避免存旧地址) func (s *Storage) PublicURL(objectName string) string { return fmt.Sprintf("%s/%s/%s", s.publicBaseURL, s.bucket, objectName) }