go 40 lines · 8 steps

Streaming a ZIP download in Gin

A Gin handler assembles project reports into a ZIP and streams it straight to the client without buffering the whole archive.

Explained by highlit
1func (h *ExportHandler) BulkExport(c *gin.Context) {
2 projectID := c.Param("projectID")
3 
4 reports, err := h.reports.ListByProject(c.Request.Context(), projectID)
5 if err != nil {
6 c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to load reports"})
7 return
8 }
9 if len(reports) == 0 {
10 c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "no reports to export"})
11 return
12 }
13 
14 filename := fmt.Sprintf("reports-%s-%s.zip", projectID, time.Now().Format("20060102"))
15 c.Header("Content-Type", "application/zip")
16 c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename=%q`, filename))
17 c.Header("X-Content-Type-Options", "nosniff")
18 
19 zw := zip.NewWriter(c.Writer)
20 defer zw.Close()
21 
22 for _, r := range reports {
23 w, err := zw.CreateHeader(&zip.FileHeader{
24 Name: fmt.Sprintf("%s/%s.csv", r.Category, r.Slug),
25 Method: zip.Deflate,
26 Modified: r.UpdatedAt,
27 })
28 if err != nil {
29 c.Error(err)
30 return
31 }
32 
33 if err := h.reports.StreamCSV(c.Request.Context(), r.ID, w); err != nil {
34 c.Error(err)
35 return
36 }
37 
38 c.Writer.Flush()
39 }
40}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Writing the zip.Writer directly onto the response writer avoids buffering the entire archive in memory.
  2. 2Setting download headers before writing any body is essential because headers can't change once bytes are flushed.
  3. 3Validate that there's something to send and fail fast before committing to a streaming response.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Streaming a ZIP download in Gin — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code