blob: 3109ea20930d61a18ea7d4e8c9cae233fc0a575a [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 (
6 "io"
7 "io/fs"
8 "mime"
9 "net/http"
10 "path"
11 "strings"
12)
13
14// Handler is an http.Handler that checks for pre-compressed files
15// and serves them with appropriate headers when available.
16type Handler struct {
17 root http.FileSystem
18}
19
20// New creates a handler that serves HTTP requests
21// with the contents of the file system rooted at root and uses pre-compressed
22// .gz files when available, with appropriate headers.
23func New(root fs.FS) http.Handler {
24 return &Handler{root: http.FS(root)}
25}
26
27// ServeHTTP serves a file with special handling for pre-compressed .gz files
28func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
29 // Clean and prepare the URL path
30 urlPath := r.URL.Path
31 if !strings.HasPrefix(urlPath, "/") {
32 urlPath = "/" + urlPath
33 }
34 urlPath = path.Clean(urlPath)
35
36 // Check if client accepts gzip encoding
37 acceptEncoding := r.Header.Get("Accept-Encoding")
38 acceptsGzip := strings.Contains(acceptEncoding, "gzip")
39
40 // Check if the file itself is not a gzip file (we don't want to double-compress)
41 isCompressibleFile := !strings.HasSuffix(urlPath, ".gz")
42
43 if acceptsGzip && isCompressibleFile {
44 // Try to open the gzipped version of the file
45 gzPath := urlPath + ".gz"
46 gzFile, err := h.root.Open(gzPath)
47
48 if err == nil {
49 defer gzFile.Close()
50
51 // Get file info to check if it's a regular file
52 gzStat, err := gzFile.Stat()
53 if err != nil || gzStat.IsDir() {
54 // Not a valid file, fall back to normal serving
55 http.FileServer(h.root).ServeHTTP(w, r)
56 return
57 }
58
59 // Determine the content type based on the original file (not the .gz)
60 contentType := mime.TypeByExtension(path.Ext(urlPath))
61 if contentType == "" {
62 contentType = "application/octet-stream"
63 }
64
65 // Set the appropriate headers for serving gzipped content
66 w.Header().Set("Content-Type", contentType)
67 w.Header().Set("Content-Encoding", "gzip")
68 w.Header().Set("Vary", "Accept-Encoding")
69
70 // Read the gzipped file into memory to avoid 'seeker can't seek' error
71 gzippedData, err := io.ReadAll(gzFile)
72 if err != nil {
73 http.Error(w, "Error reading gzipped content", http.StatusInternalServerError)
74 return
75 }
76
77 // Write the headers and gzipped content
78 w.WriteHeader(http.StatusOK)
79 w.Write(gzippedData)
80 return
81 }
82 }
83
84 // Fall back to standard file serving if gzipped version not found or not applicable
85 http.FileServer(h.root).ServeHTTP(w, r)
86}