mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-03 15:06:36 +08:00
refactor(api): extract repository layer and thin HTTP handlers
Introduce internal/repository for data access and cache-backed system config reads. Move business logic into logics.go across admin push, user, template, cache, system_config, and upload/handler packages. Remove Gin from internal/util by relocating request-scoped helpers to oauth/gin_context.go. Propagate request context for config lookups in user flows. Slim model entities and delete model-level DB/cache helpers. Wire handlers to logics/repository so targeted packages no longer call db.DB directly. Update admin router tests to use ErrorHandlerMiddleware.
This commit is contained in:
@@ -4,22 +4,17 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/upload/shared"
|
||||
uploadstats "github.com/Rain-kl/Wavelet/internal/apps/upload/stats"
|
||||
uploadstorage "github.com/Rain-kl/Wavelet/internal/apps/upload/storage"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
apputil "github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type listFilesRequest struct {
|
||||
@@ -69,31 +64,15 @@ func ListFiles(c *gin.Context) {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
query := db.DB(ctx).Model(&model.Upload{}).
|
||||
Where("status != ?", model.UploadStatusDeleted)
|
||||
|
||||
if req.UserID != 0 {
|
||||
query = query.Where("user_id = ?", req.UserID)
|
||||
}
|
||||
if req.Keyword != "" {
|
||||
query = query.Where("LOWER(file_name) LIKE ?", "%"+strings.ToLower(req.Keyword)+"%")
|
||||
}
|
||||
if req.Type != "" {
|
||||
query = query.Where("type = ?", req.Type)
|
||||
}
|
||||
if req.Extension != "" {
|
||||
query = query.Where("extension = ?", strings.ToLower(req.Extension))
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
response.AbortBadRequest(c, shared.ErrQueryFileCountFailed)
|
||||
return
|
||||
}
|
||||
|
||||
var items []model.Upload
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&items).Error; err != nil {
|
||||
total, items, err := listUploadFiles(ctx, repository.UploadListFilter{
|
||||
UserID: req.UserID,
|
||||
Keyword: req.Keyword,
|
||||
Type: req.Type,
|
||||
Extension: req.Extension,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
})
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, shared.ErrQueryFileListFailed)
|
||||
return
|
||||
}
|
||||
@@ -130,20 +109,14 @@ func DeleteFile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var upload model.Upload
|
||||
if err := db.DB(ctx).Where("id = ? AND status != ?", uploadID, model.UploadStatusDeleted).First(&upload).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if _, err := softDeleteUpload(ctx, uploadID); err != nil {
|
||||
if isRecordNotFound(err) {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
response.AbortBadRequest(c, shared.ErrQueryUploadRecordFailed)
|
||||
return
|
||||
}
|
||||
if err := db.DB(ctx).Model(&upload).Update("status", model.UploadStatusDeleted).Error; err != nil {
|
||||
response.AbortBadRequest(c, shared.ErrDeleteFileFailed)
|
||||
return
|
||||
}
|
||||
uploadstats.RecordUploadStatsRemove(ctx, &upload)
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
@@ -159,16 +132,12 @@ func DeleteFile(c *gin.Context) {
|
||||
// @Failure 500 {object} response.Any "内部错误"
|
||||
// @Router /api/v1/admin/uploads/types [get]
|
||||
func GetDistinctUploadTypes(c *gin.Context) {
|
||||
var dbTypes []string
|
||||
if err := db.DB(c.Request.Context()).Model(&model.Upload{}).
|
||||
Where("type IS NOT NULL AND type != ''").
|
||||
Distinct().
|
||||
Pluck("type", &dbTypes).Error; err != nil {
|
||||
types, err := listDistinctUploadTypes(c.Request.Context())
|
||||
if err != nil {
|
||||
response.AbortInternal(c, err.Error())
|
||||
return
|
||||
}
|
||||
sort.Strings(dbTypes)
|
||||
c.JSON(http.StatusOK, response.OK(dbTypes))
|
||||
c.JSON(http.StatusOK, response.OK(types))
|
||||
}
|
||||
|
||||
type listMyFilesRequest struct {
|
||||
@@ -201,7 +170,7 @@ type listMyFilesResponse struct {
|
||||
// @Failure 401 {object} response.Any "未登录"
|
||||
// @Router /api/v1/upload/my [get]
|
||||
func ListMyFiles(c *gin.Context) {
|
||||
currUser, _ := apputil.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req listMyFilesRequest
|
||||
@@ -216,28 +185,14 @@ func ListMyFiles(c *gin.Context) {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
query := db.DB(ctx).Model(&model.Upload{}).
|
||||
Where("user_id = ? AND status != ?", currUser.ID, model.UploadStatusDeleted)
|
||||
|
||||
if req.Keyword != "" {
|
||||
query = query.Where("LOWER(file_name) LIKE ?", "%"+strings.ToLower(req.Keyword)+"%")
|
||||
}
|
||||
if req.Type != "" {
|
||||
query = query.Where("type = ?", req.Type)
|
||||
}
|
||||
if req.Extension != "" {
|
||||
query = query.Where("extension = ?", strings.ToLower(req.Extension))
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
response.AbortBadRequest(c, shared.ErrQueryFileCountFailed)
|
||||
return
|
||||
}
|
||||
|
||||
var items []model.Upload
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&items).Error; err != nil {
|
||||
total, items, err := listMyUploadFiles(ctx, currUser.ID, repository.UploadListFilter{
|
||||
Keyword: req.Keyword,
|
||||
Type: req.Type,
|
||||
Extension: req.Extension,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
})
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, shared.ErrQueryFileListFailed)
|
||||
return
|
||||
}
|
||||
@@ -262,7 +217,7 @@ func ListMyFiles(c *gin.Context) {
|
||||
// @Failure 404 {object} response.Any "文件不存在"
|
||||
// @Router /api/v1/upload/{id} [delete]
|
||||
func DeleteMyFile(c *gin.Context) {
|
||||
currUser, _ := apputil.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
if uploadstorage.ReadOnly(ctx) {
|
||||
response.AbortConflict(c, shared.ErrStorageReadOnly)
|
||||
@@ -275,26 +230,18 @@ func DeleteMyFile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var upload model.Upload
|
||||
if err := db.DB(ctx).Where("id = ? AND status != ?", uploadID, model.UploadStatusDeleted).First(&upload).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if _, err := softDeleteOwnedUpload(ctx, currUser.ID, uploadID); err != nil {
|
||||
if isRecordNotFound(err) {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
response.AbortBadRequest(c, shared.ErrQueryUploadRecordFailed)
|
||||
return
|
||||
}
|
||||
|
||||
if upload.UserID != currUser.ID {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Model(&upload).Update("status", model.UploadStatusDeleted).Error; err != nil {
|
||||
if err == errUploadForbidden {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
response.AbortBadRequest(c, shared.ErrDeleteFileFailed)
|
||||
return
|
||||
}
|
||||
uploadstats.RecordUploadStatsRemove(ctx, &upload)
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
|
||||
@@ -317,7 +264,7 @@ type updateMyFileRequest struct {
|
||||
// @Failure 404 {object} response.Any "文件不存在"
|
||||
// @Router /api/v1/upload/{id} [put]
|
||||
func UpdateMyFile(c *gin.Context) {
|
||||
currUser, _ := apputil.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
if uploadstorage.ReadOnly(ctx) {
|
||||
response.AbortConflict(c, shared.ErrStorageReadOnly)
|
||||
@@ -336,34 +283,18 @@ func UpdateMyFile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var upload model.Upload
|
||||
if err := db.DB(ctx).Where("id = ? AND status != ?", uploadID, model.UploadStatusDeleted).First(&upload).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
upload, err := updateOwnedUpload(ctx, currUser.ID, uploadID, updateMyUploadInput(req))
|
||||
if err != nil {
|
||||
if isRecordNotFound(err) {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
response.AbortBadRequest(c, shared.ErrQueryUploadRecordFailed)
|
||||
return
|
||||
}
|
||||
|
||||
if upload.UserID != currUser.ID {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]any)
|
||||
if req.FileName != "" {
|
||||
updates["file_name"] = req.FileName
|
||||
}
|
||||
if req.AccessMode != nil {
|
||||
updates["access_mode"] = *req.AccessMode
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := db.DB(ctx).Model(&upload).Updates(updates).Error; err != nil {
|
||||
response.AbortBadRequest(c, "更新文件记录失败")
|
||||
if err == errUploadForbidden {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
response.AbortBadRequest(c, "更新文件记录失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.OK(upload))
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/upload/shared"
|
||||
uploadstats "github.com/Rain-kl/Wavelet/internal/apps/upload/stats"
|
||||
uploadstorage "github.com/Rain-kl/Wavelet/internal/apps/upload/storage"
|
||||
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||
"github.com/Rain-kl/Wavelet/internal/storage"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func listUploadFiles(ctx context.Context, filter repository.UploadListFilter) (int64, []model.Upload, error) {
|
||||
return repository.ListUploads(ctx, filter)
|
||||
}
|
||||
|
||||
func listMyUploadFiles(ctx context.Context, userID uint64, filter repository.UploadListFilter) (int64, []model.Upload, error) {
|
||||
filter.UserID = userID
|
||||
return repository.ListUploads(ctx, filter)
|
||||
}
|
||||
|
||||
func softDeleteUpload(ctx context.Context, uploadID uint64) (model.Upload, error) {
|
||||
upload, err := repository.GetActiveUploadByID(ctx, uploadID)
|
||||
if err != nil {
|
||||
return model.Upload{}, err
|
||||
}
|
||||
if err := repository.SoftDeleteUpload(ctx, &upload); err != nil {
|
||||
return model.Upload{}, err
|
||||
}
|
||||
uploadstats.RecordUploadStatsRemove(ctx, &upload)
|
||||
return upload, nil
|
||||
}
|
||||
|
||||
func softDeleteOwnedUpload(ctx context.Context, userID, uploadID uint64) (model.Upload, error) {
|
||||
upload, err := repository.GetActiveUploadByID(ctx, uploadID)
|
||||
if err != nil {
|
||||
return model.Upload{}, err
|
||||
}
|
||||
if upload.UserID != userID {
|
||||
return model.Upload{}, errUploadForbidden
|
||||
}
|
||||
if err := repository.SoftDeleteUpload(ctx, &upload); err != nil {
|
||||
return model.Upload{}, err
|
||||
}
|
||||
uploadstats.RecordUploadStatsRemove(ctx, &upload)
|
||||
return upload, nil
|
||||
}
|
||||
|
||||
func listDistinctUploadTypes(ctx context.Context) ([]string, error) {
|
||||
types, err := repository.ListDistinctUploadTypes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(types)
|
||||
return types, nil
|
||||
}
|
||||
|
||||
type updateMyUploadInput struct {
|
||||
FileName string
|
||||
AccessMode *int
|
||||
}
|
||||
|
||||
func updateOwnedUpload(ctx context.Context, userID, uploadID uint64, input updateMyUploadInput) (model.Upload, error) {
|
||||
upload, err := repository.GetActiveUploadByID(ctx, uploadID)
|
||||
if err != nil {
|
||||
return model.Upload{}, err
|
||||
}
|
||||
if upload.UserID != userID {
|
||||
return model.Upload{}, errUploadForbidden
|
||||
}
|
||||
|
||||
updates := make(map[string]any)
|
||||
if input.FileName != "" {
|
||||
updates["file_name"] = input.FileName
|
||||
}
|
||||
if input.AccessMode != nil {
|
||||
updates["access_mode"] = *input.AccessMode
|
||||
}
|
||||
if err := repository.UpdateUpload(ctx, &upload, updates); err != nil {
|
||||
return model.Upload{}, err
|
||||
}
|
||||
if name, ok := updates["file_name"].(string); ok {
|
||||
upload.FileName = name
|
||||
}
|
||||
if mode, ok := updates["access_mode"].(int); ok {
|
||||
upload.AccessMode = mode
|
||||
}
|
||||
return upload, nil
|
||||
}
|
||||
|
||||
func listUploadsForBatchDownload(ctx context.Context, ids []uint64) ([]model.Upload, error) {
|
||||
return repository.ListUploadsByIDs(ctx, ids)
|
||||
}
|
||||
|
||||
type instantUploadInput struct {
|
||||
UserID uint64
|
||||
FileHash string
|
||||
Size int64
|
||||
MimeType string
|
||||
Extension string
|
||||
OrigName string
|
||||
UploadType string
|
||||
AccessMode int
|
||||
}
|
||||
|
||||
func createInstantUpload(ctx context.Context, existing model.Upload, input instantUploadInput) (model.Upload, error) {
|
||||
newUpload := model.Upload{
|
||||
ID: idgen.NextUint64ID(),
|
||||
UserID: input.UserID,
|
||||
FileName: input.OrigName,
|
||||
FilePath: existing.FilePath,
|
||||
FileSize: input.Size,
|
||||
MimeType: input.MimeType,
|
||||
Extension: input.Extension,
|
||||
Hash: input.FileHash,
|
||||
StorageDriver: existing.StorageDriver,
|
||||
Type: input.UploadType,
|
||||
Status: model.UploadStatusUsed,
|
||||
AccessMode: input.AccessMode,
|
||||
Metadata: existing.Metadata,
|
||||
}
|
||||
if err := repository.CreateUpload(ctx, &newUpload); err != nil {
|
||||
return model.Upload{}, err
|
||||
}
|
||||
uploadstats.RecordUploadStatsAdd(ctx, &newUpload)
|
||||
logger.InfoF(ctx, "文件触发秒传成功! ID: %d, Path: %s", newUpload.ID, existing.FilePath)
|
||||
return newUpload, nil
|
||||
}
|
||||
|
||||
func findReusableUpload(ctx context.Context, hash string, size int64) (model.Upload, error) {
|
||||
return repository.FindReusableUploadByHash(ctx, hash, size)
|
||||
}
|
||||
|
||||
func saveNewUploadRecord(ctx context.Context, upload *model.Upload, storageDriver, filePath string) error {
|
||||
if err := repository.CreateUpload(ctx, upload); err != nil {
|
||||
backend, backendErr := storage.ForDriver(ctx, storage.Driver(storageDriver))
|
||||
if backendErr == nil {
|
||||
if deleteErr := backend.Delete(ctx, filePath); deleteErr != nil {
|
||||
logger.WarnF(ctx, "清理未写入数据库的上传对象失败: %v", deleteErr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
uploadstats.RecordUploadStatsAdd(ctx, upload)
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadUploadStats(ctx context.Context) ([]model.UploadStat, error) {
|
||||
return repository.ListUploadStats(ctx)
|
||||
}
|
||||
|
||||
var errUploadForbidden = errors.New("upload forbidden")
|
||||
|
||||
func storeUploadObject(ctx context.Context, subPath string, size int64, mimeType string, buf *bytes.Buffer, meta *model.UploadMetadata) (string, string, error) {
|
||||
if uploadstorage.ReadOnly(ctx) {
|
||||
return "", "", errors.New(shared.ErrStorageReadOnly)
|
||||
}
|
||||
driver, backend, err := storage.Active(ctx)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "初始化活动存储失败: %v", err)
|
||||
return "", "", errors.New(shared.ErrSaveFileFailed)
|
||||
}
|
||||
result, err := backend.Put(ctx, subPath, bytes.NewReader(buf.Bytes()), size, mimeType)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "写入 %s 存储失败: %v", driver, err)
|
||||
return "", "", errors.New(shared.ErrSaveFileFailed)
|
||||
}
|
||||
meta.Bucket = result.Bucket
|
||||
return string(driver), result.Key, nil
|
||||
}
|
||||
|
||||
func validateUploadAllowedExtension(ctx context.Context, ext string) string {
|
||||
sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeyUploadAllowedExtensions)
|
||||
if err != nil || sc.Value == "" {
|
||||
return ""
|
||||
}
|
||||
allowedExts := strings.Split(strings.ToLower(sc.Value), ",")
|
||||
for _, allowedExt := range allowedExts {
|
||||
if strings.TrimSpace(allowedExt) == ext {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return shared.ErrUnsupportedFormat
|
||||
}
|
||||
|
||||
func isRecordNotFound(err error) bool {
|
||||
return errors.Is(err, gorm.ErrRecordNotFound)
|
||||
}
|
||||
@@ -26,16 +26,12 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/upload/filesrv"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/upload/shared"
|
||||
uploadstats "github.com/Rain-kl/Wavelet/internal/apps/upload/stats"
|
||||
uploadstorage "github.com/Rain-kl/Wavelet/internal/apps/upload/storage"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/upload/util"
|
||||
"github.com/Rain-kl/Wavelet/internal/common"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/storage"
|
||||
apputil "github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -68,7 +64,7 @@ func UploadFile(c *gin.Context) {
|
||||
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, shared.MaxUploadSize)
|
||||
|
||||
currUser, _ := apputil.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
|
||||
header, err := c.FormFile("file")
|
||||
@@ -95,7 +91,7 @@ func UploadFile(c *gin.Context) {
|
||||
ext = "bin"
|
||||
}
|
||||
|
||||
if errMsg := validateUploadExtension(ctx, ext); errMsg != "" {
|
||||
if errMsg := validateUploadAllowedExtension(ctx, ext); errMsg != "" {
|
||||
response.AbortBadRequest(c, errMsg)
|
||||
return
|
||||
}
|
||||
@@ -142,9 +138,9 @@ func UploadFile(c *gin.Context) {
|
||||
id := idgen.NextUint64ID()
|
||||
subPath := fmt.Sprintf("uploads/%s/%d.%s", time.Now().Format("2006/01/02"), id, ext)
|
||||
|
||||
storageDriver, subPath, errMsg := storeUploadFile(ctx, subPath, size, mimeType, &buf, &meta)
|
||||
if errMsg != "" {
|
||||
response.AbortBadRequest(c, errMsg)
|
||||
storageDriver, subPath, err := storeUploadObject(ctx, subPath, size, mimeType, &buf, &meta)
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -164,8 +160,8 @@ func UploadFile(c *gin.Context) {
|
||||
Metadata: meta,
|
||||
}
|
||||
|
||||
if err := saveUploadRecord(ctx, &newUpload, storageDriver, subPath); err != "" {
|
||||
response.AbortBadRequest(c, err)
|
||||
if err := saveNewUploadRecord(ctx, &newUpload, storageDriver, subPath); err != nil {
|
||||
response.AbortBadRequest(c, shared.ErrSaveUploadRecordFailed)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -253,8 +249,8 @@ func BatchDownloadFiles(c *gin.Context) {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
var uploads []model.Upload
|
||||
if err := db.DB(ctx).Where("id IN ? AND status IN (?, ?)", ids, model.UploadStatusPending, model.UploadStatusUsed).Find(&uploads).Error; err != nil {
|
||||
uploads, err := listUploadsForBatchDownload(ctx, ids)
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, shared.ErrRetrieveUploadRecordsFailed)
|
||||
return
|
||||
}
|
||||
@@ -325,27 +321,8 @@ func resolveUploadAccessMode(c *gin.Context, uploadType string) (int, string) {
|
||||
return accessMode, ""
|
||||
}
|
||||
|
||||
func validateUploadExtension(ctx context.Context, ext string) string {
|
||||
var sc model.SystemConfig
|
||||
if err := sc.GetByKey(ctx, model.ConfigKeyUploadAllowedExtensions); err == nil && sc.Value != "" {
|
||||
allowedExts := strings.Split(strings.ToLower(sc.Value), ",")
|
||||
allowed := false
|
||||
for _, allowedExt := range allowedExts {
|
||||
if strings.TrimSpace(allowedExt) == ext {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return shared.ErrUnsupportedFormat
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func tryInstantUpload(ctx context.Context, c *gin.Context, currUser *model.User, fileHash string, size int64, mimeType, ext, origName string, accessMode int) (bool, error) {
|
||||
var existing model.Upload
|
||||
err := db.DB(ctx).Where("hash = ? AND file_size = ? AND status IN (?, ?)", fileHash, size, model.UploadStatusPending, model.UploadStatusUsed).First(&existing).Error
|
||||
existing, err := findReusableUpload(ctx, fileHash, size)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -354,52 +331,24 @@ func tryInstantUpload(ctx context.Context, c *gin.Context, currUser *model.User,
|
||||
return true, nil
|
||||
}
|
||||
|
||||
id := idgen.NextUint64ID()
|
||||
newUpload := model.Upload{
|
||||
ID: id,
|
||||
UserID: currUser.ID,
|
||||
FileName: origName,
|
||||
FilePath: existing.FilePath,
|
||||
FileSize: size,
|
||||
MimeType: mimeType,
|
||||
Extension: ext,
|
||||
Hash: fileHash,
|
||||
StorageDriver: existing.StorageDriver,
|
||||
Type: c.DefaultPostForm("type", "generic"),
|
||||
Status: model.UploadStatusUsed,
|
||||
AccessMode: accessMode,
|
||||
Metadata: existing.Metadata,
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Create(&newUpload).Error; err != nil {
|
||||
newUpload, err := createInstantUpload(ctx, existing, instantUploadInput{
|
||||
UserID: currUser.ID,
|
||||
FileHash: fileHash,
|
||||
Size: size,
|
||||
MimeType: mimeType,
|
||||
Extension: ext,
|
||||
OrigName: origName,
|
||||
UploadType: c.DefaultPostForm("type", "generic"),
|
||||
AccessMode: accessMode,
|
||||
})
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, shared.ErrSaveUploadRecordFailed)
|
||||
return true, err
|
||||
}
|
||||
uploadstats.RecordUploadStatsAdd(ctx, &newUpload)
|
||||
|
||||
logger.InfoF(ctx, "文件触发秒传成功! ID: %d, Path: %s", id, existing.FilePath)
|
||||
c.JSON(http.StatusOK, response.OK(newUpload))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func storeUploadFile(ctx context.Context, subPath string, size int64, mimeType string, buf *bytes.Buffer, meta *model.UploadMetadata) (string, string, string) {
|
||||
if uploadstorage.ReadOnly(ctx) {
|
||||
return "", "", shared.ErrStorageReadOnly
|
||||
}
|
||||
driver, backend, err := storage.Active(ctx)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "初始化活动存储失败: %v", err)
|
||||
return "", "", shared.ErrSaveFileFailed
|
||||
}
|
||||
result, err := backend.Put(ctx, subPath, bytes.NewReader(buf.Bytes()), size, mimeType)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "写入 %s 存储失败: %v", driver, err)
|
||||
return "", "", shared.ErrSaveFileFailed
|
||||
}
|
||||
meta.Bucket = result.Bucket
|
||||
return string(driver), result.Key, ""
|
||||
}
|
||||
|
||||
func parseUploadMetadata(c *gin.Context, mimeType string) (model.UploadMetadata, string) {
|
||||
var meta model.UploadMetadata
|
||||
metadataStr := c.DefaultPostForm("metadata", "")
|
||||
@@ -422,16 +371,3 @@ func detectMimeType(buf *bytes.Buffer, header *multipart.FileHeader, size int64)
|
||||
return mimeType
|
||||
}
|
||||
|
||||
func saveUploadRecord(ctx context.Context, upload *model.Upload, storageDriver, filePath string) string {
|
||||
if err := db.DB(ctx).Create(upload).Error; err != nil {
|
||||
backend, backendErr := storage.ForDriver(ctx, storage.Driver(storageDriver))
|
||||
if backendErr == nil {
|
||||
if deleteErr := backend.Delete(ctx, filePath); deleteErr != nil {
|
||||
logger.WarnF(ctx, "清理未写入数据库的上传对象失败: %v", deleteErr)
|
||||
}
|
||||
}
|
||||
return shared.ErrSaveUploadRecordFailed
|
||||
}
|
||||
uploadstats.RecordUploadStatsAdd(ctx, upload)
|
||||
return ""
|
||||
}
|
||||
@@ -21,13 +21,13 @@ import (
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/upload/shared"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
uploadstats "github.com/Rain-kl/Wavelet/internal/apps/upload/stats"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||
"github.com/Rain-kl/Wavelet/internal/storage"
|
||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ func setupTestRouter(authUser *model.User) *gin.Engine {
|
||||
|
||||
authMiddleware := func(c *gin.Context) {
|
||||
if authUser != nil {
|
||||
util.SetToContext(c, oauth.UserObjKey, authUser)
|
||||
oauth.SetToContext(c, oauth.UserObjKey, authUser)
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
@@ -297,8 +297,8 @@ func TestUploadFile(t *testing.T) {
|
||||
dbConn.Where("key = ?", model.ConfigKeyUploadAllowedExtensions).First(&sc)
|
||||
sc.Value = "jpg,png,webp,txt"
|
||||
dbConn.Save(&sc)
|
||||
_ = db.HSetJSON(context.Background(), model.SystemConfigRedisHashKey, sc.Key, &sc)
|
||||
model.ResetSystemConfigRAMCacheForTest()
|
||||
_ = db.HSetJSON(context.Background(), repository.SystemConfigRedisHashKey, sc.Key, &sc)
|
||||
repository.ResetSystemConfigRAMCacheForTest()
|
||||
|
||||
contentType, body := createMultipartRequest(t, "file", "doc.txt", []byte("hello world generic document file"), map[string]string{
|
||||
"type": "document",
|
||||
@@ -998,4 +998,3 @@ func TestUserUploadManagement(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/upload/shared"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -48,8 +47,8 @@ type fileStatsResponse struct {
|
||||
func GetFileStats(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var stats []model.UploadStat
|
||||
if err := db.DB(ctx).Find(&stats).Error; err != nil {
|
||||
stats, err := loadUploadStats(ctx)
|
||||
if err != nil {
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user