Golang serving resume able file downloads with net/http

March 30, 2020

Here is a fully commented example golang function on how to serve and transmit files to clients with the go net/http package. It supports the "Accept-Ranges" header, which allows for downloads to be paused or interrupted and resumed later on. This is very useful for clients who don't have stable connections to download larger files. However this function does not support Multipart downloads, if you wish to have that functionality check this function in the net/http source code1 for implementation.

This function is written with the following sources RFC 72332 section 3.13, MDN Range Requests4 and Content Range5.

 Go function

package main
import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"
)
func serveFile(writer http.ResponseWriter, request *http.Request, filePath string) (err error) {
	file, err := os.Open(filePath)
	if err != nil {
		return
	}
	defer file.Close() // Close the file after function return
	// Reading header info from the opened file, this will be used for response header "Content-Type"
	fileHeader := make([]byte, 512)
	_, err = file.Read(fileHeader) // File offset is now len(fileHeader)
	if err != nil {
		return err
	}
	// Get file info which we will use for the response headers "Content-Disposition" and "Content-Length"
	fileInfo, err := file.Stat()
	if err != nil {
		return err
	}
	// Set default headers
	// attachment is required to tell some (older) browsers who follow an href to download the file
	// instead of showing/printing the content to the screen.
	// For example, if you click a link to an image, the browser will pop up the download dialog
	// box compared to drawing the image in the browser tab.
	writer.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s""`, fileInfo.Name()))
	// A must for every request that has a body (see RFC 2616 section 7.2.1)
	writer.Header().Set("Content-Type", http.DetectContentType(fileHeader))
	// Tell the client we accept ranges, this gives clients the option to pause the transfer
	// and pick up later where they left up. Or download managers to establish multiple connections
	writer.Header().Set("Accept-Ranges", "bytes")
	// Check if the client requests a range from the file (see RFC 7233 section 4.2)
	requestRange := request.Header.Get("range")
	if requestRange == "" {
		// No range is defined, tell the client the incoming length of data, the size of the open file
		writer.Header().Set("Content-Length", strconv.Itoa(int(fileInfo.Size())))
		// Since we read 512 bytes for 'fileHeader' earlier, we set the reader offset back
		// to 0 starting from the beginning of the the file (the 0 in the second argument)
		file.Seek(0, 0)
		// Stream the file to the client
		io.Copy(writer, file)
		return nil
	}
	// Client requests a part of the file
	// Decode the request header to integers we can use for offset
	requestRange = requestRange[6:] // Strip the "bytes=", left over is now "begin-end"
	splitRange := strings.Split(requestRange, "-")
	if len(splitRange) != 2 {
		return fmt.Errorf("invalid values for header 'Range'")
	}
	begin, err := strconv.ParseInt(splitRange[0], 10, 64)
	if err != nil {
		return err
	}
	end, err := strconv.ParseInt(splitRange[1], 10, 64)
	if err != nil {
		return err
	}
	if begin > fileInfo.Size() || end > fileInfo.Size() {
		return fmt.Errorf("range out of bounds for file")
	}
	if begin >= end {
		return fmt.Errorf("range begin cannot be bigger than range end")
	}
	// Tell the amount bytes the client will receive
	writer.Header().Set("Content-Length", strconv.FormatInt(end-begin+1, 10))
	// Confirm the range values to the client, and the total size of the file
	// 'Content-Range' : 'bytes begin-end/totalFileSize'
	writer.Header().Set("Content-Range",
		fmt.Sprintf("bytes %d-%d/%d", begin, end, fileInfo.Size()))
	// Response http status code 206
	writer.WriteHeader(http.StatusPartialContent)
	// Set the file offset to the requested beginning
	file.Seek(begin, 0)
	// Send the (end-begin) amount of bytes to the client
	io.CopyN(writer, file, end-begin)
	return nil
}

Read also

Implementing rate limiters in the Echo framework
Golang transfer a file over a TCP socket
Generate CRC32 hash of a file in Golang turorial
Generate SHA1 hash of a file in Golang example
How to Create Linked Lists in Go
Embedding Files into Your Go Program
Comments
References
1
go/fs.go at 7bfac4c3ddde3dd906b344f141a9d09a5f855c77 · golang/go · GitHub

https://github.com/golang/go/blob/7bfac4c3ddde3dd906b344f141a9d09a5f855c77/src/net/http/fs.go#L259

cached copy
3
RFC 7233: Hypertext Transfer Protocol (HTTP/1.1): Range Requests

https://tools.ietf.org/html/rfc7233#section-3.1

cached copy
4
HTTP range requests - HTTP | MDN

https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests

cached copy
5
Content-Range - HTTP | MDN

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range

cached copy
Tags