Skip to main content

Gin File Downloads

Introduction

File downloads are a common requirement in web applications. Whether you're building a file-sharing service, allowing users to download reports, or serving assets like images and PDFs, understanding how to implement file downloads in Gin is essential.

In this tutorial, we'll explore various techniques for handling file downloads in Gin, from serving static files to generating files dynamically. You'll learn how to:

  • Serve existing files from your server
  • Generate files on-the-fly for download
  • Set proper headers for different file types
  • Implement conditional downloads

Basic File Downloads

Serving Static Files

The simplest way to serve files in Gin is using the built-in c.File() method, which automatically handles all the necessary HTTP headers and streaming.

go
func main() {
router := gin.Default()

router.GET("/download", func(c *gin.Context) {
filepath := "./files/sample.pdf"
c.File(filepath)
})

router.Run(":8080")
}

When a user visits /download, the server will respond with the file and appropriate HTTP headers, prompting the browser to download or display it (depending on the file type).

Custom Filename for Downloads

Sometimes you want users to download a file with a different name than what's stored on the server. You can use c.FileAttachment() to specify both the filepath and download filename:

go
router.GET("/download/report", func(c *gin.Context) {
// Actual file on server
filepath := "./generated/report-20220315.pdf"

// What the user will see when downloading
filename := "annual-report-2022.pdf"

c.FileAttachment(filepath, filename)
})

This will force the browser to download the file with the given name, regardless of the original filename.

Dynamic File Generation

Generating Files On-the-Fly

For dynamic content, you might need to generate files before serving them. Here's how to create a CSV file on-the-fly:

go
router.GET("/generate-csv", func(c *gin.Context) {
// Set appropriate headers
c.Header("Content-Disposition", "attachment; filename=data.csv")
c.Header("Content-Type", "text/csv")

// Create CSV data
data := [][]string{
{"Name", "Age", "City"},
{"John", "30", "New York"},
{"Alice", "25", "London"},
{"Bob", "28", "Paris"},
}

// Write CSV directly to response writer
writer := csv.NewWriter(c.Writer)
for _, record := range data {
if err := writer.Write(record); err != nil {
c.AbortWithError(500, err)
return
}
}
writer.Flush()
})

Serving In-Memory Files

If you have data in memory that you want to serve as a downloadable file:

go
router.GET("/memory-file", func(c *gin.Context) {
// Create some in-memory data
content := []byte("This is the content of the file.\nIt will be downloaded as a .txt file.")

// Create a temporary file
tmpfile, err := os.CreateTemp("", "example*.txt")
if err != nil {
c.String(http.StatusInternalServerError, "Could not create temp file")
return
}
defer os.Remove(tmpfile.Name()) // Clean up

// Write data to temp file
if _, err := tmpfile.Write(content); err != nil {
c.String(http.StatusInternalServerError, "Could not write to temp file")
return
}
if err := tmpfile.Close(); err != nil {
c.String(http.StatusInternalServerError, "Could not close temp file")
return
}

// Serve the file
c.FileAttachment(tmpfile.Name(), "document.txt")
})

Alternatively, for simpler cases, you can use c.Data():

go
router.GET("/simple-download", func(c *gin.Context) {
content := []byte("Hello, this is a downloadable text file!")

c.Header("Content-Disposition", "attachment; filename=hello.txt")
c.Header("Content-Type", "text/plain")
c.Data(http.StatusOK, "text/plain", content)
})

Advanced File Download Techniques

Downloading Files with Progress

For large files, you might want to show download progress. This requires client-side JavaScript, but the server needs to provide the correct headers:

go
router.GET("/large-file", func(c *gin.Context) {
filepath := "./files/large-video.mp4"

// Get file info
fileInfo, err := os.Stat(filepath)
if err != nil {
c.String(http.StatusNotFound, "File not found")
return
}

// Open the file
file, err := os.Open(filepath)
if err != nil {
c.String(http.StatusInternalServerError, "Could not open file")
return
}
defer file.Close()

// Set content length for progress monitoring
c.Header("Content-Description", "File Transfer")
c.Header("Content-Transfer-Encoding", "binary")
c.Header("Content-Disposition", "attachment; filename=video.mp4")
c.Header("Content-Type", "video/mp4")
c.Header("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))

// Stream the file to the client
http.ServeContent(c.Writer, c.Request, "video.mp4", fileInfo.ModTime(), file)
})

Conditional Downloads

You can implement conditional downloads using query parameters:

go
router.GET("/conditional-download", func(c *gin.Context) {
// Check if the user is allowed to download
userParam := c.DefaultQuery("user", "")
tokenParam := c.DefaultQuery("token", "")

// Simple validation (in a real app, use proper authentication)
if userParam == "admin" && tokenParam == "secret123" {
c.File("./files/confidential.pdf")
} else {
c.String(http.StatusUnauthorized, "You don't have permission to download this file")
}
})

Download Limiting and Throttling

For bandwidth management, you might want to limit download speeds:

go
import (
"golang.org/x/time/rate"
"io"
)

func main() {
router := gin.Default()

// Create a rate limiter: 1MB/s
limiter := rate.NewLimiter(rate.Limit(1*1024*1024), 2*1024*1024)

router.GET("/throttled-download", func(c *gin.Context) {
filepath := "./files/large-file.zip"

file, err := os.Open(filepath)
if err != nil {
c.String(http.StatusInternalServerError, "Could not open file")
return
}
defer file.Close()

fileInfo, err := file.Stat()
if err != nil {
c.String(http.StatusInternalServerError, "Could not get file info")
return
}

c.Header("Content-Disposition", "attachment; filename=limited-speed.zip")
c.Header("Content-Type", "application/zip")
c.Header("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))

// Create a reader that respects the rate limit
limitedReader := &LimitedReader{
R: file,
Limiter: limiter,
}

// Stream the file with rate limiting
io.Copy(c.Writer, limitedReader)
})

router.Run(":8080")
}

// LimitedReader wraps an io.Reader with rate limiting
type LimitedReader struct {
R io.Reader
Limiter *rate.Limiter
}

func (r *LimitedReader) Read(buf []byte) (int, error) {
n, err := r.R.Read(buf)
if n <= 0 {
return n, err
}

// Wait for token availability based on bytes read
r.Limiter.WaitN(context.Background(), n)
return n, err
}

Best Practices for File Downloads

1. Set Appropriate Headers

Always set the correct headers for your downloads:

go
// For forced downloads (will trigger download dialog)
c.Header("Content-Disposition", "attachment; filename=yourfile.ext")

// For in-browser viewing
c.Header("Content-Disposition", "inline; filename=yourfile.ext")

// Set the correct content type
c.Header("Content-Type", "application/pdf") // For PDFs

2. Handle File Not Found Gracefully

Always check if files exist before attempting to serve them:

go
router.GET("/download/:filename", func(c *gin.Context) {
filename := c.Param("filename")
filepath := "./files/" + filename

// Check if file exists
if _, err := os.Stat(filepath); os.IsNotExist(err) {
c.HTML(http.StatusNotFound, "error.html", gin.H{
"error": "The requested file was not found",
})
return
}

c.File(filepath)
})

3. Security Considerations

Never blindly serve files based on user input without validation:

go
router.GET("/download", func(c *gin.Context) {
filename := c.Query("file")

// DANGEROUS: Don't do this!
// c.File("./files/" + filename)

// Instead, validate and sanitize:
validFiles := map[string]string{
"report": "./files/report.pdf",
"data": "./files/data.csv",
"image": "./files/image.jpg",
}

if filepath, ok := validFiles[filename]; ok {
c.File(filepath)
} else {
c.String(http.StatusBadRequest, "Invalid file requested")
}
})

Real-World Examples

File Download Service

Here's a more complete example of a file download service:

go
package main

import (
"github.com/gin-gonic/gin"
"net/http"
"os"
"path/filepath"
"time"
)

func main() {
router := gin.Default()

// Setup file download routes
setupDownloadRoutes(router)

router.Run(":8080")
}

func setupDownloadRoutes(router *gin.Engine) {
// Create a download group
download := router.Group("/download")

// Public downloads - no authentication required
download.GET("/public/:filename", handlePublicDownload)

// Private downloads - require authentication
download.GET("/private/:filename", authMiddleware(), handlePrivateDownload)

// Generate a dynamic report
download.GET("/report", generateReport)
}

func handlePublicDownload(c *gin.Context) {
filename := c.Param("filename")

// Validate filename to prevent directory traversal attacks
if filepath.Ext(filename) == "" || containsDotDot(filename) {
c.String(http.StatusBadRequest, "Invalid filename")
return
}

// Check if the file exists in the public directory
filepath := "./public_files/" + filename
if _, err := os.Stat(filepath); os.IsNotExist(err) {
c.String(http.StatusNotFound, "File not found")
return
}

// Serve the file
c.File(filepath)
}

func handlePrivateDownload(c *gin.Context) {
filename := c.Param("filename")

// Validate filename to prevent directory traversal attacks
if filepath.Ext(filename) == "" || containsDotDot(filename) {
c.String(http.StatusBadRequest, "Invalid filename")
return
}

// Get user from the authentication middleware
user, exists := c.Get("user")
if !exists {
c.String(http.StatusInternalServerError, "User context not found")
return
}

// Check if the user has access to this file (simplified)
if !userHasAccess(user.(string), filename) {
c.String(http.StatusForbidden, "Access denied")
return
}

// Serve the file
filepath := "./private_files/" + filename
c.FileAttachment(filepath, filename)
}

func generateReport(c *gin.Context) {
// Get parameters for the report
reportType := c.DefaultQuery("type", "summary")
format := c.DefaultQuery("format", "pdf")

// Generate report (this would be more complex in a real app)
var content []byte
var contentType string
var filename string

switch format {
case "pdf":
content = []byte("PDF CONTENT WOULD GO HERE")
contentType = "application/pdf"
filename = "report.pdf"
case "csv":
content = []byte("Name,Age,Email\nJohn,30,[email protected]")
contentType = "text/csv"
filename = "report.csv"
default:
content = []byte("Unsupported format requested")
contentType = "text/plain"
filename = "error.txt"
}

// Add report type to filename
filename = reportType + "-" + filename

// Generate timestamp for unique filenames
timestamp := time.Now().Format("20060102-150405")
filename = timestamp + "-" + filename

c.Header("Content-Disposition", "attachment; filename="+filename)
c.Data(http.StatusOK, contentType, content)
}

// Auth middleware (simplified)
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")

// Very simplified token check
if token == "Bearer valid-token" {
c.Set("user", "authenticated-user")
c.Next()
} else {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
}
}
}

// Helper functions
func containsDotDot(v string) bool {
if filepath.IsAbs(v) {
return true
}

parts := filepath.SplitList(v)
for _, p := range parts {
if p == ".." || p == "." {
return true
}
}

return false
}

func userHasAccess(user, filename string) bool {
// In a real application, you would check permissions
// against a database or permission system
return true // Simplified for this example
}

Summary

In this tutorial, you've learned how to implement file downloads in Gin, including:

  • Basic file serving with c.File() and c.FileAttachment()
  • Generating dynamic files for download
  • Serving in-memory files
  • Advanced techniques like handling large file downloads and conditional downloads
  • Best practices for security and error handling

With these techniques, you can handle a wide range of file download scenarios in your Gin applications.

Exercises

  1. Create an endpoint that allows downloading a ZIP file containing multiple files from a specified directory.
  2. Implement a download counter that tracks how many times each file has been downloaded.
  3. Create an image resizer endpoint that generates and serves images in different dimensions based on query parameters.
  4. Build a file download system with token-based authentication that expires after a certain time.
  5. Implement resumable downloads for large files using Range headers.

Additional Resources

Happy coding!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)