97 lines
3.2 KiB
Go
97 lines
3.2 KiB
Go
// Package upload 提供平台总后台的受控文件上传服务。
|
|
package upload
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.apinb.com/bsm-sdk/core/errcode"
|
|
"git.apinb.com/bsm-sdk/core/infra"
|
|
sdkmiddleware "git.apinb.com/bsm-sdk/core/middleware"
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const maxUploadSize int64 = 10 << 20
|
|
|
|
var allowedExtensions = map[string]bool{
|
|
".jpg": false, ".jpeg": false, ".png": false, ".webp": false, ".pdf": false,
|
|
".mp4": true, ".mov": true,
|
|
}
|
|
|
|
// UploadFileReply 是文件上传完成后返回的受控资源标识。
|
|
type UploadFileReply struct {
|
|
URI string `json:"uri"` // 资源访问标识,后续可由对象存储适配层解析
|
|
OriginalName string `json:"original_name"` // 原始文件名,仅用于展示
|
|
ContentType string `json:"content_type"` // 客户端声明的媒体类型
|
|
Size int64 `json:"size"` // 文件字节数
|
|
}
|
|
|
|
// UploadFile 将允许类型的文件保存至本地 Mock 存储,不直接暴露绝对磁盘路径。
|
|
func UploadFile(ctx *gin.Context) {
|
|
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, config.Spec.Global.UploadVideoMaxSize)
|
|
fileHeader, err := ctx.FormFile("file")
|
|
if err != nil || fileHeader == nil || fileHeader.Size <= 0 {
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
return
|
|
}
|
|
|
|
extension := strings.ToLower(filepath.Ext(fileHeader.Filename))
|
|
isVideo, allowed := allowedExtensions[extension]
|
|
if !allowed || (!isVideo && fileHeader.Size > maxUploadSize) ||
|
|
(isVideo && fileHeader.Size > config.Spec.Global.UploadVideoMaxSize) {
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
return
|
|
}
|
|
|
|
file, err := fileHeader.Open()
|
|
if err != nil {
|
|
infra.Response.Error(ctx, err)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
datePath := time.Now().Format("2006/01/02")
|
|
filename := models.NewIdentity() + extension
|
|
directory := filepath.Join(uploadRoot(), filepath.FromSlash(datePath))
|
|
if err := os.MkdirAll(directory, 0o750); err != nil {
|
|
infra.Response.Error(ctx, err)
|
|
return
|
|
}
|
|
|
|
target, err := os.OpenFile(filepath.Join(directory, filename), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o640)
|
|
if err != nil {
|
|
infra.Response.Error(ctx, err)
|
|
return
|
|
}
|
|
defer target.Close()
|
|
if _, err := io.Copy(target, file); err != nil {
|
|
infra.Response.Error(ctx, err)
|
|
return
|
|
}
|
|
|
|
infra.Response.Success(ctx, UploadFileReply{
|
|
URI: "/uploads/" + datePath + "/" + filename,
|
|
OriginalName: fileHeader.Filename,
|
|
ContentType: fileHeader.Header.Get("Content-Type"),
|
|
Size: fileHeader.Size,
|
|
})
|
|
if claims, err := sdkmiddleware.ParseAuth(ctx); err == nil {
|
|
log.Printf("upload client=%s account=%s type=%s size=%d uri=%s", claims.Client, claims.Identity, fileHeader.Header.Get("Content-Type"), fileHeader.Size, "/uploads/"+datePath+"/"+filename)
|
|
}
|
|
}
|
|
|
|
// uploadRoot 返回本地 Mock 存储根目录;生产环境可通过环境变量映射到受控挂载目录。
|
|
func uploadRoot() string {
|
|
if directory := strings.TrimSpace(os.Getenv("HEQI_UPLOAD_DIR")); directory != "" {
|
|
return directory
|
|
}
|
|
return filepath.Join("runtime", "uploads")
|
|
}
|