Files
oss/minio/minio.go

216 lines
6.4 KiB
Go
Raw Permalink Normal View History

package minio
import (
"context"
"fmt"
"io"
"net/http"
"path/filepath"
"strings"
"time"
2026-06-10 16:30:36 +08:00
"gitea.redpowerfuture.com/red-future/common/utils"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/glog"
"github.com/google/uuid"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
// 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 IoConfig
// 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)
}
}
}
// ensureBucketAndObjectName 确保桶存在,并生成对象名
// 返回: bucketName, objectName, fileFormat, error
func ensureBucketAndObjectName(ctx context.Context, fileName string, fileStoreURL string) (string, string, string, error) {
bucketName, err := utils.GetBucketName(ctx)
if err != nil {
glog.Errorf(ctx, "获取桶名称失败: %v", err)
return "", "", "", err
}
// 检查/创建桶
exists, err := minioClient.BucketExists(ctx, bucketName)
if err != nil {
glog.Errorf(ctx, "检查桶是否存在失败: %v", err)
return "", "", "", err
}
if !exists {
if err = minioClient.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: minioCfg.Region}); err != nil {
glog.Errorf(ctx, "创建桶失败: %v", err)
return "", "", "", err
}
glog.Infof(ctx, "成功创建 MinIO 桶: %s", bucketName)
}
// 生成对象名
fileExt := filepath.Ext(fileName)
uniqueID := uuid.New().String()[:32]
timestamp := time.Now().Format("2006-01-02")
objectName := fmt.Sprintf("/%s/%s%s", timestamp, uniqueID, fileExt)
if fileStoreURL != "" {
objectName = fmt.Sprintf("/%s/%s/%s%s", timestamp, fileStoreURL, uniqueID, fileExt)
}
// 设置存储桶公共读权限
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
}
return bucketName, objectName, strings.TrimPrefix(fileExt, "."), nil
}
func UploadFile(ctx context.Context, fileHeader *ghttp.UploadFile) (imagesUrl string, fileName string, fileFormat string, err error) {
bucketName, objectName, fileFormat, err := ensureBucketAndObjectName(ctx, fileHeader.Filename, "")
if err != nil {
return
}
// 打开文件,获取 io.Reader*os.File 实现了 io.Reader
file, err := fileHeader.Open()
if err != nil {
glog.Errorf(ctx, "打开文件失败: %v", err)
return
}
defer file.Close() // 必须关闭,避免文件句柄泄露
// 获取文件类型
buffer := make([]byte, 512)
_, err = file.Read(buffer)
if err != nil {
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
}
// 执行图片上传
_, err = minioClient.PutObject(
ctx,
bucketName,
objectName,
file,
fileHeader.Size,
minio.PutObjectOptions{
ContentType: contentType, // 关键指定图片MIME类型S3会根据此类型处理
},
)
if err != nil {
glog.Errorf(ctx, "上传图片失败: %v", err)
return
}
return objectName, fileHeader.Filename, fileFormat, nil
}
// DeleteFile 删除单个文件
func DeleteFile(ctx context.Context, objectName string) (err error) {
bucketName, err := utils.GetBucketName(ctx)
if err != nil {
glog.Errorf(ctx, "获取桶名称失败: %v", err)
return
}
// 执行删除
err = minioClient.RemoveObject(ctx, bucketName, objectName, minio.RemoveObjectOptions{})
if err != nil {
glog.Errorf(ctx, "删除失败: %v\n", err)
return
}
return
}
func DownloadToFile(ctx context.Context, fileURL string, localPath string) (err error) {
bucketName, err := utils.GetBucketName(ctx) // 桶名
if err != nil {
glog.Errorf(ctx, "获取桶名称失败: %v", err)
return
}
fileName := filepath.Base(fileURL)
localPath = filepath.Join(localPath, fileName)
// 执行下载
err = minioClient.FGetObject(
ctx,
bucketName,
fileURL,
localPath,
minio.GetObjectOptions{}, // 可设置签名、加密等参数
)
if err != nil {
fmt.Println("下载失败:", err)
return
}
return
}
// DownloadToBrowser 下载文件到浏览器(返回 读取接口 + 文件名 + 类型 + 大小,不加载全量内存)
func DownloadToBrowser(ctx context.Context, fileURL string) (reader io.Reader, fileName string, contentType string, fileSize int64, err error) {
bucketName, err := utils.GetBucketName(ctx)
if err != nil {
glog.Errorf(ctx, "获取桶名称失败: %v", err)
return
}
// 获取对象流
object, err := minioClient.GetObject(ctx, bucketName, fileURL, minio.GetObjectOptions{})
if err != nil {
glog.Errorf(ctx, "获取文件流失败: %v", err)
return
}
// 获取对象元信息
info, err := object.Stat()
if err != nil {
defer object.Close() // 出错必须关闭
glog.Errorf(ctx, "获取文件信息失败: %v", err)
return
}
// 直接返回流,不读取到内存
reader = object
fileName = filepath.Base(fileURL)
contentType = info.ContentType
fileSize = info.Size
if contentType == "" {
contentType = "application/octet-stream"
}
return
}