feat: upload

This commit is contained in:
Rogee
2025-01-15 10:43:16 +08:00
parent 9bfdf0e0ea
commit ab827715fb
16 changed files with 378 additions and 161 deletions

View File

@@ -1,29 +1,25 @@
package medias
import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"time"
"backend/app/http/storages"
"backend/app/http/tenants"
"backend/database/models/qvyun_v2/public/model"
"backend/pkg/storage"
"backend/providers/jwt"
"github.com/gofiber/fiber/v3"
log "github.com/sirupsen/logrus"
)
const (
uploadTempDir = "./temp/chunks" // 临时分片目录
uploadStorageDir = "./uploads" // 最终文件存储目录
)
// @provider
type Controller struct {
svc *Service
log *log.Entry `inject:"false"`
tenantSvc *tenants.Service
svc *Service
storageSvc *storages.Service
log *log.Entry `inject:"false"`
}
func (ctl *Controller) Prepare() error {
@@ -32,119 +28,55 @@ func (ctl *Controller) Prepare() error {
}
// Upload
// @Router /api/v1/medias/upload [post]
// @Router /api/v1/medias/:tenant/upload [post]
// @Bind tenantSlug path
// @Bind req body
// @Bind file file
func (ctl *Controller) Upload(ctx fiber.Ctx, file *multipart.FileHeader, req *UploadReq) (*UploadResp, error) {
// 使用MD5创建唯一的临时目录
tempDir := filepath.Join(uploadTempDir, req.FileMD5)
if err := os.MkdirAll(tempDir, 0o755); err != nil {
// @Bind claim local
func (ctl *Controller) Upload(ctx fiber.Ctx, claim *jwt.Claims, tenantSlug string, file *multipart.FileHeader, req *UploadReq) (*storage.UploadedFile, error) {
tenant, err := ctl.tenantSvc.GetTenantBySlug(ctx.Context(), tenantSlug)
if err != nil {
return nil, err
}
chunkPath := filepath.Join(tempDir, fmt.Sprintf("chunk_%d", req.ChunkNumber))
if err := ctx.SaveFile(file, chunkPath); err != nil {
defaultStorage, err := ctl.storageSvc.GetDefault(ctx.Context())
if err != nil {
return nil, err
}
// 如果是最后一个分片
if req.ChunkNumber == req.TotalChunks-1 {
// 生成唯一的文件存储路径
ext := filepath.Ext(req.FileName)
storageDir := filepath.Join(uploadStorageDir, time.Now().Format("2006/01/02"))
if err := os.MkdirAll(storageDir, 0o755); err != nil {
os.RemoveAll(tempDir)
return nil, err
}
finalPath := filepath.Join(storageDir, req.FileMD5+ext)
// 计算所有分片的实际大小总和
totalSize, err := calculateTotalSize(tempDir, req.TotalChunks)
if err != nil {
os.RemoveAll(tempDir)
return nil, fmt.Errorf("计算文件大小失败: %w", err)
}
// 合并文件
if err := combineChunks(tempDir, finalPath, req.TotalChunks); err != nil {
os.RemoveAll(tempDir)
return nil, fmt.Errorf("合并文件失败: %w", err)
}
// 验证MD5
calculatedMD5, err := calculateFileMD5(finalPath)
if err != nil || calculatedMD5 != req.FileMD5 {
os.RemoveAll(tempDir)
os.Remove(finalPath)
return nil, errors.New("文件MD5验证失败")
}
// 清理临时目录
os.RemoveAll(tempDir)
return &UploadResp{
Files: []UploadFile{
{
HashID: calculatedMD5,
Name: req.FileName,
Path: finalPath,
Size: totalSize,
MimeType: file.Header.Get("Content-Type"),
},
},
}, nil
}
return &UploadResp{}, nil
}
// 计算所有分片的实际大小总和
func calculateTotalSize(tempDir string, totalChunks int) (int64, error) {
var totalSize int64
for i := 0; i < totalChunks; i++ {
chunkPath := filepath.Join(tempDir, fmt.Sprintf("chunk_%d", i))
info, err := os.Stat(chunkPath)
if err != nil {
return 0, err
}
totalSize += info.Size()
}
return totalSize, nil
}
func combineChunks(tempDir, finalPath string, totalChunks int) error {
finalFile, err := os.Create(finalPath)
uploader, err := storage.NewUploader(req.FileName, req.ChunkNumber, req.TotalChunks, req.FileMD5)
if err != nil {
return err
}
defer finalFile.Close()
for i := 0; i < totalChunks; i++ {
chunkPath := fmt.Sprintf("%s/chunk_%d", tempDir, i)
chunk, err := os.ReadFile(chunkPath)
if err != nil {
return err
}
if _, err := finalFile.Write(chunk); err != nil {
return err
}
return nil, err
}
return nil
}
func calculateFileMD5(filePath string) (string, error) {
file, err := os.Open(filePath)
uploadedFile, err := uploader.Save(ctx, file)
if err != nil {
return "", err
}
defer file.Close()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
return nil, err
}
return hex.EncodeToString(hash.Sum(nil)), nil
if uploadedFile == nil {
return uploadedFile, nil
}
uploadedFile, err = storage.Build(defaultStorage).Save(ctx.Context(), uploadedFile)
if err != nil {
return nil, err
}
// save to db
_, err = ctl.svc.Create(ctx.Context(), &model.Medias{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
TenantID: tenant.ID,
UserID: claim.UserID,
StorageID: defaultStorage.ID,
Name: uploadedFile.Name,
UUID: uploadedFile.Hash,
MimeType: uploadedFile.MimeType,
Size: uploadedFile.Size,
Path: uploadedFile.Path,
})
uploadedFile.Preview = ""
return uploadedFile, err
}