package storage import ( "context" "fmt" "io" "strings" "time" "git.toowon.com/jimmy/go-common/config" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) // MinIOStorage MinIO存储实现 type MinIOStorage struct { config *config.MinIOConfig client *minio.Client bucket string domain string protocol string } // NewMinIOStorage 创建MinIO存储实例 func NewMinIOStorage(cfg *config.MinIOConfig) (*MinIOStorage, error) { if cfg == nil { return nil, fmt.Errorf("MinIO config is nil") } // 创建MinIO客户端 client, err := minio.New(cfg.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretAccessKey, ""), Secure: cfg.UseSSL, }) if err != nil { return nil, fmt.Errorf("failed to create MinIO client: %w", err) } // 检查bucket是否存在,不存在则创建 ctx := context.Background() exists, err := client.BucketExists(ctx, cfg.Bucket) if err != nil { // 如果检查失败,可能是网络问题,但不阻止客户端创建 // 继续创建客户端,后续上传时会再次检查 } else if !exists { err = client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{}) if err != nil { // 不阻止客户端创建,后续上传时会再次尝试 } } protocol := "http" if cfg.UseSSL { protocol = "https" } return &MinIOStorage{ config: cfg, client: client, bucket: cfg.Bucket, domain: cfg.Domain, protocol: protocol, }, nil } // Upload 上传文件到MinIO func (s *MinIOStorage) Upload(ctx context.Context, objectKey string, reader io.Reader, contentType ...string) error { if s.client == nil { return fmt.Errorf("MinIO client is not initialized") } ct := "application/octet-stream" if len(contentType) > 0 && contentType[0] != "" { ct = contentType[0] } opts := minio.PutObjectOptions{ ContentType: ct, } _, err := s.client.PutObject(ctx, s.bucket, objectKey, reader, -1, opts) if err != nil { return fmt.Errorf("failed to upload object: %w", err) } return nil } // GetURL 获取MinIO文件访问URL func (s *MinIOStorage) GetURL(objectKey string, expires int64) (string, error) { if s.client == nil { return "", fmt.Errorf("MinIO client is not initialized") } // 如果设置了过期时间,生成预签名URL if expires > 0 { ctx := context.Background() expiry := time.Duration(expires) * time.Second presignedURL, err := s.client.PresignedGetObject(ctx, s.bucket, objectKey, expiry, nil) if err != nil { return "", fmt.Errorf("failed to generate presigned URL: %w", err) } return presignedURL.String(), nil } // 使用自定义域名或默认域名 if s.domain != "" { // 使用自定义域名 if strings.HasSuffix(s.domain, "/") { return fmt.Sprintf("%s://%s%s/%s", s.protocol, s.domain, s.bucket, objectKey), nil } return fmt.Sprintf("%s://%s/%s/%s", s.protocol, s.domain, s.bucket, objectKey), nil } // 使用MinIO默认域名 // 格式: http://endpoint/bucket/objectKey return fmt.Sprintf("%s://%s/%s/%s", s.protocol, s.config.Endpoint, s.bucket, objectKey), nil } // Delete 删除MinIO文件 func (s *MinIOStorage) Delete(ctx context.Context, objectKey string) error { if s.client == nil { return fmt.Errorf("MinIO client is not initialized") } err := s.client.RemoveObject(ctx, s.bucket, objectKey, minio.RemoveObjectOptions{}) if err != nil { return fmt.Errorf("failed to delete object: %w", err) } return nil } // Exists 检查MinIO文件是否存在 func (s *MinIOStorage) Exists(ctx context.Context, objectKey string) (bool, error) { if s.client == nil { return false, fmt.Errorf("MinIO client is not initialized") } _, err := s.client.StatObject(ctx, s.bucket, objectKey, minio.StatObjectOptions{}) if err != nil { errResp := minio.ToErrorResponse(err) if errResp.Code == "NoSuchKey" { return false, nil } return false, fmt.Errorf("failed to check object existence: %w", err) } return true, nil } // GetObject 获取MinIO文件内容 func (s *MinIOStorage) GetObject(ctx context.Context, objectKey string) (io.ReadCloser, error) { if s.client == nil { return nil, fmt.Errorf("MinIO client is not initialized") } obj, err := s.client.GetObject(ctx, s.bucket, objectKey, minio.GetObjectOptions{}) if err != nil { return nil, fmt.Errorf("failed to get object: %w", err) } return obj, nil }