Files
oss/minio/minio.go
qhd 625ec05599 feat: 新增删除文件接口并支持多文件上传
- 新增 DeleteFile 接口,支持通过文件 URL 删除文件
- UploadFile 接口支持多文件上传,返回结果包含文件列表
- DownloadToBrowser 改为流式读取,避免大文件占用内存
- 移除 UploadFileBytes 字节流上传接口
- 修复租户存储容量校验顺序,先校验容量再写入 Redis
2026-06-11 09:14:45 +08:00

216 lines
6.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package minio
import (
"context"
"fmt"
"io"
"net/http"
"path/filepath"
"strings"
"time"
"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
}