Files
backend/pkg/storage/minio.go

179 lines
4.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package storage
import (
"context"
"fmt"
"io"
"net/url"
"strings"
"time"
"carrotskin/pkg/config"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
// StorageClient S3兼容对象存储客户端包装 (支持RustFS、MinIO等)
type StorageClient struct {
client *minio.Client
buckets map[string]string
publicURL string // 公开访问URL前缀
}
// NewStorage 创建新的对象存储客户端 (S3兼容支持RustFS)
func NewStorage(cfg config.RustFSConfig) (*StorageClient, error) {
// 创建S3兼容客户端
// minio-go SDK支持所有S3兼容的存储包括RustFS
// 不指定Region让SDK自动检测
client, err := minio.New(cfg.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
Secure: cfg.UseSSL,
})
if err != nil {
return nil, fmt.Errorf("创建对象存储客户端失败: %w", err)
}
// 测试连接如果AccessKey和SecretKey为空跳过测试
if cfg.AccessKey != "" && cfg.SecretKey != "" {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_, err = client.ListBuckets(ctx)
if err != nil {
return nil, fmt.Errorf("对象存储连接测试失败: %w", err)
}
}
// 构建公开访问URL
publicURL := cfg.PublicURL
if publicURL == "" {
// 如果未配置 PublicURL使用 Endpoint 构建
protocol := "http"
if cfg.UseSSL {
protocol = "https"
}
publicURL = fmt.Sprintf("%s://%s", protocol, cfg.Endpoint)
}
storageClient := &StorageClient{
client: client,
buckets: cfg.Buckets,
publicURL: publicURL,
}
return storageClient, nil
}
// GetClient 获取底层S3客户端
func (s *StorageClient) GetClient() *minio.Client {
return s.client
}
// GetBucket 获取存储桶名称
func (s *StorageClient) GetBucket(name string) (string, error) {
bucket, exists := s.buckets[name]
if !exists {
return "", fmt.Errorf("存储桶 %s 不存在", name)
}
return bucket, nil
}
// BuildFileURL 构建文件的公开访问URL
func (s *StorageClient) BuildFileURL(bucketName, objectName string) string {
return fmt.Sprintf("%s/%s/%s", s.publicURL, bucketName, objectName)
}
// GetPublicURL 获取公开访问URL前缀
func (s *StorageClient) GetPublicURL() string {
return s.publicURL
}
// ObjectInfo 对象信息
type ObjectInfo struct {
Size int64
LastModified time.Time
ContentType string
ETag string
}
// GetObject 获取对象内容和信息
func (s *StorageClient) GetObject(ctx context.Context, bucketName, objectName string) (io.ReadCloser, *ObjectInfo, error) {
obj, err := s.client.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
return nil, nil, fmt.Errorf("获取对象失败: %w", err)
}
stat, err := obj.Stat()
if err != nil {
obj.Close()
return nil, nil, fmt.Errorf("获取对象信息失败: %w", err)
}
info := &ObjectInfo{
Size: stat.Size,
LastModified: stat.LastModified,
ContentType: stat.ContentType,
ETag: stat.ETag,
}
return obj, info, nil
}
// ParseFileURL 从文件URL中解析出bucket和objectName
// URL格式: {publicURL}/{bucket}/{objectName}[?query],自动忽略查询参数
func (s *StorageClient) ParseFileURL(fileURL string) (bucket, objectName string, err error) {
u, err := url.Parse(fileURL)
if err != nil {
return "", "", fmt.Errorf("URL解析失败: %w", err)
}
// 校验前缀(协议+主机+端口)
public, err := url.Parse(s.publicURL)
if err != nil {
return "", "", fmt.Errorf("publicURL解析失败: %w", err)
}
if u.Scheme != public.Scheme || u.Host != public.Host {
return "", "", fmt.Errorf("URL格式不正确必须以 %s 开头", s.publicURL)
}
// 去掉前缀与开头的斜杠,仅使用路径部分,不包含 query
path := strings.TrimPrefix(u.Path, "/")
// 如果 publicURL 自带路径前缀,移除该前缀
pubPath := strings.TrimPrefix(public.Path, "/")
if pubPath != "" {
if !strings.HasPrefix(path, pubPath) {
return "", "", fmt.Errorf("URL格式不正确缺少前缀 %s", public.Path)
}
path = strings.TrimPrefix(path, pubPath)
path = strings.TrimPrefix(path, "/")
}
parts := strings.SplitN(path, "/", 2)
if len(parts) < 2 {
return "", "", fmt.Errorf("URL格式不正确无法解析bucket和objectName")
}
bucket = parts[0]
objectName = parts[1]
// URL解码 objectName
if decoded, decErr := url.PathUnescape(objectName); decErr == nil {
objectName = decoded
}
return bucket, objectName, nil
}
// UploadObject 上传对象到存储
func (s *StorageClient) UploadObject(ctx context.Context, bucketName, objectName string, reader io.Reader, size int64, contentType string) error {
_, err := s.client.PutObject(ctx, bucketName, objectName, reader, size, minio.PutObjectOptions{
ContentType: contentType,
})
if err != nil {
return fmt.Errorf("上传对象失败: %w", err)
}
return nil
}