go
57 lines · 9 steps
Resumable chunked uploads in Gin
A Gin handler writes each byte range of a file to its correct offset and finalizes once the whole thing has arrived.
Explained by
highlit
1func UploadChunk(c *gin.Context) {
2 uploadID := c.Param("uploadID")
3 if !validUploadID.MatchString(uploadID) {
4 c.JSON(http.StatusBadRequest, gin.H{"error": "invalid upload id"})
5 return
6 }
7
8 start, end, total, err := parseContentRange(c.GetHeader("Content-Range"))
9 if err != nil {
10 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
11 return
12 }
13
14 path := filepath.Join(uploadDir, uploadID+".part")
15 f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
16 if err != nil {
17 c.JSON(http.StatusInternalServerError, gin.H{"error": "cannot open upload"})
18 return
19 }
20 defer f.Close()
21
22 if _, err := f.Seek(start, io.SeekStart); err != nil {
23 c.JSON(http.StatusInternalServerError, gin.H{"error": "seek failed"})
24 return
25 }
26
27 written, err := io.CopyN(f, c.Request.Body, end-start+1)
28 if err != nil && err != io.EOF {
29 c.JSON(http.StatusInternalServerError, gin.H{"error": "write failed"})
30 return
31 }
32
33 if start+written >= total {
34 if err := os.Rename(path, filepath.Join(uploadDir, uploadID)); err != nil {
35 c.JSON(http.StatusInternalServerError, gin.H{"error": "finalize failed"})
36 return
37 }
38 c.JSON(http.StatusCreated, gin.H{"uploadID": uploadID, "size": total, "complete": true})
39 return
40 }
41
42 c.Header("Range", fmt.Sprintf("bytes=0-%d", start+written-1))
43 c.JSON(http.StatusAccepted, gin.H{"uploadID": uploadID, "received": start + written, "total": total})
44}
45
46func parseContentRange(h string) (start, end, total int64, err error) {
47 if !strings.HasPrefix(h, "bytes ") {
48 return 0, 0, 0, fmt.Errorf("missing Content-Range header")
49 }
50 if _, err = fmt.Sscanf(h, "bytes %d-%d/%d", &start, &end, &total); err != nil {
51 return 0, 0, 0, fmt.Errorf("malformed Content-Range")
52 }
53 if start < 0 || end < start || end >= total {
54 return 0, 0, 0, fmt.Errorf("invalid range bounds")
55 }
56 return start, end, total, nil
57}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Writing chunks at their declared offset lets uploads resume without buffering the whole file in memory.
- 2Staging to a .part file and renaming on completion gives an atomic, all-or-nothing finalize.
- 3Every piece of client-supplied input — the id, the range header, the bounds — must be validated before it touches disk.
Related explainers
go
package handlers import ( "net/http"
Serving embedded static meta files in Gin
embedding
static assets
http caching
Intermediate
6 steps
go
package config import ( "fmt"
A thread-safe config singleton in Go
singleton
concurrency
environment-variables
Intermediate
7 steps
go
package scheduler import ( "container/heap"
A priority job queue with Go's container/heap
priority-queue
heap
interfaces
Intermediate
9 steps
ruby
require "timeout" require "net/http" class RemoteInventoryClient
Bounding a slow HTTP call with Timeout in Ruby
timeout
error-handling
http
Intermediate
6 steps
rust
use axum::{ body::Body, extract::Path, http::{header, HeaderMap, HeaderValue, StatusCode},
HTTP range requests for video streaming in Axum
http-range-requests
streaming
async-io
Advanced
8 steps
go
func (h *ArticleHandler) Create(c *gin.Context) { var req struct { Title string `json:"title" binding:"required"` Body string `json:"body" binding:"required"`
Handling a POST request in Gin
request binding
validation
http status codes
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/resumable-chunked-uploads-in-gin-explained-go-4fcd/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.