fix(upload): 分片用 multipart 字段 chunk、路由顺序与串行上传

- 前端 FormData+chunk,避免 raw body 被中间层断连
- Gin 分片路由置于 POST .../assets 之前
- 分片并发降为 1

Made-with: Cursor
This commit is contained in:
whm
2026-04-14 09:30:09 +08:00
parent cce3d158d5
commit 65574e3762
4 changed files with 43 additions and 8 deletions

View File

@@ -215,7 +215,7 @@ func MultipartUploadStatus(c *gin.Context) {
})
}
// PutMultipartChunk 上传单个分片(二进制 body长度须与分片大小一致路由同时注册 POST 与 PUT,建议客户端用 POST
// PutMultipartChunk 上传单个分片。支持 multipart 字段 chunk推荐或 application/octet-stream 原始 body路由同时注册 POST 与 PUT。
func PutMultipartChunk(c *gin.Context) {
siteID := c.Param("site_id")
uploadID := c.Param("upload_id")
@@ -252,7 +252,38 @@ func PutMultipartChunk(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建临时文件失败"})
return
}
n, err := io.Copy(f, io.LimitReader(c.Request.Body, expected+1))
ct := strings.ToLower(c.GetHeader("Content-Type"))
var src io.Reader
if strings.HasPrefix(ct, "multipart/form-data") {
// 与整文件上传一致走 multipart避免部分网关对 raw POST body 断连
fh, err := c.FormFile("chunk")
if err != nil {
_ = f.Close()
_ = os.Remove(tmp)
c.JSON(http.StatusBadRequest, gin.H{"error": "请使用表单字段 chunk 上传分片"})
return
}
if fh.Size > 0 && fh.Size != expected {
_ = f.Close()
_ = os.Remove(tmp)
c.JSON(http.StatusBadRequest, gin.H{"error": "分片大小不符"})
return
}
part, err := fh.Open()
if err != nil {
_ = f.Close()
_ = os.Remove(tmp)
c.JSON(http.StatusBadRequest, gin.H{"error": "打开分片失败"})
return
}
defer part.Close()
src = part
} else {
src = c.Request.Body
}
n, err := io.Copy(f, io.LimitReader(src, expected+1))
_ = f.Close()
if err != nil {
_ = os.Remove(tmp)

View File

@@ -171,14 +171,14 @@ func main() {
admin.PUT("/sites/:site_id/homepage", handlers.RequirePermission(models.PermHomepageEdit), handlers.UpdateHomepage)
admin.GET("/sites/:site_id/assets/downloadable", handlers.RequirePermission(models.PermHomepageEdit), handlers.ListDownloadableAssets)
admin.GET("/sites/:site_id/assets", handlers.RequirePermission(models.PermSiteManage), handlers.ListSiteAssets)
admin.POST("/sites/:site_id/assets", handlers.RequirePermission(models.PermModuleUpload), handlers.UploadSiteAsset)
// 分片路由须在 POST .../assets 整文件上传之前注册,避免被更泛的路由误匹配
admin.POST("/sites/:site_id/assets/init-multipart", handlers.RequirePermission(models.PermModuleUpload), handlers.InitMultipartUpload)
admin.GET("/sites/:site_id/assets/multipart/:upload_id/status", handlers.RequirePermission(models.PermModuleUpload), handlers.MultipartUploadStatus)
// 分片用 POST部分反向代理对 PUT + 大 body 会断连,浏览器表现为 Network Error
admin.POST("/sites/:site_id/assets/multipart/:upload_id/chunk/:chunk_index", handlers.RequirePermission(models.PermModuleUpload), handlers.PutMultipartChunk)
admin.PUT("/sites/:site_id/assets/multipart/:upload_id/chunk/:chunk_index", handlers.RequirePermission(models.PermModuleUpload), handlers.PutMultipartChunk)
admin.POST("/sites/:site_id/assets/multipart/:upload_id/complete", handlers.RequirePermission(models.PermModuleUpload), handlers.CompleteMultipartUpload)
admin.DELETE("/sites/:site_id/assets/multipart/:upload_id", handlers.RequirePermission(models.PermModuleUpload), handlers.AbortMultipartUpload)
admin.POST("/sites/:site_id/assets", handlers.RequirePermission(models.PermModuleUpload), handlers.UploadSiteAsset)
admin.POST("/sites/:site_id/folders", handlers.RequirePermission(models.PermModuleUpload), handlers.CreateSiteFolder)
admin.DELETE("/sites/:site_id/assets/:asset_id", handlers.RequirePermission(models.PermSiteManage), handlers.DeleteSiteAsset)
admin.GET("/sites", handlers.RequirePermission(models.PermSiteManage), handlers.GetSites)