feat: add rustfs direct upload endpoint
Build docker and publish / build (20.15.1) (push) Failing after 8m36s

This commit is contained in:
2026-05-14 23:33:50 -07:00
parent 3284cb45f0
commit f2033fd4b9
6 changed files with 187 additions and 2 deletions
@@ -0,0 +1,43 @@
package file
import (
"mime/multipart"
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/file"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Upload file to RustFS
func FileUploadHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.FileUploadRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
fileHeader, err := c.FormFile("file")
if err != nil {
result.ParamErrorResult(c, err)
return
}
fileReader, err := fileHeader.Open()
if err != nil {
result.HttpResult(c, nil, err)
return
}
defer func(file multipart.File) {
_ = file.Close()
}(fileReader)
l := file.NewFileUploadLogic(c.Request.Context(), svcCtx)
resp, err := l.FileUpload(&req, fileHeader, fileReader)
result.HttpResult(c, resp, err)
}
}
+3
View File
@@ -870,6 +870,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
publicFileGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
{
// Upload file to RustFS
publicFileGroupRouter.POST("/upload", publicFile.FileUploadHandler(serverCtx))
// Init file upload
publicFileGroupRouter.POST("/upload/init", publicFile.FileUploadInitHandler(serverCtx))
@@ -0,0 +1,88 @@
package file
import (
"bytes"
"context"
"io"
"mime/multipart"
"net/http"
"time"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type FileUploadLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Upload file to RustFS
func NewFileUploadLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FileUploadLogic {
return &FileUploadLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *FileUploadLogic) FileUpload(req *types.FileUploadRequest, fileHeader *multipart.FileHeader, file multipart.File) (resp *types.FileUploadResponse, err error) {
u, err := currentUserFromContext(l.ctx)
if err != nil {
return nil, err
}
if fileHeader == nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "file is required")
}
contentType, err := sniffContentType(fileHeader, file)
if err != nil {
return nil, err
}
if err := validateInitRequest(l.svcCtx, req.BizType, fileHeader.Filename, contentType, fileHeader.Size); err != nil {
return nil, err
}
now := time.Now()
fileID := buildFileID(u.Id, req.BizType, fileHeader.Filename)
objectKey := buildObjectKey(l.svcCtx.Config.S3.Prefix, u.Id, req.BizType, fileID, fileHeader.Filename, now)
putResult, err := l.svcCtx.S3Store.PutObject(l.ctx, objectKey, file, fileHeader.Size, contentType)
if err != nil {
l.Errorw("put object failed", logger.Field("error", err.Error()), logger.Field("user_id", u.Id), logger.Field("file_id", fileID))
return nil, err
}
return &types.FileUploadResponse{
FileId: fileID,
FileName: fileHeader.Filename,
ObjectKey: objectKey,
Size: fileHeader.Size,
ContentType: contentType,
Etag: putResult.ETag,
Status: fileUploadCompleteStatus,
}, nil
}
func sniffContentType(fileHeader *multipart.FileHeader, file multipart.File) (string, error) {
if headerType := fileHeader.Header.Get("Content-Type"); headerType != "" {
return headerType, nil
}
if seeker, ok := file.(io.ReadSeeker); ok {
buf := make([]byte, 512)
n, readErr := seeker.Read(buf)
if readErr != nil && readErr != io.EOF {
return "", readErr
}
if _, err := seeker.Seek(0, io.SeekStart); err != nil {
return "", err
}
return http.DetectContentType(bytes.TrimRight(buf[:n], "\x00")), nil
}
return "application/octet-stream", nil
}
+14
View File
@@ -762,6 +762,20 @@ type FamilySummary struct {
UpdatedAt int64 `json:"updated_at"`
}
type FileUploadRequest struct {
BizType string `form:"biz_type" validate:"required"`
}
type FileUploadResponse struct {
FileId string `json:"file_id"`
FileName string `json:"file_name"`
ObjectKey string `json:"object_key"`
Size int64 `json:"size"`
ContentType string `json:"content_type"`
Etag string `json:"etag"`
Status string `json:"status"`
}
type FileUploadCompleteRequest struct {
FileId string `json:"file_id" validate:"required"`
}