// Package upload 提供平台总后台的受控文件上传服务。 package upload import ( "io" "net/http" "os" "path/filepath" "strings" "time" "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/infra" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/gin-gonic/gin" ) const maxUploadSize int64 = 10 << 20 var allowedExtensions = map[string]struct{}{ ".jpg": {}, ".jpeg": {}, ".png": {}, ".webp": {}, ".pdf": {}, } // 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, maxUploadSize) fileHeader, err := ctx.FormFile("file") if err != nil || fileHeader == nil || fileHeader.Size <= 0 || fileHeader.Size > maxUploadSize { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } extension := strings.ToLower(filepath.Ext(fileHeader.Filename)) if _, allowed := allowedExtensions[extension]; !allowed { 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, }) } // uploadRoot 返回本地 Mock 存储根目录;生产环境可通过环境变量映射到受控挂载目录。 func uploadRoot() string { if directory := strings.TrimSpace(os.Getenv("HEQI_UPLOAD_DIR")); directory != "" { return directory } return filepath.Join("runtime", "uploads") }