package storage import ( "context" "fmt" "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 } // 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) } } storageClient := &StorageClient{ client: client, buckets: cfg.Buckets, } 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 } // GeneratePresignedURL 生成预签名上传URL (PUT方法) func (s *StorageClient) GeneratePresignedURL(ctx context.Context, bucketName, objectName string, expires time.Duration) (string, error) { url, err := s.client.PresignedPutObject(ctx, bucketName, objectName, expires) if err != nil { return "", fmt.Errorf("生成预签名URL失败: %w", err) } return url.String(), nil } // PresignedPostPolicyResult 预签名POST策略结果 type PresignedPostPolicyResult struct { PostURL string // POST的URL FormData map[string]string // 表单数据 FileURL string // 文件的最终访问URL } // GeneratePresignedPostURL 生成预签名POST URL (支持表单上传) // 注意:使用时必须确保file字段是表单的最后一个字段 func (s *StorageClient) GeneratePresignedPostURL(ctx context.Context, bucketName, objectName string, minSize, maxSize int64, expires time.Duration, useSSL bool, endpoint string) (*PresignedPostPolicyResult, error) { // 创建上传策略 policy := minio.NewPostPolicy() // 设置策略的基本信息 policy.SetBucket(bucketName) policy.SetKey(objectName) policy.SetExpires(time.Now().UTC().Add(expires)) // 设置文件大小限制 if err := policy.SetContentLengthRange(minSize, maxSize); err != nil { return nil, fmt.Errorf("设置文件大小限制失败: %w", err) } // 使用MinIO客户端和策略生成预签名的POST URL和表单数据 postURL, formData, err := s.client.PresignedPostPolicy(ctx, policy) if err != nil { return nil, fmt.Errorf("生成预签名POST URL失败: %w", err) } // 移除form_data中多余的bucket字段(MinIO Go SDK可能会添加这个字段,但会导致签名错误) // 注意:在Go中直接delete不存在的key是安全的 delete(formData, "bucket") // 构造文件的永久访问URL protocol := "http" if useSSL { protocol = "https" } fileURL := fmt.Sprintf("%s://%s/%s/%s", protocol, endpoint, bucketName, objectName) return &PresignedPostPolicyResult{ PostURL: postURL.String(), FormData: formData, FileURL: fileURL, }, nil }