feat: 添加文件下载及字节流上传功能
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"oss/model/dto"
|
"oss/model/dto"
|
||||||
"oss/service"
|
"oss/service"
|
||||||
|
|
||||||
|
"gitea.com/red-future/common/beans"
|
||||||
)
|
)
|
||||||
|
|
||||||
type file struct{}
|
type file struct{}
|
||||||
@@ -14,7 +16,18 @@ var File = new(file)
|
|||||||
func init() {
|
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 上传文件
|
// UploadFile 上传文件
|
||||||
func (c *file) UploadFile(ctx context.Context, req *dto.UploadFileReq) (res *dto.UploadFileRes, err error) {
|
func (c *file) UploadFile(ctx context.Context, req *dto.UploadFileReq) (res *dto.UploadFileRes, err error) {
|
||||||
return service.File.UploadFile(ctx, req)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
140
minio/minio.go
140
minio/minio.go
@@ -1,7 +1,9 @@
|
|||||||
package minio
|
package minio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -54,29 +56,49 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadFile(ctx context.Context, fileHeader *ghttp.UploadFile) (imagesUrl string, fileName string, fileFormat string, err error) {
|
// ensureBucketAndObjectName 确保桶存在,并生成对象名
|
||||||
return uploadFile(ctx, fileHeader)
|
// 返回: bucketName, objectName, fileFormat, error
|
||||||
}
|
func ensureBucketAndObjectName(ctx context.Context, fileName string, fileStoreURL string) (string, string, string, error) {
|
||||||
|
|
||||||
func uploadFile(ctx context.Context, fileHeader *ghttp.UploadFile) (imagesUrl string, fileName string, fileFormat string, err error) {
|
|
||||||
bucketName, err := utils.GetBucketName(ctx)
|
bucketName, err := utils.GetBucketName(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf(ctx, "获取桶名称失败: %v", err)
|
glog.Errorf(ctx, "获取桶名称失败: %v", err)
|
||||||
return
|
return "", "", "", err
|
||||||
}
|
}
|
||||||
// 检查/创建桶
|
// 检查/创建桶
|
||||||
exists, err := minioClient.BucketExists(ctx, bucketName)
|
exists, err := minioClient.BucketExists(ctx, bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf(ctx, "检查桶是否存在失败: %v", err)
|
glog.Errorf(ctx, "检查桶是否存在失败: %v", err)
|
||||||
return
|
return "", "", "", err
|
||||||
}
|
}
|
||||||
if !exists {
|
if !exists {
|
||||||
if err = minioClient.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: minioCfg.Region}); err != nil {
|
if err = minioClient.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: minioCfg.Region}); err != nil {
|
||||||
glog.Errorf(ctx, "创建桶失败: %v", err)
|
glog.Errorf(ctx, "创建桶失败: %v", err)
|
||||||
return
|
return "", "", "", err
|
||||||
}
|
}
|
||||||
glog.Infof(ctx, "成功创建 MinIO 桶: %s", bucketName)
|
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)
|
// 打开文件,获取 io.Reader(*os.File 实现了 io.Reader)
|
||||||
file, err := fileHeader.Open()
|
file, err := fileHeader.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -97,17 +119,6 @@ func uploadFile(ctx context.Context, fileHeader *ghttp.UploadFile) (imagesUrl st
|
|||||||
glog.Errorf(ctx, "重置文件读取位置失败: %v", err)
|
glog.Errorf(ctx, "重置文件读取位置失败: %v", err)
|
||||||
return
|
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(
|
_, err = minioClient.PutObject(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -117,13 +128,98 @@ func uploadFile(ctx context.Context, fileHeader *ghttp.UploadFile) (imagesUrl st
|
|||||||
fileHeader.Size,
|
fileHeader.Size,
|
||||||
minio.PutObjectOptions{
|
minio.PutObjectOptions{
|
||||||
ContentType: contentType, // 关键:指定图片MIME类型,S3会根据此类型处理
|
ContentType: contentType, // 关键:指定图片MIME类型,S3会根据此类型处理
|
||||||
// 若需要图片可公开访问,添加如下配置(根据需求选择)
|
|
||||||
//ACL: minio.ACLPublicRead,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf(ctx, "上传图片失败: %v", err)
|
glog.Errorf(ctx, "上传图片失败: %v", err)
|
||||||
return
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import (
|
|||||||
"github.com/gogf/gf/v2/net/ghttp"
|
"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 上传文件请求
|
// UploadFileReq 上传文件请求
|
||||||
type UploadFileReq struct {
|
type UploadFileReq struct {
|
||||||
g.Meta `path:"/uploadFile" method:"post" tags:"存储管理" summary:"上传文件" dc:"上传文件"`
|
g.Meta `path:"/uploadFile" method:"post" tags:"存储管理" summary:"上传文件" dc:"上传文件"`
|
||||||
@@ -25,3 +31,11 @@ type UploadFileRes struct {
|
|||||||
FileFormat string `json:"fileFormat" dc:"文件格式"`
|
FileFormat string `json:"fileFormat" dc:"文件格式"`
|
||||||
FileAddressPrefix string `json:"fileAddressPrefix"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ type file struct{}
|
|||||||
// File 存储文件服务
|
// File 存储文件服务
|
||||||
var File = new(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) {
|
func (f *file) UploadFile(ctx context.Context, req *dto.UploadFileReq) (res *dto.UploadFileRes, err error) {
|
||||||
fileSize := gconv.Int(req.File.Size)
|
fileSize := gconv.Int(req.File.Size)
|
||||||
totalFileSize := 0
|
totalFileSize := 0
|
||||||
@@ -99,7 +103,8 @@ func (f *file) UploadFile(ctx context.Context, req *dto.UploadFileReq) (res *dto
|
|||||||
return nil, gerror.New("存储服务内存不足")
|
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 {
|
if err != nil {
|
||||||
glog.Errorf(ctx, "上传图片失败: %v", err)
|
glog.Errorf(ctx, "上传图片失败: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -127,3 +132,40 @@ func (f *file) UploadFile(ctx context.Context, req *dto.UploadFileReq) (res *dto
|
|||||||
res.FileAddressPrefix = url
|
res.FileAddressPrefix = url
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user