feat: 添加文件下载及字节流上传功能

This commit is contained in:
2026-04-22 08:45:45 +08:00
parent 2181cab5a5
commit 7fa8b6eb7d
4 changed files with 188 additions and 23 deletions

View File

@@ -4,6 +4,8 @@ import (
"context"
"oss/model/dto"
"oss/service"
"gitea.com/red-future/common/beans"
)
type file struct{}
@@ -14,7 +16,18 @@ var File = new(file)
func init() {
}
// DownloadToFile 下载文件到本地
func (c *file) DownloadToFile(ctx context.Context, req *dto.DownloadToFileReq) (res *beans.ResponseEmpty, err error) {
err = service.File.DownloadToFile(ctx, req)
return
}
// UploadFile 上传文件
func (c *file) UploadFile(ctx context.Context, req *dto.UploadFileReq) (res *dto.UploadFileRes, err error) {
return service.File.UploadFile(ctx, req)
}
// UploadFileBytes 上传文件(字节流)
func (c *file) UploadFileBytes(ctx context.Context, req *dto.UploadFileBytesReq) (res *dto.UploadFileRes, err error) {
return service.File.UploadFileBytes(ctx, req)
}

View File

@@ -1,7 +1,9 @@
package minio
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"net/http"
"path/filepath"
@@ -54,29 +56,49 @@ func init() {
}
}
func UploadFile(ctx context.Context, fileHeader *ghttp.UploadFile) (imagesUrl string, fileName string, fileFormat string, err error) {
return uploadFile(ctx, fileHeader)
}
func uploadFile(ctx context.Context, fileHeader *ghttp.UploadFile) (imagesUrl string, fileName string, fileFormat string, err error) {
// 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
return "", "", "", err
}
// 检查/创建桶
exists, err := minioClient.BucketExists(ctx, bucketName)
if err != nil {
glog.Errorf(ctx, "检查桶是否存在失败: %v", err)
return
return "", "", "", err
}
if !exists {
if err = minioClient.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: minioCfg.Region}); err != nil {
glog.Errorf(ctx, "创建桶失败: %v", err)
return
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 {
@@ -97,17 +119,6 @@ func uploadFile(ctx context.Context, fileHeader *ghttp.UploadFile) (imagesUrl st
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,
@@ -117,13 +128,98 @@ func uploadFile(ctx context.Context, fileHeader *ghttp.UploadFile) (imagesUrl st
fileHeader.Size,
minio.PutObjectOptions{
ContentType: contentType, // 关键指定图片MIME类型S3会根据此类型处理
// 若需要图片可公开访问,添加如下配置(根据需求选择)
//ACL: minio.ACLPublicRead,
},
)
if err != nil {
glog.Errorf(ctx, "上传图片失败: %v", err)
return
}
return objectName, fileHeader.Filename, strings.ReplaceAll(fileExt, ".", ""), err
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)
if err != nil {
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,
)
if err != nil {
glog.Errorf(ctx, "上传文件失败: %v", 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",
}
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
}

View File

@@ -5,6 +5,12 @@ import (
"github.com/gogf/gf/v2/net/ghttp"
)
type DownloadToFileReq struct {
g.Meta `path:"/downloadToFile" method:"post" tags:"存储管理" summary:"下载文件到本地" dc:"下载文件到本地"`
FileURL string `json:"fileURL" dc:"文件URL"`
LocalPath string `json:"localPath" dc:"本地路径"`
}
// UploadFileReq 上传文件请求
type UploadFileReq struct {
g.Meta `path:"/uploadFile" method:"post" tags:"存储管理" summary:"上传文件" dc:"上传文件"`
@@ -25,3 +31,11 @@ type UploadFileRes struct {
FileFormat string `json:"fileFormat" dc:"文件格式"`
FileAddressPrefix string `json:"fileAddressPrefix"`
}
// UploadFileBytesReq 上传文件请求(字节流)
type UploadFileBytesReq struct {
g.Meta `path:"/uploadFileBytes" method:"post" tags:"存储管理" summary:"上传文件(字节流)" dc:"上传文件(字节流)"`
FileName string `json:"fileName" dc:"文件名"`
FileBytes []byte `json:"fileBytes" dc:"文件字节流"`
FileStoreURL string `json:"fileStoreURL" dc:"文件存储的URL"`
}

View File

@@ -23,6 +23,10 @@ type file struct{}
// File 存储文件服务
var File = new(file)
func (f *file) DownloadToFile(ctx context.Context, req *dto.DownloadToFileReq) (err error) {
return minio.DownloadToFile(ctx, req.FileURL, req.LocalPath)
}
func (f *file) UploadFile(ctx context.Context, req *dto.UploadFileReq) (res *dto.UploadFileRes, err error) {
fileSize := gconv.Int(req.File.Size)
totalFileSize := 0
@@ -99,7 +103,8 @@ func (f *file) UploadFile(ctx context.Context, req *dto.UploadFileReq) (res *dto
return nil, gerror.New("存储服务内存不足")
}
// 上传图片
fileURL, fileName, fileFormat, err := minio.UploadFile(ctx, req.File)
var fileURL, fileName, fileFormat string
fileURL, fileName, fileFormat, err = minio.UploadFile(ctx, req.File)
if err != nil {
glog.Errorf(ctx, "上传图片失败: %v", err)
return nil, err
@@ -127,3 +132,40 @@ func (f *file) UploadFile(ctx context.Context, req *dto.UploadFileReq) (res *dto
res.FileAddressPrefix = url
return
}
// UploadFileBytes 上传文件(字节流)
func (f *file) UploadFileBytes(ctx context.Context, req *dto.UploadFileBytesReq) (res *dto.UploadFileRes, err error) {
// 获取用户信息
user, err := utils.GetUserInfo(ctx)
if err != nil {
glog.Errorf(ctx, "获取用户信息失败: %v", err)
return
}
tenantId := user.TenantId
// 上传到 MinIO
fileURL, fileFormat, err := minio.UploadFileBytes(ctx, req.FileName, req.FileBytes, req.FileStoreURL)
if err != nil {
glog.Errorf(ctx, "上传文件失败: %v", err)
return nil, err
}
// 插入数据库记录
ossEntity := &dto.UploadFile{
TenantId: tenantId,
FileURL: fileURL,
FileSize: len(req.FileBytes),
}
if _, err = dao.File.Insert(ctx, ossEntity); err != nil {
return nil, err
}
res = &dto.UploadFileRes{
FileURL: fileURL,
FileSize: len(req.FileBytes),
FileName: req.FileName,
FileFormat: fileFormat,
}
res.FileAddressPrefix, _ = utils.GetFileAddressPrefix(ctx)
return
}