// Package storage 提供对象存储基础设施 // 封装 MinIO 客户端初始化和存储桶自动创建逻辑 package storage import ( "context" "fmt" "strings" "github.com/echochat/backend/config" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) // NewMinioClient 初始化 MinIO 客户端并确保存储桶存在 // 启动时: // 1. 创建配置中指定的 bucket(如不存在) // 2. 为 bucket 设置 public-read 策略,使前端可通过 URL 直接访问已上传的图片、语音、文件等资源 // (写入操作仍需 AccessKey/SecretKey 凭据,不影响安全性) func NewMinioClient(cfg *config.MinioConfig) (*minio.Client, error) { 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("创建 MinIO 客户端失败: %w", err) } ctx := context.Background() exists, err := client.BucketExists(ctx, cfg.Bucket) if err != nil { return nil, fmt.Errorf("检查存储桶 %s 失败: %w", cfg.Bucket, err) } if !exists { if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{}); err != nil { return nil, fmt.Errorf("创建存储桶 %s 失败: %w", cfg.Bucket, err) } } if err := ensureBucketPublicRead(ctx, client, cfg.Bucket); err != nil { return nil, fmt.Errorf("设置存储桶 %s 公开读策略失败: %w", cfg.Bucket, err) } return client, nil } // ensureBucketPublicRead 设置 bucket 的匿名公开读策略 // 允许匿名用户通过 HTTP GET 访问 bucket 内的对象(s3:GetObject) // 其他操作(上传、删除等)仍然需要 AccessKey/SecretKey 凭据 func ensureBucketPublicRead(ctx context.Context, client *minio.Client, bucket string) error { policy := fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": {"AWS": ["*"]}, "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::%s/*"] } ] }`, bucket) return client.SetBucketPolicy(ctx, bucket, policy) } func BuildObjectURL(cfg *config.MinioConfig, objectName string) string { objectName = strings.TrimLeft(strings.TrimSpace(objectName), "/") if cfg == nil { return objectName } bucket := strings.Trim(strings.TrimSpace(cfg.Bucket), "/") if base := strings.TrimRight(strings.TrimSpace(cfg.PublicBaseURL), "/"); base != "" { if bucket != "" && !strings.HasSuffix(base, "/"+bucket) { base = fmt.Sprintf("%s/%s", base, bucket) } if objectName == "" { return base } return fmt.Sprintf("%s/%s", base, objectName) } scheme := "http" if cfg.UseSSL { scheme = "https" } if objectName == "" { return fmt.Sprintf("%s://%s/%s", scheme, cfg.Endpoint, bucket) } return fmt.Sprintf("%s://%s/%s/%s", scheme, cfg.Endpoint, bucket, objectName) }