176 lines
5.1 KiB
Go
176 lines
5.1 KiB
Go
|
|
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, "")
|
||
|
|
}
|