package storage import ( "context" "fmt" "io" "net/url" "time" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) // MinIOEngine MinIO 对象存储引擎 type MinIOEngine struct { Client *minio.Client BucketName string Endpoint string UseSSL bool } // NewMinIOEngine 创建 MinIO 存储引擎实例 // endpoint 格式: host:port (如 localhost:9000), 不带 http:// func NewMinIOEngine(endpoint, accessKey, secretKey, bucket string, useSSL bool) (*MinIOEngine, error) { // 清理 endpoint: 去掉 http:// 或 https:// 前缀 cleanEndpoint := endpoint if len(cleanEndpoint) > 7 && cleanEndpoint[:7] == "http://" { cleanEndpoint = cleanEndpoint[7:] } if len(cleanEndpoint) > 8 && cleanEndpoint[:8] == "https://" { cleanEndpoint = cleanEndpoint[8:] } // 去掉末尾的 / if len(cleanEndpoint) > 0 && cleanEndpoint[len(cleanEndpoint)-1] == '/' { cleanEndpoint = cleanEndpoint[:len(cleanEndpoint)-1] } // 支持匿名访问(公开桶不需要凭证) var creds *credentials.Credentials if accessKey != "" && secretKey != "" { creds = credentials.NewStaticV4(accessKey, secretKey, "") } else { creds = credentials.NewStaticV4("", "", "") } client, err := minio.New(cleanEndpoint, &minio.Options{ Creds: creds, Secure: useSSL, }) if err != nil { return nil, fmt.Errorf("创建MinIO客户端失败: %w", err) } engine := &MinIOEngine{ Client: client, BucketName: bucket, Endpoint: endpoint, UseSSL: useSSL, } // 有凭证时确保 bucket 存在(匿名访问跳过) ctx := context.Background() if accessKey != "" { exists, checkErr := client.BucketExists(ctx, bucket) if checkErr != nil { return nil, fmt.Errorf("检查bucket失败: %w", checkErr) } if !exists { if err := client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}); err != nil { return nil, fmt.Errorf("创建bucket失败: %w", err) } // 设置 bucket 为公开读 policy := fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"AWS": ["*"]}, "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::%s/*"] }] }`, bucket) client.SetBucketPolicy(ctx, bucket, policy) } } return engine, nil } // Upload 上传文件到 MinIO func (e *MinIOEngine) Upload(storageKey string, reader io.Reader, size int64) error { ctx := context.Background() _, err := e.Client.PutObject(ctx, e.BucketName, storageKey, reader, size, minio.PutObjectOptions{ ContentType: "application/octet-stream", }) if err != nil { return fmt.Errorf("MinIO上传失败: %w", err) } return nil } // Download 从 MinIO 下载文件 func (e *MinIOEngine) Download(storageKey string) (io.ReadCloser, error) { ctx := context.Background() object, err := e.Client.GetObject(ctx, e.BucketName, storageKey, minio.GetObjectOptions{}) if err != nil { return nil, fmt.Errorf("MinIO下载失败: %w", err) } return object, nil } // Delete 从 MinIO 删除文件 func (e *MinIOEngine) Delete(storageKey string) error { ctx := context.Background() err := e.Client.RemoveObject(ctx, e.BucketName, storageKey, minio.RemoveObjectOptions{}) if err != nil { return fmt.Errorf("MinIO删除失败: %w", err) } return nil } // Exists 判断文件是否存在于 MinIO func (e *MinIOEngine) Exists(storageKey string) (bool, error) { ctx := context.Background() _, err := e.Client.StatObject(ctx, e.BucketName, storageKey, minio.StatObjectOptions{}) if err != nil { // 检查是否是"不存在"的错误 errResponse := minio.ToErrorResponse(err) if errResponse.Code == "NoSuchKey" { return false, nil } return false, err } return true, nil } // GetURL 获取文件的公开访问 URL func (e *MinIOEngine) GetURL(storageKey string) string { scheme := "http" if e.UseSSL { scheme = "https" } return fmt.Sprintf("%s/%s/%s/%s", scheme+"://"+e.Endpoint, e.BucketName, storageKey, "") } // GetPresignedURL 生成预签名下载链接(临时授权) // expiration: 链接有效期 func (e *MinIOEngine) GetPresignedURL(storageKey string, expiration time.Duration) (string, error) { ctx := context.Background() reqParams := make(url.Values) presignedURL, err := e.Client.PresignedGetObject(ctx, e.BucketName, storageKey, expiration, reqParams) if err != nil { return "", fmt.Errorf("生成预签名URL失败: %w", err) } return presignedURL.String(), nil } // GetPresignedPutURL 生成预签名上传链接(用于前端直传) func (e *MinIOEngine) GetPresignedPutURL(storageKey string, expiration time.Duration) (string, error) { ctx := context.Background() presignedURL, err := e.Client.PresignedPutObject(ctx, e.BucketName, storageKey, expiration) if err != nil { return "", fmt.Errorf("生成预签名上传URL失败: %w", err) } return presignedURL.String(), nil } // GetPreviewURL 获取文件预览链接 // 对于图片/文本/PDF等可直接预览的文件, 返回公开URL func (e *MinIOEngine) GetPreviewURL(storageKey string) string { scheme := "http" if e.UseSSL { scheme = "https" } return fmt.Sprintf("%s/%s/%s/%s", scheme+"://"+e.Endpoint, e.BucketName, storageKey, "") }