blob: 6658db2d1f4dc368052cc90a4d8d153f1e811281 [file] [log] [blame]
// Package gzhandler provides an HTTP file server implementation that serves pre-compressed files
// when available to clients that support gzip encoding.
package gzhandler
import (
"compress/gzip"
"io"
"io/fs"
"mime"
"net/http"
"path"
"strings"
)
// Handler is an http.Handler that checks for pre-compressed files
// and serves them with appropriate headers when available.
type Handler struct {
root http.FileSystem
}
// New creates a handler that serves HTTP requests
// with the contents of the file system rooted at root and uses pre-compressed
// .gz files when available, with appropriate headers.
func New(root fs.FS) http.Handler {
return &Handler{root: http.FS(root)}
}
// ServeHTTP serves a file with special handling for pre-compressed .gz files
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
urlPath := r.URL.Path
if !strings.HasPrefix(urlPath, "/") {
urlPath = "/" + urlPath
}
urlPath = path.Clean(urlPath)
// Check if the file itself is not a gzip file (we don't want to double-compress)
isCompressibleFile := !strings.HasSuffix(urlPath, ".gz")
if !isCompressibleFile {
// Fall back to regular serving.
http.FileServer(h.root).ServeHTTP(w, r)
return
}
// Try to open the gzipped version of the file
gzPath := urlPath + ".gz"
gzFile, err := h.root.Open(gzPath)
if err != nil {
// Fall back to regular serving.
http.FileServer(h.root).ServeHTTP(w, r)
return
}
defer gzFile.Close()
// Fall back to regular serving for directories (how would this even happen?)
gzStat, err := gzFile.Stat()
if err != nil || gzStat.IsDir() {
// Not a valid file, fall back to normal serving
http.FileServer(h.root).ServeHTTP(w, r)
return
}
// Determine the content type based on the original file (not the .gz)
contentType := mime.TypeByExtension(path.Ext(urlPath))
if contentType == "" {
contentType = "application/octet-stream"
}
acceptsGzip := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
if acceptsGzip {
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Vary", "Accept-Encoding")
// Read the gzipped file into memory to avoid 'seeker can't seek' error
gzippedData, err := io.ReadAll(gzFile)
if err != nil {
http.Error(w, "Error reading gzipped content", http.StatusInternalServerError)
return
}
// Write the headers and gzipped content
w.WriteHeader(http.StatusOK)
w.Write(gzippedData)
return
}
// No gzip support; decompress for them.
// Decompress the .gz file and serve it uncompressed
gzReader, err := gzip.NewReader(gzFile)
if err != nil {
http.FileServer(h.root).ServeHTTP(w, r)
return
}
defer gzReader.Close()
w.Header().Set("Content-Type", contentType)
w.WriteHeader(http.StatusOK)
io.Copy(w, gzReader)
}