feat: 新增统一文件模块,统一文件操作

This commit is contained in:
meilin.huang
2024-10-21 22:27:42 +08:00
parent 6343173cf8
commit ea3c70a8a8
71 changed files with 1642 additions and 1216 deletions

View File

@@ -0,0 +1,53 @@
package api
import (
"mayfly-go/internal/file/api/vo"
"mayfly-go/internal/file/application"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"strings"
)
type File struct {
FileApp application.File `inject:""`
}
func (f *File) GetFileByKeys(rc *req.Ctx) {
keysStr := rc.PathParam("keys")
biz.NotEmpty(keysStr, "keys不能为空")
var files []vo.SimpleFile
err := f.FileApp.ListByCondToAny(model.NewCond().In("file_key", strings.Split(keysStr, ",")), &files)
biz.ErrIsNil(err)
rc.ResData = files
}
func (f *File) GetFileContent(rc *req.Ctx) {
key := rc.PathParam("key")
biz.NotEmpty(key, "key不能为空")
filename, reader, err := f.FileApp.GetReader(rc.MetaCtx, key)
if err != nil {
rc.GetWriter().Write([]byte(err.Error()))
return
}
defer reader.Close()
rc.Download(reader, filename)
}
func (f *File) Upload(rc *req.Ctx) {
multipart, err := rc.GetRequest().MultipartReader()
biz.ErrIsNilAppendErr(err, "读取文件失败: %s")
file, err := multipart.NextPart()
biz.ErrIsNilAppendErr(err, "读取文件失败: %s")
defer file.Close()
fileKey, err := f.FileApp.Upload(rc.MetaCtx, rc.Query("fileKey"), file.FileName(), file)
biz.ErrIsNil(err)
rc.ResData = fileKey
}
func (f *File) Remove(rc *req.Ctx) {
biz.ErrIsNil(f.FileApp.Remove(rc.MetaCtx, rc.PathParam("key")))
}

View File

@@ -0,0 +1,7 @@
package vo
type SimpleFile struct {
Filename string `json:"filename"`
FileKey string `json:"fileKey"`
Size int64 `json:"size"`
}

View File

@@ -0,0 +1,9 @@
package application
import (
"mayfly-go/pkg/ioc"
)
func InitIoc() {
ioc.Register(new(fileAppImpl), ioc.WithComponentName("FileApp"))
}

View File

@@ -0,0 +1,169 @@
package application
import (
"context"
"io"
"mayfly-go/internal/file/config"
"mayfly-go/internal/file/domain/entity"
"mayfly-go/internal/file/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/utils/writerx"
"os"
"path/filepath"
"time"
"github.com/may-fly/cast"
)
type File interface {
base.App[*entity.File]
// Upload 上传文件
//
// @param fileKey 文件key若存在则使用存在的文件key否则生成新的文件key。
//
// @param filename 文件名,带文件后缀
//
// @return fileKey 文件key
Upload(ctx context.Context, fileKey string, filename string, r io.Reader) (string, error)
// NewWriter 创建文件writer
//
// @param canEmptyFileKey 文件key若不为空则使用该文件key否则生成新的文件key。
//
// @param filename 文件名,带文件后缀
//
// @return fileKey 文件key
//
// @return writer 文件writer
//
// @return saveFunc 保存文件信息的回调函数 (必须要defer中调用才会入库保存该文件信息)
NewWriter(ctx context.Context, canEmptyFileKey string, filename string) (fileKey string, writer *writerx.CountingWriteCloser, saveFunc func() error, err error)
// GetReader 获取文件reader
//
// @return filename 文件名
//
// @return reader 文件reader
//
// @return err 错误
GetReader(ctx context.Context, fileKey string) (string, io.ReadCloser, error)
// Remove 删除文件
Remove(ctx context.Context, fileKey string) error
}
type fileAppImpl struct {
base.AppImpl[*entity.File, repository.File]
}
func (f *fileAppImpl) InjectFileRepo(repo repository.File) {
f.Repo = repo
}
func (f *fileAppImpl) Upload(ctx context.Context, fileKey string, filename string, r io.Reader) (string, error) {
fileKey, writer, saveFileFunc, err := f.NewWriter(ctx, fileKey, filename)
if err != nil {
return fileKey, err
}
defer saveFileFunc()
if _, err := io.Copy(writer, r); err != nil {
return fileKey, err
}
return fileKey, nil
}
func (f *fileAppImpl) NewWriter(ctx context.Context, canEmptyFileKey string, filename string) (fileKey string, writer *writerx.CountingWriteCloser, saveFunc func() error, err error) {
isNewFile := true
file := &entity.File{}
if canEmptyFileKey == "" {
canEmptyFileKey = stringx.RandUUID()
file.FileKey = canEmptyFileKey
} else {
file.FileKey = canEmptyFileKey
if err := f.GetByCond(file); err == nil {
isNewFile = false
}
}
file.Filename = filename
if !isNewFile {
// 先删除旧文件
f.remove(ctx, file)
}
// 生产新的文件名
newFilename := canEmptyFileKey + filepath.Ext(filename)
filepath, w, err := f.newWriter(newFilename)
if err != nil {
return "", nil, nil, err
}
file.Path = filepath
fileKey = canEmptyFileKey
writer = writerx.NewCountingWriteCloser(w)
// 创建回调函数
saveFunc = func() error {
// 获取已写入的字节数
file.Size = writer.BytesWritten()
writer.Close()
// 保存文件信息
return f.Save(ctx, file)
}
return fileKey, writer, saveFunc, nil
}
func (f *fileAppImpl) GetReader(ctx context.Context, fileKey string) (string, io.ReadCloser, error) {
file := &entity.File{FileKey: fileKey}
if err := f.GetByCond(file); err != nil {
return "", nil, errorx.NewBiz("文件不存在")
}
r, err := os.Open(filepath.Join(config.GetFileConfig().BasePath, file.Path))
return file.Filename, r, err
}
func (f *fileAppImpl) Remove(ctx context.Context, fileKey string) error {
file := &entity.File{FileKey: fileKey}
if err := f.GetByCond(file); err != nil {
return errorx.NewBiz("文件不存在")
}
f.DeleteById(ctx, file.Id)
return f.remove(ctx, file)
}
func (f *fileAppImpl) newWriter(filename string) (string, io.WriteCloser, error) {
now := time.Now()
filePath := filepath.Join(cast.ToString(now.Year()), cast.ToString(int(now.Month())), cast.ToString(now.Day()), cast.ToString(now.Hour()), filename)
fileAbsPath := filepath.Join(config.GetFileConfig().BasePath, filePath)
// 目录不存在则创建
fileDir := filepath.Dir(fileAbsPath)
if _, err := os.Stat(fileDir); os.IsNotExist(err) {
err = os.MkdirAll(fileDir, os.ModePerm)
if err != nil {
return "", nil, err
}
}
// 创建目标文件
out, err := os.OpenFile(fileAbsPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0766)
if err != nil {
return "", nil, err
}
return filePath, out, nil
}
func (f *fileAppImpl) remove(ctx context.Context, file *entity.File) error {
if err := os.Remove(filepath.Join(config.GetFileConfig().BasePath, file.Path)); err != nil {
logx.ErrorfContext(ctx, "删除旧文件[%s] 失败: %s", file.Path, err.Error())
return err
}
return nil
}

View File

@@ -0,0 +1,24 @@
package config
import (
sysapp "mayfly-go/internal/sys/application"
"github.com/may-fly/cast"
)
const (
ConfigKeyFile string = "FileConfig" // 文件配置key
)
type FileConfig struct {
BasePath string // 文件基础路径
}
func GetFileConfig() *FileConfig {
c := sysapp.GetConfigApp().GetConfig(ConfigKeyFile)
jm := c.GetJsonMap()
fc := new(FileConfig)
fc.BasePath = cast.ToStringD(jm["basePath"], "./file")
return fc
}

View File

@@ -0,0 +1,16 @@
package entity
import "mayfly-go/pkg/model"
type File struct {
model.Model
FileKey string `json:"fikeKey"` // 文件key
Filename string `json:"filename"` // 文件名
Path string `json:"path" ` // 文件路径
Size int64 `json:"size"`
}
func (a *File) TableName() string {
return "t_sys_file"
}

View File

@@ -0,0 +1,5 @@
package entity
type FileQuery struct {
Keys []string
}

View File

@@ -0,0 +1,10 @@
package repository
import (
"mayfly-go/internal/file/domain/entity"
"mayfly-go/pkg/base"
)
type File interface {
base.Repo[*entity.File]
}

View File

@@ -0,0 +1,15 @@
package persistence
import (
"mayfly-go/internal/file/domain/entity"
"mayfly-go/internal/file/domain/repository"
"mayfly-go/pkg/base"
)
type fileRepoImpl struct {
base.RepoImpl[*entity.File]
}
func newFileRepo() repository.File {
return &fileRepoImpl{base.RepoImpl[*entity.File]{M: new(entity.File)}}
}

View File

@@ -0,0 +1,9 @@
package persistence
import (
"mayfly-go/pkg/ioc"
)
func InitIoc() {
ioc.Register(newFileRepo(), ioc.WithComponentName("FileRepo"))
}

View File

@@ -0,0 +1,16 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/file/application"
"mayfly-go/internal/file/infrastructure/persistence"
"mayfly-go/internal/file/router"
)
func init() {
initialize.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
})
initialize.AddInitRouterFunc(router.Init)
}

View File

@@ -0,0 +1,29 @@
package router
import (
"mayfly-go/internal/file/api"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ioc"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
func InitFileRouter(router *gin.RouterGroup) {
file := router.Group("sys/files")
f := new(api.File)
biz.ErrIsNil(ioc.Inject(f))
reqs := [...]*req.Conf{
req.NewGet("/detail/:keys", f.GetFileByKeys).DontNeedToken(),
req.NewGet("/:key", f.GetFileContent).DontNeedToken().NoRes(),
req.NewPost("/upload", f.Upload).Log(req.NewLogSave("file-文件上传")),
req.NewDelete("/:key", f.Remove).Log(req.NewLogSave("file-文件删除")),
}
req.BatchSetGroup(file, reqs[:])
}

View File

@@ -0,0 +1,9 @@
package router
import (
"github.com/gin-gonic/gin"
)
func Init(router *gin.RouterGroup) {
InitFileRouter(router)
}