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 存储引擎实例 func NewMinIOEngine(endpoint, accessKey, secretKey, bucket string, useSSL bool) (*MinIOEngine, error) { 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: cleanEndpoint, 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) } 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 上传文件 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 下载文件 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 删除文件 func (e *MinIOEngine) Delete(storageKey string) error { ctx := context.Background() return e.Client.RemoveObject(ctx, e.BucketName, storageKey, minio.RemoveObjectOptions{}) } // Exists 检查文件是否存在 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 生成预签名下载链接 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 } // GetPresignedPreviewURL 生成预签名预览链接(内联展示, 不触发下载) func (e *MinIOEngine) GetPresignedPreviewURL(storageKey string, mimeType string, expiration time.Duration) (string, error) { ctx := context.Background() reqParams := make(url.Values) // S3协议: 通过response-*参数覆盖响应头, 签名时一并签名 reqParams.Set("response-content-disposition", "inline") if mimeType != "" { reqParams.Set("response-content-type", mimeType) } 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 } // GetPresignedChunkURL 生成分片预签名上传URL // chunkKey: 分片的唯一标识, 如 "2026/07/03/xx/chunk_001" func (e *MinIOEngine) GetPresignedChunkURL(chunkKey string, expiration time.Duration) (string, error) { return e.GetPresignedPutURL(chunkKey, expiration) } // MergeChunks 合并分片到目标key // 读取所有分片, 按顺序合并写入目标key func (e *MinIOEngine) MergeChunks(chunkKeys []string, targetKey string) error { ctx := context.Background() // 创建管道用于流式合并 pr, pw := io.Pipe() go func() { defer pw.Close() for _, key := range chunkKeys { obj, err := e.Client.GetObject(ctx, e.BucketName, key, minio.GetObjectOptions{}) if err != nil { pw.CloseWithError(err) return } _, err = io.Copy(pw, obj) obj.Close() if err != nil { pw.CloseWithError(err) return } } }() // 上传合并后的文件(流式, 不需要知道总大小) _, err := e.Client.PutObject(ctx, e.BucketName, targetKey, pr, -1, minio.PutObjectOptions{ ContentType: "application/octet-stream", }) if err != nil { return fmt.Errorf("合并上传失败: %w", err) } // 清理分片 for _, key := range chunkKeys { e.Client.RemoveObject(ctx, e.BucketName, key, minio.RemoveObjectOptions{}) } return nil } // GetPreviewURL 获取预览链接 func (e *MinIOEngine) GetPreviewURL(storageKey string) string { return e.GetURL(storageKey) }