blob: 6658db2d1f4dc368052cc90a4d8d153f1e811281 [file] [log] [blame]
Philip Zeyliger176de792025-04-21 12:25:18 -07001// Package gzhandler provides an HTTP file server implementation that serves pre-compressed files
2// when available to clients that support gzip encoding.
3package gzhandler
4
5import (
Josh Bleecher Snydera002a232025-07-09 19:38:03 +00006 "compress/gzip"
Philip Zeyliger176de792025-04-21 12:25:18 -07007 "io"
8 "io/fs"
9 "mime"
10 "net/http"
11 "path"
12 "strings"
13)
14
15// Handler is an http.Handler that checks for pre-compressed files
16// and serves them with appropriate headers when available.
17type Handler struct {
18 root http.FileSystem
19}
20
21// New creates a handler that serves HTTP requests
22// with the contents of the file system rooted at root and uses pre-compressed
23// .gz files when available, with appropriate headers.
24func New(root fs.FS) http.Handler {
25 return &Handler{root: http.FS(root)}
26}
27
28// ServeHTTP serves a file with special handling for pre-compressed .gz files
29func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Philip Zeyliger176de792025-04-21 12:25:18 -070030 urlPath := r.URL.Path
31 if !strings.HasPrefix(urlPath, "/") {
32 urlPath = "/" + urlPath
33 }
34 urlPath = path.Clean(urlPath)
35
Philip Zeyliger176de792025-04-21 12:25:18 -070036 // Check if the file itself is not a gzip file (we don't want to double-compress)
37 isCompressibleFile := !strings.HasSuffix(urlPath, ".gz")
Josh Bleecher Snydera002a232025-07-09 19:38:03 +000038 if !isCompressibleFile {
39 // Fall back to regular serving.
40 http.FileServer(h.root).ServeHTTP(w, r)
41 return
Philip Zeyliger176de792025-04-21 12:25:18 -070042 }
43
Josh Bleecher Snydera002a232025-07-09 19:38:03 +000044 // Try to open the gzipped version of the file
45 gzPath := urlPath + ".gz"
46 gzFile, err := h.root.Open(gzPath)
47 if err != nil {
48 // Fall back to regular serving.
49 http.FileServer(h.root).ServeHTTP(w, r)
50 return
51 }
52 defer gzFile.Close()
53
54 // Fall back to regular serving for directories (how would this even happen?)
55 gzStat, err := gzFile.Stat()
56 if err != nil || gzStat.IsDir() {
57 // Not a valid file, fall back to normal serving
58 http.FileServer(h.root).ServeHTTP(w, r)
59 return
60 }
61
62 // Determine the content type based on the original file (not the .gz)
63 contentType := mime.TypeByExtension(path.Ext(urlPath))
64 if contentType == "" {
65 contentType = "application/octet-stream"
66 }
67
68 acceptsGzip := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
69 if acceptsGzip {
70 w.Header().Set("Content-Type", contentType)
71 w.Header().Set("Content-Encoding", "gzip")
72 w.Header().Set("Vary", "Accept-Encoding")
73
74 // Read the gzipped file into memory to avoid 'seeker can't seek' error
75 gzippedData, err := io.ReadAll(gzFile)
76 if err != nil {
77 http.Error(w, "Error reading gzipped content", http.StatusInternalServerError)
78 return
79 }
80
81 // Write the headers and gzipped content
82 w.WriteHeader(http.StatusOK)
83 w.Write(gzippedData)
84 return
85 }
86
87 // No gzip support; decompress for them.
88
89 // Decompress the .gz file and serve it uncompressed
90 gzReader, err := gzip.NewReader(gzFile)
91 if err != nil {
92 http.FileServer(h.root).ServeHTTP(w, r)
93 return
94 }
95 defer gzReader.Close()
96 w.Header().Set("Content-Type", contentType)
97 w.WriteHeader(http.StatusOK)
98 io.Copy(w, gzReader)
Philip Zeyliger176de792025-04-21 12:25:18 -070099}