Handle MultipleUpload file uploads with Go's standard library
net/http package and
multipart.Reader.
No external dependencies required.
Use Go's built-in multipart form parsing. Files are saved to disk with a unique GUID filename.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/google/uuid"
)
const uploadDir = "./uploads"
const chunkDir = "./uploads/chunks"
const maxUploadSize = 200 << 20 // 200 MB
func main() {
os.MkdirAll(uploadDir, 0755)
os.MkdirAll(chunkDir, 0755)
mux := http.NewServeMux()
mux.HandleFunc("/upload", corsMiddleware(handleUpload))
mux.HandleFunc("/upload-chunk", corsMiddleware(handleChunkUpload))
fmt.Println("Go upload server on http://localhost:8080")
http.ListenAndServe(":8080", mux)
}
func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers",
"Content-Type, X-Upload-Id, X-Chunk-Index, X-Chunk-Count, X-File-Name, X-File-Size")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next(w, r)
}
}
func handleUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
if err := r.ParseMultipartForm(32 << 20); err != nil {
jsonError(w, "File too large", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("file")
if err != nil {
jsonError(w, "No file uploaded", http.StatusBadRequest)
return
}
defer file.Close()
fileGuid := uuid.New().String()
ext := filepath.Ext(header.Filename)
destPath := filepath.Join(uploadDir, fileGuid+ext)
dest, err := os.Create(destPath)
if err != nil {
jsonError(w, "Failed to save file", http.StatusInternalServerError)
return
}
defer dest.Close()
io.Copy(dest, file)
jsonResponse(w, map[string]interface{}{
"success": true,
"fileGuid": fileGuid,
"fileName": header.Filename,
"fileSize": header.Size,
})
}
Each chunk arrives as a raw body with custom headers. Store chunks in a temporary directory and assemble the final file once all chunks have arrived.
func handleChunkUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
uploadId := r.Header.Get("X-Upload-Id")
chunkIndex, _ := strconv.Atoi(r.Header.Get("X-Chunk-Index"))
chunkCount, _ := strconv.Atoi(r.Header.Get("X-Chunk-Count"))
fileName := r.Header.Get("X-File-Name")
fileSize, _ := strconv.ParseInt(r.Header.Get("X-File-Size"), 10, 64)
if uploadId == "" || chunkCount == 0 {
jsonError(w, "Missing chunk headers", http.StatusBadRequest)
return
}
sessionDir := filepath.Join(chunkDir, uploadId)
os.MkdirAll(sessionDir, 0755)
// Save this chunk
chunkPath := filepath.Join(sessionDir, fmt.Sprintf("chunk_%d", chunkIndex))
chunkFile, err := os.Create(chunkPath)
if err != nil {
jsonError(w, "Failed to save chunk", http.StatusInternalServerError)
return
}
io.Copy(chunkFile, r.Body)
chunkFile.Close()
// Count received chunks
entries, _ := os.ReadDir(sessionDir)
received := len(entries)
if received == chunkCount {
// Assemble final file
fileGuid := uuid.New().String()
ext := filepath.Ext(fileName)
finalPath := filepath.Join(uploadDir, fileGuid+ext)
output, err := os.Create(finalPath)
if err != nil {
jsonError(w, "Failed to assemble file", http.StatusInternalServerError)
return
}
for i := 0; i < chunkCount; i++ {
cp := filepath.Join(sessionDir, fmt.Sprintf("chunk_%d", i))
chunk, _ := os.Open(cp)
io.Copy(output, chunk)
chunk.Close()
}
output.Close()
// Clean up chunks
os.RemoveAll(sessionDir)
jsonResponse(w, map[string]interface{}{
"success": true,
"fileGuid": fileGuid,
"fileName": fileName,
"fileSize": fileSize,
})
return
}
jsonResponse(w, map[string]interface{}{
"success": true,
"chunksReceived": received,
})
}
func jsonResponse(w http.ResponseWriter, data map[string]interface{}) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}
func jsonError(w http.ResponseWriter, msg string, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"error": msg,
})
}
// Note: add to imports if using strings package
var _ = strings.TrimSpace
Both endpoints must return this JSON structure on successful upload completion.
{
"success": true,
"fileGuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"fileName": "photo.jpg",
"fileSize": 204800
}
Initialize MultipleUpload on the client, pointing to your Go server endpoints.
<link rel="stylesheet" href="multipleupload.css" />
<div id="uploader"></div>
<script src="multipleupload.js"></script>
<script>
var uploader = new MultipleUpload("#uploader", {
uploadUrl: "http://localhost:8080/upload",
chunkUploadUrl: "http://localhost:8080/upload-chunk",
chunkSize: 2 * 1024 * 1024, // 2 MB chunks
onFileUploaded: function (file, response) {
console.log("Uploaded:", response.fileName, response.fileGuid);
}
});
</script>