package handlers import ( "errors" "net/http" "os" "strconv" "github.com/gin-gonic/gin" "booklib/internal/ports" "booklib/internal/upload" ) // 分片上传:HTTP 层只做参数绑定与错误映射,域逻辑(指纹续传/分片落盘/拼接/清扫) // 全部在 internal/upload;会话清扫由 scanner ticker 接管(B16),不在请求路径。 // maxChunkBytes 与 upload 包内常量同值,作为 PutPart 的防御性读取上限。 const maxChunkBytes = 32 << 20 func (h *H) UploadInit(c *gin.Context) { id, ok := idParam(c) if !ok { return } lib, ok := h.getLib(c, id) if !ok { return } if _, ok := h.libRoot(c, lib); !ok { return } var req struct { Name string `json:"name"` Size int64 `json:"size"` ChunkSize int64 `json:"chunkSize"` } if c.ShouldBindJSON(&req) != nil { err(c, http.StatusBadRequest, "bad_request", "name, size required") return } uid, e := h.up.Init(c, lib.ID, req.Name, req.Size, req.ChunkSize) if e != nil { h.mapUploadErr(c, e) return } c.JSON(http.StatusOK, gin.H{"uploadId": uid}) } func (h *H) UploadStatus(c *gin.Context) { recv, e := h.up.Status(c, c.Param("uid")) if e != nil { h.mapUploadErr(c, e) return } c.JSON(http.StatusOK, gin.H{"received": recv}) } func (h *H) UploadPart(c *gin.Context) { uid := c.Param("uid") idx, e := strconv.ParseInt(c.Param("index"), 10, 64) if e != nil { err(c, http.StatusBadRequest, "bad_request", "bad part index") return } // maxSize 防御性上限:分片声明大小由会话 meta 决定,这里再垫一层 32MB 全局上限 e = h.up.PutPart(c, uid, idx, c.Request.Body, maxChunkBytes) if e != nil { h.mapUploadErr(c, e) return } c.JSON(http.StatusAccepted, gin.H{"accepted": true}) } func (h *H) UploadComplete(c *gin.Context) { uid := c.Param("uid") libID, e := h.up.LibraryID(c, uid) if e != nil { h.mapUploadErr(c, e) return } lib, e := h.libs.GetLibrary(c, libID) if e != nil { dbErr(c, e) return } root, ok := h.libRoot(c, lib) if !ok { return } rel, e := h.up.Complete(c, uid, root) if e != nil { h.mapUploadErr(c, e) return } c.JSON(http.StatusAccepted, gin.H{"accepted": true, "path": rel}) } // mapUploadErr 把 upload 包的 sentinel 错误映射为原契约的 status/code/message。 func (h *H) mapUploadErr(c *gin.Context, e error) { switch { case errors.Is(e, ports.ErrTooLarge): err(c, http.StatusRequestEntityTooLarge, "too_large", "file exceeds upload limit of "+strconv.FormatInt(h.cfg.UploadMaxMB, 10)+"MB") case errors.Is(e, upload.ErrBadName): err(c, http.StatusBadRequest, "bad_request", "bad name") case errors.Is(e, upload.ErrBadFormat): err(c, http.StatusBadRequest, "bad_format", "extension must be cbz/pdf/epub/txt/md") case errors.Is(e, upload.ErrBadSize): err(c, http.StatusBadRequest, "bad_request", "bad size") case errors.Is(e, upload.ErrBadChunk): err(c, http.StatusBadRequest, "bad_request", "chunkSize must be <= 33554432") case errors.Is(e, upload.ErrBadUploadID): err(c, http.StatusBadRequest, "bad_request", "bad upload id") case errors.Is(e, upload.ErrBadIndex): err(c, http.StatusBadRequest, "bad_request", "bad part index") case errors.Is(e, upload.ErrPartTooBig): err(c, http.StatusRequestEntityTooLarge, "too_large", "part exceeds declared size") case errors.Is(e, upload.ErrPartSizeMismatch): err(c, http.StatusRequestEntityTooLarge, "too_large", "part size mismatch") case errors.Is(e, ports.ErrNotFound): err(c, http.StatusNotFound, "not_found", "no such upload") case errors.Is(e, upload.ErrCorrupt): err(c, http.StatusInternalServerError, "internal", "corrupt session") case errors.Is(e, ports.ErrIncomplete): err(c, http.StatusBadRequest, "bad_request", "upload incomplete; missing or corrupt parts, re-upload them") case errors.Is(e, ports.ErrSizeMismatch): err(c, http.StatusBadRequest, "bad_request", "total size mismatch") case errors.Is(e, os.ErrInvalid), errors.Is(e, os.ErrExist): err(c, http.StatusForbidden, "forbidden", e.Error()) case errors.Is(e, os.ErrPermission), errors.Is(e, os.ErrClosed): err(c, http.StatusInternalServerError, "internal", "io error") default: var oe *upload.OpError if errors.As(e, &oe) { err(c, http.StatusInternalServerError, "internal", oe.Op) return } err(c, http.StatusInternalServerError, "internal", "upload failed") } }