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

225 lines
6.0 KiB
Go
Raw 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 service
import (
"context"
"fmt"
"io"
"net/url"
"oss/consts"
"oss/dao"
"oss/minio"
"oss/model/dto"
"oss/model/entity"
"time"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/glog"
"gitea.redpowerfuture.com/red-future/common/utils"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
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)
}
// DownloadToBrowser 下载文件到浏览器
func (f *file) DownloadToBrowser(ctx context.Context, req *dto.DownloadToBrowserReq) (err error) {
// 拿到流,而不是全量字节数组
reader, fileName, contentType, fileSize, err := minio.DownloadToBrowser(ctx, req.FileURL)
if err != nil {
return err
}
// 重要:确保流最后关闭
if closer, ok := reader.(io.Closer); ok {
defer closer.Close()
}
r := ghttp.RequestFromCtx(ctx)
resp := r.Response
resp.Header().Set("Content-Type", contentType)
resp.Header().Set("Content-Disposition", fmt.Sprintf(
`attachment; filename="%s"; filename*=UTF-8''%s`,
fileName, url.PathEscape(fileName),
))
resp.Header().Set("Content-Length", fmt.Sprintf("%d", fileSize))
resp.Header().Del("Content-Encoding")
resp.Header().Del("Transfer-Encoding")
data, err := io.ReadAll(reader)
if err != nil {
return err
}
resp.WriteExit(data)
return
}
func (f *file) DeleteFile(ctx context.Context, req *dto.DeleteFileReq) (err error) {
return minio.DeleteFile(ctx, req.FileURL)
}
func (f *file) UploadFile(ctx context.Context, req *dto.UploadFileReq) (res *dto.UploadFileRes, err error) {
fileReq := new(dto.UploadFileReq)
if !g.IsEmpty(req.File) {
fileReq.Files = &ghttp.UploadFiles{req.File}
files, err := f.UploadFiles(ctx, fileReq)
if err != nil {
return nil, err
}
res = files.Items[0]
} else if !g.IsEmpty(req.Files) {
fileReq.Files = req.Files
res, err = f.UploadFiles(ctx, fileReq)
} else {
return nil, gerror.New("上传内容不能为空")
}
return
}
// UploadFiles 上传多个文件(新接口)
func (f *file) UploadFiles(ctx context.Context, req *dto.UploadFileReq) (res *dto.UploadFileRes, err error) {
var files = *req.Files
totalAdd := 0
for _, file := range files {
if file == nil {
continue
}
totalAdd += gconv.Int(file.Size)
}
tenantId, err := f.reserveTenantOssSize(ctx, totalAdd)
if err != nil {
return nil, err
}
prefix, _ := utils.GetFileAddressPrefix(ctx)
items := make([]*dto.UploadFileRes, 0, len(files))
for _, file := range files {
if file == nil {
continue
}
fileURL, fileName, fileFormat, e := minio.UploadFile(ctx, file)
if e != nil {
glog.Errorf(ctx, "上传文件失败: %v", e)
return nil, e
}
ossEntity := &dto.UploadFile{
TenantId: tenantId,
FileURL: fileURL,
FileSize: gconv.Int(file.Size),
}
if _, e = dao.File.Insert(ctx, ossEntity); e != nil {
return nil, e
}
items = append(items, &dto.UploadFileRes{
FileURL: fileURL,
FileSize: gconv.Int(file.Size),
FileName: fileName,
FileFormat: fileFormat,
FileAddressPrefix: prefix,
})
}
fileRes := &dto.UploadFileRes{
FileAddressPrefix: prefix,
Items: items,
}
return fileRes, nil
}
func (f *file) reserveTenantOssSize(ctx context.Context, addSize int) (tenantId uint64, err error) {
if addSize < 0 {
err = gerror.New("addSize 不能为负数")
return
}
// 获取租户id
user, e := utils.GetUserInfo(ctx)
if e != nil {
err = e
glog.Errorf(ctx, "获取用户信息失败: %v", err)
return
}
tenantId = user.TenantId
// 获取redis-租户存储容量总数key
tenantOssTotalKey := fmt.Sprintf(consts.TenantOssTotalKey, gconv.String(user.TenantId))
// 获取redis-租户存储-锁key
fileLockKey := fmt.Sprintf(consts.FileLockKey, gconv.String(user.TenantId))
success, e := utils.Lock(ctx, fileLockKey, gconv.Int64(time.Minute*1), func(ctx context.Context) error {
// 获取redis-租户存储容量总数
get, e := g.Redis().Get(ctx, tenantOssTotalKey)
if e != nil {
glog.Errorf(ctx, "获取redis-租户存储容量总数失败: %v", e)
return e
}
tenantOssTotalEntity := &entity.TenantOssTotal{}
if g.IsEmpty(get) {
//查询数据库-获取租户存储容量总数
getByTenantIdReq := &dto.GetByTenantIdReq{
TenantId: user.TenantId,
}
tenantOssTotalRes, e := dao.TenantOssTotal.GetOneByTenantId(ctx, getByTenantIdReq)
if e != nil {
glog.Errorf(ctx, "查询数据库-获取租户存储容量总数失败: %v", e)
return e
}
if tenantOssTotalRes == nil || g.IsEmpty(tenantOssTotalRes.Id) {
// 数据库中没有该租户的记录,创建默认配置
tenantOssTotalEntity.TenantId = user.TenantId
tenantOssTotalEntity.UsedOssSize = 0
tenantOssTotalEntity.TotalOssSize = g.Cfg().MustGet(ctx, "oss.capacitySize").Int() * 1024 * 1024
} else {
tenantOssTotalEntity = tenantOssTotalRes
}
} else {
// 反序列化-redis获取租户存储容量总数
if e = gconv.Struct(get, tenantOssTotalEntity); e != nil {
glog.Errorf(ctx, "反序列化-redis获取租户存储容量总数失败: %v", e)
return e
}
}
tenantId = tenantOssTotalEntity.TenantId
usedAfter := tenantOssTotalEntity.UsedOssSize + addSize
total := tenantOssTotalEntity.TotalOssSize
if usedAfter > total {
return gerror.New("存储服务内存不足")
}
// 设置redis-租户存储容量总数超时时间10分钟
tenantOssTotalKeyMap := dto.UpdateUsedOssReq{
TenantId: tenantId,
UsedOssSize: usedAfter,
TotalOssSize: total,
Creator: user.UserName,
Updater: user.UserName,
}
if e = g.Redis().SetEX(ctx, tenantOssTotalKey, tenantOssTotalKeyMap, gconv.Int64(time.Minute*10)); e != nil {
glog.Errorf(ctx, "修改redis-租户存储容量总数失败: %v", e)
return e
}
return nil
})
if e != nil {
err = e
return
}
if !success {
err = gerror.New("存储服务内存不足")
return
}
return
}