add chunked upload to bypass 100MB proxy body limit

Resumable /api/uploads flow (init -> append -> complete -> abort) so a
large recording uploads as many small slices (<= ASR_CHUNK_SIZE_MB, default
50MB) instead of one request a size-capping proxy (Cloudflare's 100MB) would
reject. The web console auto-routes files larger than one slice through the
chunked path with cross-slice progress and transient-error retry; smaller
files keep the single-shot upload.

- store: in-memory upload sessions + .part reassembly (offset writes,
  out-of-order/idempotent), reuse upload id as task id, orphan sweep on
  start, idle-session GC
- fix limitedReader rejecting bodies exactly at the cap (broke full chunks)
- fix drag-drop double-firing when a drop lands on the dropzone (its drop
  handler now stopPropagation so the window listener doesn't re-upload)
- tests: store + HTTP flow, plus a 120MB reassembly test (ASR_TEST_LARGE)
This commit is contained in:
root
2026-09-10 15:12:15 +08:00
parent ab6bf8b370
commit e5a2dc0a8d
11 changed files with 1144 additions and 35 deletions
+8
View File
@@ -64,6 +64,14 @@ func (s *Server) Router() http.Handler {
mux.HandleFunc("POST /api/tasks/{id}/skip", s.handleSkipTask)
mux.HandleFunc("GET /api/tasks/{id}/transcript", s.handleTranscript)
// Resumable chunked uploads (also authed). Lets a large file slip past
// size-capping proxies (e.g. Cloudflare's 100MB body limit) as many
// small slices, finalized into a task indistinguishable from /api/tasks.
mux.HandleFunc("POST /api/uploads", s.handleCreateUpload)
mux.HandleFunc("POST /api/uploads/{id}", s.handleUploadChunk)
mux.HandleFunc("POST /api/uploads/{id}/complete", s.handleCompleteUpload)
mux.HandleFunc("DELETE /api/uploads/{id}", s.handleAbortUpload)
// OpenAI-compatible synchronous transcription (also authed).
mux.HandleFunc("POST /v1/audio/transcriptions", s.handleOpenAITranscription)