feat: 新增删除文件接口并支持多文件上传

- 新增 DeleteFile 接口,支持通过文件 URL 删除文件
- UploadFile 接口支持多文件上传,返回结果包含文件列表
- DownloadToBrowser 改为流式读取,避免大文件占用内存
- 移除 UploadFileBytes 字节流上传接口
- 修复租户存储容量校验顺序,先校验容量再写入 Redis
This commit is contained in:
2026-06-11 09:14:45 +08:00
parent 99cbdec579
commit 625ec05599
4 changed files with 172 additions and 191 deletions

View File

@@ -1,9 +1,7 @@
package minio
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"net/http"
@@ -138,65 +136,20 @@ func UploadFile(ctx context.Context, fileHeader *ghttp.UploadFile) (imagesUrl st
return objectName, fileHeader.Filename, fileFormat, nil
}
// UploadFileBytes 直接上传字节流到 MinIO
func UploadFileBytes(ctx context.Context, fileName string, fileBytes []byte, fileStoreURL string) (imagesUrl string, fileFormat string, err error) {
bucketName, objectName, fileFormat, err := ensureBucketAndObjectName(ctx, fileName, fileStoreURL)
// DeleteFile 删除单个文件
func DeleteFile(ctx context.Context, objectName string) (err error) {
bucketName, err := utils.GetBucketName(ctx)
if err != nil {
glog.Errorf(ctx, "获取桶名称失败: %v", err)
return
}
// ============== 1. 强制从文件后缀获取 ContentType绝对准确 ==============
ext := strings.ToLower(filepath.Ext(fileName))
contentType := "application/octet-stream" // 默认二进制流
if t, ok := contentTypeMap[ext]; ok {
contentType = t
}
// ============== 2. 强制指定编码,解决 HTML 乱码 ==============
putOpts := minio.PutObjectOptions{
ContentType: contentType,
// 强制存储为 utf-8解决网页/文本乱码
UserMetadata: map[string]string{
"Charset": "UTF-8",
},
}
// ====================== 核心修复 ======================
// 1. 尝试解码 Base64因为 JSON 传 []byte 会自动编码)
var rawBytes []byte
decodedBytes, decodeErr := base64.StdEncoding.DecodeString(string(fileBytes))
if decodeErr == nil {
// 解码成功 → 使用原始二进制(图片/HTML
rawBytes = decodedBytes
} else {
// 解码失败 → 直接使用原字节
rawBytes = fileBytes
}
// 上传
_, err = minioClient.PutObject(
ctx,
bucketName,
objectName,
bytes.NewReader(rawBytes),
int64(len(rawBytes)),
putOpts,
)
// 执行删除
err = minioClient.RemoveObject(ctx, bucketName, objectName, minio.RemoveObjectOptions{})
if err != nil {
glog.Errorf(ctx, "上传文件失败: %v", err)
glog.Errorf(ctx, "删除失败: %v\n", err)
return
}
return objectName, fileFormat, nil
}
// 常见图片/HTML 类型映射
var contentTypeMap = map[string]string{
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".bmp": "image/bmp",
".webp": "image/webp",
".svg": "image/svg+xml",
".html": "text/html",
".htm": "text/html",
return
}
func DownloadToFile(ctx context.Context, fileURL string, localPath string) (err error) {
@@ -225,10 +178,8 @@ func DownloadToFile(ctx context.Context, fileURL string, localPath string) (err
return
}
// DownloadToBrowser 下载文件到浏览器(返回文件流,由 HTTP Response 直接写出
// 参数 fileURL: MinIO 中的对象路径(即 objectName
// 返回: 文件字节流、文件名、ContentType、错误
func DownloadToBrowser(ctx context.Context, fileURL string) (fileBytes []byte, fileName string, contentType string, err error) {
// 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)
@@ -241,25 +192,21 @@ func DownloadToBrowser(ctx context.Context, fileURL string) (fileBytes []byte, f
glog.Errorf(ctx, "获取文件流失败: %v", err)
return
}
defer object.Close()
// 获取对象元信息(用于读取 ContentType 和 Size
// 获取对象元信息
info, err := object.Stat()
if err != nil {
defer object.Close() // 出错必须关闭
glog.Errorf(ctx, "获取文件信息失败: %v", err)
return
}
// 读取全部内容
fileBytes, err = io.ReadAll(object)
if err != nil {
glog.Errorf(ctx, "读取文件流失败: %v", err)
return
}
// 提取文件名
// 直接返回流,不读取到内存
reader = object
fileName = filepath.Base(fileURL)
contentType = info.ContentType
fileSize = info.Size
if contentType == "" {
contentType = "application/octet-stream"
}