Files
SeeyonFileSystem/server/storage/minio.go

223 lines
6.3 KiB
Go
Raw Normal View History

2026-07-03 16:01:08 +08:00
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,
2026-07-10 17:33:33 +08:00
Endpoint: cleanEndpoint,
2026-07-03 16:01:08 +08:00
UseSSL: useSSL,
}
2026-07-10 17:33:33 +08:00
// 有凭证时确保 bucket 存在
2026-07-03 16:01:08 +08:00
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
}
2026-07-10 17:33:33 +08:00
// Upload 上传文件
2026-07-03 16:01:08 +08:00
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
}
2026-07-10 17:33:33 +08:00
// Download 下载文件
2026-07-03 16:01:08 +08:00
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
}
2026-07-10 17:33:33 +08:00
// Delete 删除文件
2026-07-03 16:01:08 +08:00
func (e *MinIOEngine) Delete(storageKey string) error {
ctx := context.Background()
2026-07-10 17:33:33 +08:00
return e.Client.RemoveObject(ctx, e.BucketName, storageKey, minio.RemoveObjectOptions{})
2026-07-03 16:01:08 +08:00
}
2026-07-10 17:33:33 +08:00
// Exists 检查文件是否存在
2026-07-03 16:01:08 +08:00
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
}
2026-07-10 17:33:33 +08:00
// GetURL 获取公开访问URL
2026-07-03 16:01:08 +08:00
func (e *MinIOEngine) GetURL(storageKey string) string {
scheme := "http"
if e.UseSSL {
scheme = "https"
}
2026-07-10 17:33:33 +08:00
return fmt.Sprintf("%s://%s/%s/%s", scheme, e.Endpoint, e.BucketName, storageKey)
2026-07-03 16:01:08 +08:00
}
2026-07-10 17:33:33 +08:00
// GetPresignedURL 生成预签名下载链接
2026-07-03 16:01:08 +08:00
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
}
2026-07-10 17:33:33 +08:00
// 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 生成预签名上传链接
2026-07-03 16:01:08 +08:00
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
}
2026-07-10 17:33:33 +08:00
// 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{})
2026-07-03 16:01:08 +08:00
}
2026-07-10 17:33:33 +08:00
return nil
}
// GetPreviewURL 获取预览链接
func (e *MinIOEngine) GetPreviewURL(storageKey string) string {
return e.GetURL(storageKey)
2026-07-03 16:01:08 +08:00
}