feat: add rustfs direct upload endpoint
Build docker and publish / build (20.15.1) (push) Failing after 8m36s
Build docker and publish / build (20.15.1) (push) Failing after 8m36s
This commit is contained in:
@@ -11,6 +11,20 @@ info (
|
|||||||
import "../types.api"
|
import "../types.api"
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
FileUploadRequest {
|
||||||
|
BizType string `form:"biz_type" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
FileUploadResponse {
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
FileUploadInitRequest {
|
FileUploadInitRequest {
|
||||||
BizType string `json:"biz_type" validate:"required"`
|
BizType string `json:"biz_type" validate:"required"`
|
||||||
FileName string `json:"file_name" validate:"required"`
|
FileName string `json:"file_name" validate:"required"`
|
||||||
@@ -48,6 +62,10 @@ type (
|
|||||||
middleware: AuthMiddleware,DeviceMiddleware
|
middleware: AuthMiddleware,DeviceMiddleware
|
||||||
)
|
)
|
||||||
service ppanel {
|
service ppanel {
|
||||||
|
@doc "Upload file to RustFS"
|
||||||
|
@handler FileUpload
|
||||||
|
post /upload (FileUploadRequest) returns (FileUploadResponse)
|
||||||
|
|
||||||
@doc "Init file upload"
|
@doc "Init file upload"
|
||||||
@handler FileUploadInit
|
@handler FileUploadInit
|
||||||
post /upload/init (FileUploadInitRequest) returns (FileUploadInitResponse)
|
post /upload/init (FileUploadInitRequest) returns (FileUploadInitResponse)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -870,6 +870,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
publicFileGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
publicFileGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||||
|
|
||||||
{
|
{
|
||||||
|
// Upload file to RustFS
|
||||||
|
publicFileGroupRouter.POST("/upload", publicFile.FileUploadHandler(serverCtx))
|
||||||
|
|
||||||
// Init file upload
|
// Init file upload
|
||||||
publicFileGroupRouter.POST("/upload/init", publicFile.FileUploadInitHandler(serverCtx))
|
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
|
||||||
|
}
|
||||||
@@ -762,6 +762,20 @@ type FamilySummary struct {
|
|||||||
UpdatedAt int64 `json:"updated_at"`
|
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 {
|
type FileUploadCompleteRequest struct {
|
||||||
FileId string `json:"file_id" validate:"required"`
|
FileId string `json:"file_id" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -27,6 +28,10 @@ type ObjectMeta struct {
|
|||||||
ContentLength int64
|
ContentLength int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PutObjectResult struct {
|
||||||
|
ETag string
|
||||||
|
}
|
||||||
|
|
||||||
type S3Store struct {
|
type S3Store struct {
|
||||||
cfg config.S3Config
|
cfg config.S3Config
|
||||||
client *s3.Client
|
client *s3.Client
|
||||||
@@ -96,6 +101,20 @@ func (s *S3Store) HeadObject(ctx context.Context, objectKey string) (*ObjectMeta
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *S3Store) PutObject(ctx context.Context, objectKey string, body io.Reader, size int64, contentType string) (*PutObjectResult, error) {
|
||||||
|
resp, err := s.client.PutObject(ctx, &s3.PutObjectInput{
|
||||||
|
Bucket: aws.String(s.cfg.Bucket),
|
||||||
|
Key: aws.String(objectKey),
|
||||||
|
Body: body,
|
||||||
|
ContentLength: aws.Int64(size),
|
||||||
|
ContentType: aws.String(contentType),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &PutObjectResult{ETag: strings.Trim(aws.ToString(resp.ETag), "\"")}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *S3Store) BuildObjectURL(objectKey string) string {
|
func (s *S3Store) BuildObjectURL(objectKey string) string {
|
||||||
if s.cfg.PublicBaseURL != "" {
|
if s.cfg.PublicBaseURL != "" {
|
||||||
return strings.TrimRight(s.cfg.PublicBaseURL, "/") + "/" + strings.TrimLeft(objectKey, "/")
|
return strings.TrimRight(s.cfg.PublicBaseURL, "/") + "/" + strings.TrimLeft(objectKey, "/")
|
||||||
|
|||||||
Reference in New Issue
Block a user