minio文件上传存储路径优化

This commit is contained in:
2025-12-30 16:17:57 +08:00
committed by 张斌
parent 342a7b00cb
commit 5ded2b08a2

View File

@@ -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)
}