diff --git a/minio/minio.go b/minio/minio.go index 7ffcf96..03df1d4 100644 --- a/minio/minio.go +++ b/minio/minio.go @@ -3,8 +3,11 @@ package minio import ( "context" "fmt" + "gitee.com/red-future---jilin-g/common/utils" + "github.com/gogf/gf/v2/os/glog" + "github.com/gogf/gf/v2/util/gconv" + "net/http" "path/filepath" - "strings" "time" "github.com/gogf/gf/v2/frame/g" @@ -14,134 +17,128 @@ import ( "github.com/minio/minio-go/v7/pkg/credentials" ) -// MinIOConfig 映射 YAML 中的 minio 配置节点 -type MinIOConfig struct { - Endpoint string `yaml:"endpoint"` // MinIO API 地址 - AccessKey string `yaml:"accessKey"` // AK - SecretKey string `yaml:"secretKey"` // SK - Secure bool `yaml:"secure"` // 是否启用 SSL - Region string `yaml:"region"` // 区域 - BucketName string `yaml:"bucketName"` // 默认桶名 - PresignedExpire string `yaml:"presignedExpire"` // 预签名URL过期时间(如 5m、1h) +// IoConfig 映射 YAML 中的 minio 配置节点 +type IoConfig struct { + Endpoint string `yaml:"endpoint"` // MinIO API 地址 + AccessKey string `yaml:"accessKey"` // AK + SecretKey string `yaml:"secretKey"` // SK + Secure bool `yaml:"secure"` // 是否启用 SSL + Region string `yaml:"region"` // 区域 } // 全局 MinIO 客户端(初始化一次,避免重复创建) var minioClient *minio.Client -var minioCfg MinIOConfig +var minioCfg IoConfig -// initMinIO 初始化 MinIO 客户端 -func initMinIO(ctx context.Context) error { - var err error - // 加载 MinIO 配置(可从配置文件/环境变量读取,这里硬编码示例) - minioCfg = MinIOConfig{ - Endpoint: g.Cfg().MustGet(ctx, "minio.endpoint").String(), - AccessKey: g.Cfg().MustGet(ctx, "minio.accessKey").String(), - SecretKey: g.Cfg().MustGet(ctx, "minio.secretKey").String(), - Secure: g.Cfg().MustGet(ctx, "minio.secure").Bool(), - Region: g.Cfg().MustGet(ctx, "minio.region").String(), - BucketName: g.Cfg().MustGet(ctx, "minio.bucketName").String(), // 专门存储图片的桶 +// initMinIO 初始化 MinIO 客户端。 +func init() { + ctx := context.Background() + if !g.Cfg().MustGet(ctx, "minio").IsEmpty() { + // 加载 MinIO 配置(可从配置文件/环境变量读取,这里硬编码示例) + minioCfg = IoConfig{ + Endpoint: g.Cfg().MustGet(ctx, "minio.endpoint").String(), + AccessKey: g.Cfg().MustGet(ctx, "minio.accessKey").String(), + SecretKey: g.Cfg().MustGet(ctx, "minio.secretKey").String(), + Secure: g.Cfg().MustGet(ctx, "minio.secure").Bool(), + Region: g.Cfg().MustGet(ctx, "minio.region").String(), + } + // 创建 MinIO 客户端 + var err error + if minioClient, err = minio.New(minioCfg.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(minioCfg.AccessKey, minioCfg.SecretKey, ""), + Secure: minioCfg.Secure, + Region: minioCfg.Region, + }); err != nil { + glog.Errorf(ctx, "初始化 MinIO 客户端失败: %v", err) + } } - // 创建 MinIO 客户端 - minioClient, err = minio.New(minioCfg.Endpoint, &minio.Options{ - Creds: credentials.NewStaticV4(minioCfg.AccessKey, minioCfg.SecretKey, ""), - Secure: minioCfg.Secure, - Region: minioCfg.Region, - }) +} + +func UploadFile(ctx context.Context, fileHeader *ghttp.UploadFile) (imagesUrl string, err error) { + return uploadFile(ctx, getBucketName(ctx), fileHeader) +} + +func uploadFile(ctx context.Context, bucketName string, fileHeader *ghttp.UploadFile) (imagesUrl string, err error) { + // 检查/创建桶 + exists, err := minioClient.BucketExists(ctx, bucketName) if err != nil { - return fmt.Errorf("初始化 MinIO 客户端失败: %w", err) - } - // 检查/创建图片桶 - exists, err := minioClient.BucketExists(ctx, minioCfg.BucketName) - if err != nil { - return fmt.Errorf("检查桶是否存在失败: %w", err) + glog.Errorf(ctx, "检查桶是否存在失败: %v", err) + return } if !exists { - err = minioClient.MakeBucket(ctx, minioCfg.BucketName, minio.MakeBucketOptions{Region: minioCfg.Region}) - if err != nil { - return fmt.Errorf("创建桶失败: %w", err) + if err = minioClient.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: minioCfg.Region}); err != nil { + glog.Errorf(ctx, "创建桶失败: %v", err) + return } - fmt.Printf("成功创建 MinIO 桶: %s\n", minioCfg.BucketName) + glog.Infof(ctx, "成功创建 MinIO 桶: %s", bucketName) } - return nil -} - -func UploadImages(ctx context.Context, fileHeaders []*ghttp.UploadFile) (imagesUrls []string, err error) { - // 初始化 MinIO 客户端 - err = initMinIO(ctx) - if err != nil { - return nil, fmt.Errorf("初始化 MinIO 客户端失败: %w", err) - } - imageUrls := make([]string, 0, len(fileHeaders)) // 存储上传后的图片访问地址 - for _, fileHeader := range fileHeaders { - var imageUrl string - imageUrl, err = uploadImg(ctx, fileHeader) - if err != nil { - return nil, err - } - imageUrls = append(imageUrls, imageUrl) - } - return imageUrls, err -} - -func UploadImage(ctx context.Context, fileHeader *ghttp.UploadFile) (imagesUrl string, err error) { - // 初始化 MinIO 客户端 - err = initMinIO(ctx) - if err != nil { - return "", fmt.Errorf("初始化 MinIO 客户端失败: %w", err) - } - imageUrl, err := uploadImg(ctx, fileHeader) - if err != nil { - return "", err - } - return imageUrl, err -} - -func uploadImg(ctx context.Context, fileHeader *ghttp.UploadFile) (imagesUrl string, err error) { - // 生成唯一的 MinIO 对象名(避免覆盖) - fileExt := filepath.Ext(fileHeader.Filename) // 原文件后缀(如 .jpg) - uniqueID := uuid.New().String()[:8] // 8位随机UUID - timestamp := time.Now().Format("20060102") // 日期目录(便于管理) - objectName := fmt.Sprintf("images/%s/%s%s", timestamp, uniqueID, fileExt) // 存储路径:images/20251209/abc12345.jpg // 打开文件,获取 io.Reader(*os.File 实现了 io.Reader) file, err := fileHeader.Open() - if err != nil { /* 处理错误 */ - return "", fmt.Errorf("打开文件失败: %w", err) + if err != nil { + glog.Errorf(ctx, "打开文件失败: %v", err) + return } defer file.Close() // 必须关闭,避免文件句柄泄露 - // 设置存储桶公共读权限 - policy := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::` + minioCfg.BucketName + `/*"]}]}` - err = minioClient.SetBucketPolicy(context.Background(), minioCfg.BucketName, policy) + // 获取文件类型 + buffer := make([]byte, 512) + _, err = file.Read(buffer) if err != nil { - return "", fmt.Errorf("设置存储桶权限失败: %w", err) + glog.Errorf(ctx, "读取文件头失败: %v", err) + return + } + contentType := http.DetectContentType(buffer) + // 重置文件读取位置,否则后续 PutObject 会从第512字节开始上传 + if _, err = file.Seek(0, 0); err != nil { + glog.Errorf(ctx, "重置文件读取位置失败: %v", err) + return + } + // 生成唯一的 MinIO 对象名(避免覆盖) + fileExt := filepath.Ext(fileHeader.Filename) // 原文件后缀(如 .jpg) + uniqueID := uuid.New().String()[:32] // 32位随机UUID + timestamp := time.Now().Format("2006-01-02") // 日期目录(便于管理) + objectName := fmt.Sprintf("/%s/%s%s", timestamp, uniqueID, fileExt) // 存储路径:20251209/abc12345.jpg + // 设置存储桶公共读权限 + policy := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::` + bucketName + `/*"]}]}` + if err = minioClient.SetBucketPolicy(ctx, bucketName, policy); err != nil { + glog.Errorf(ctx, "设置存储桶权限失败: %v", err) + return } // 执行图片上传 _, err = minioClient.PutObject( ctx, - minioCfg.BucketName, + bucketName, objectName, file, fileHeader.Size, minio.PutObjectOptions{ - ContentType: "image/jpeg", // 关键:指定图片MIME类型,S3会根据此类型处理 + ContentType: contentType, // 关键:指定图片MIME类型,S3会根据此类型处理 // 若需要图片可公开访问,添加如下配置(根据需求选择) //ACL: minio.ACLPublicRead, }, ) if err != nil { - return "", fmt.Errorf("上传图片失败: %w", err) + glog.Errorf(ctx, "上传图片失败: %v", err) + return } - replace := strings.Replace(objectName, "images/", "/", 1) - return replace, err + return objectName, err } -// GetImgAddressPrefix 拼接图片前缀地址 -func GetImgAddressPrefix(ctx context.Context) (imageUrl string) { +// GetIFileAddressPrefix 拼接图片前缀地址 +func GetIFileAddressPrefix(ctx context.Context) (imageUrl string) { // 拼接图片前缀地址 - secure := g.Cfg().MustGet(ctx, "minio.secure").Bool() var url = "http://" - if secure { + if minioCfg.Secure { url = "https://" } - imgAddressPrefix := url + g.Cfg().MustGet(ctx, "minio.endpoint").String() + "/" + g.Cfg().MustGet(ctx, "minio.bucketName").String() + "/images" + imgAddressPrefix := url + minioCfg.Endpoint + "/" + getBucketName(ctx) return imgAddressPrefix } + +func getBucketName(ctx context.Context) (bucketName string) { + user, err := utils.GetUserInfo(ctx) + if err != nil { + glog.Errorf(ctx, "获取用户信息失败: %v", err) + return + } + return "tenantid-" + gconv.String(user.TenantId) +}