vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
| 1 | package compress |
| 2 | |
| 3 | // Package compress gzips text responses. |
| 4 | // |
| 5 | // It is shared by the two HTTP surfaces that serve bytes: the forge UI (wrapped |
| 6 | // into its middleware, because it needs the writer inside its own panic and |
| 7 | // page-view bookkeeping) and hosted sites (wrapped once around the site |
| 8 | // dispatcher). One implementation so both answer the same negotiation and the |
| 9 | // same header rules. |
| 10 | // |
| 11 | // The web surface sends HTML, CSS, JavaScript and source blobs, all of which |
| 12 | // compress to a fraction of their size: the vendored Prism bundle alone is |
| 13 | // 545 KB and lands near 200 KB gzipped, and a rendered page about a fifth of its |
| 14 | // size. There is no brotli here on purpose — it would add a dependency for a few |
| 15 | // points of ratio, and Go's gzip needs none. |
| 16 | // |
| 17 | // The rules are narrow, because the same handler chain also streams git |
| 18 | // responses, release archives and LFS objects: |
| 19 | // |
| 20 | // - only when the client sent `Accept-Encoding: gzip` with q > 0; |
| 21 | // - only text-like content types, never a body that is already encoded; |
| 22 | // - never a Range request (the compressed body has no meaningful ranges); |
| 23 | // - never a body the handler has already sized below compressMinBytes; |
| 24 | // - never a 1xx, 204, 206, 304 or redirect (nothing to gain, and 304 must |
| 25 | // stay bodiless). |
| 26 | // |
| 27 | // The writer is transparent otherwise: it passes Header through, keeps Flush |
| 28 | // working for the streaming handlers, and exposes Unwrap so a handler can reach |
| 29 | // the underlying ResponseWriter. |
| 30 | |
| 31 | import ( |
| 32 | "compress/gzip" |
| 33 | "net/http" |
| 34 | "strconv" |
| 35 | "strings" |
| 36 | ) |
| 37 | |
| 38 | // compressMinBytes is the smallest declared body worth a gzip header and |
| 39 | // trailer. Below it the compressed form is usually larger than the source. |
| 40 | const compressMinBytes = 256 |
| 41 | |
| 42 | // Writer decides per response whether to gzip, and lazily starts the stream on |
| 43 | // the first write so a header-only response never opens one. |
| 44 | type Writer struct { |
| 45 | rw http.ResponseWriter |
| 46 | r *http.Request |
| 47 | gz *gzip.Writer |
| 48 | status int |
| 49 | wroteHeader bool |
| 50 | gzipping bool |
| 51 | } |
| 52 | |
| 53 | // New wraps rw for one request. The caller must Close it when the handler has |
| 54 | // returned; Handler does that for a whole handler tree. |
| 55 | func New(rw http.ResponseWriter, r *http.Request) *Writer { |
| 56 | return &Writer{rw: rw, r: r} |
| 57 | } |
| 58 | |
| 59 | // Handler wraps next so every response it writes is compressed by the rules in |
| 60 | // New. It is the whole of the site surface's compression support. |
| 61 | func Handler(next http.Handler) http.Handler { |
| 62 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { |
| 63 | w := New(rw, r) |
| 64 | next.ServeHTTP(w, r) |
| 65 | w.Close() |
| 66 | }) |
| 67 | } |
| 68 | |
| 69 | func (c *Writer) Header() http.Header { return c.rw.Header() } |
| 70 | |
| 71 | func (c *Writer) WriteHeader(status int) { |
| 72 | if c.wroteHeader { |
| 73 | return |
| 74 | } |
| 75 | c.wroteHeader = true |
| 76 | c.status = status |
| 77 | h := c.rw.Header() |
| 78 | if c.acceptsGzip() && compressibleStatus(status) && compressibleHeader(h, c.r) { |
| 79 | c.gzipping = true |
| 80 | h.Set("Content-Encoding", "gzip") |
| 81 | h.Del("Content-Length") |
| 82 | h.Add("Vary", "Accept-Encoding") |
| 83 | } |
| 84 | c.rw.WriteHeader(status) |
| 85 | } |
| 86 | |
| 87 | func (c *Writer) Write(b []byte) (int, error) { |
| 88 | if !c.wroteHeader { |
| 89 | c.WriteHeader(http.StatusOK) |
| 90 | } |
| 91 | if !c.gzipping { |
| 92 | return c.rw.Write(b) |
| 93 | } |
| 94 | if c.gz == nil { |
| 95 | c.gz = gzip.NewWriter(c.rw) |
| 96 | } |
| 97 | return c.gz.Write(b) |
| 98 | } |
| 99 | |
| 100 | // Flush keeps streaming handlers working: the header is settled, the gzip |
| 101 | // buffer is emptied, and the underlying writer is flushed if it can be. |
| 102 | func (c *Writer) Flush() { |
| 103 | if !c.wroteHeader { |
| 104 | c.WriteHeader(http.StatusOK) |
| 105 | } |
| 106 | if c.gz != nil { |
| 107 | _ = c.gz.Flush() |
| 108 | } |
| 109 | if f, ok := c.rw.(http.Flusher); ok { |
| 110 | f.Flush() |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | // Close finishes the gzip stream. It is called once by the middleware after the |
| 115 | // handler returns, never by a handler, but it is idempotent enough for the |
| 116 | // panic path that may have written nothing at all. |
| 117 | func (c *Writer) Close() { |
| 118 | if c.gz != nil { |
| 119 | _ = c.gz.Close() |
| 120 | c.gz = nil |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | func (c *Writer) Unwrap() http.ResponseWriter { return c.rw } |
| 125 | |
| 126 | // acceptsGzip reports whether the client asked for gzip and would accept it: |
| 127 | // `gzip` with a q-value of zero is a refusal, not a request. |
| 128 | func (c *Writer) acceptsGzip() bool { |
| 129 | if c.r.Method == http.MethodHead { |
| 130 | return false |
| 131 | } |
| 132 | for _, part := range strings.Split(c.r.Header.Get("Accept-Encoding"), ",") { |
| 133 | name, params, _ := strings.Cut(strings.TrimSpace(part), ";") |
| 134 | if !strings.EqualFold(strings.TrimSpace(name), "gzip") { |
| 135 | continue |
| 136 | } |
| 137 | q := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(params), "q=")) |
| 138 | if q == "" { |
| 139 | return true |
| 140 | } |
| 141 | if v, err := strconv.ParseFloat(q, 64); err == nil { |
| 142 | return v > 0 |
| 143 | } |
| 144 | return true |
| 145 | } |
| 146 | return false |
| 147 | } |
| 148 | |
| 149 | // compressibleStatus covers the bodies worth compressing: a rendered page, a |
| 150 | // fragment, and the styled error page. Redirects are a sentence, a 304 must stay |
| 151 | // bodiless, and a 206 is already a byte range. |
| 152 | func compressibleStatus(status int) bool { |
| 153 | switch status { |
| 154 | case http.StatusNoContent, http.StatusNotModified, http.StatusPartialContent: |
| 155 | return false |
| 156 | } |
| 157 | return (status >= 200 && status < 300) || status >= 400 |
| 158 | } |
| 159 | |
| 160 | // compressibleHeader applies the remaining rules: a body that is already |
| 161 | // encoded stays as it is, a Range request is answered with bytes, and a body |
| 162 | // the handler already sized as tiny is not worth compressing. |
| 163 | func compressibleHeader(h http.Header, r *http.Request) bool { |
| 164 | if h.Get("Content-Encoding") != "" || h.Get("Content-Range") != "" { |
| 165 | return false |
| 166 | } |
| 167 | // A handler that promised exact bytes (a raw download with its Content-Length) |
| 168 | // opts out with the standard directive rather than a private header. |
| 169 | if strings.Contains(strings.ToLower(h.Get("Cache-Control")), "no-transform") { |
| 170 | return false |
| 171 | } |
| 172 | if r.Header.Get("Range") != "" { |
| 173 | return false |
| 174 | } |
| 175 | if n, err := strconv.Atoi(h.Get("Content-Length")); err == nil && n < compressMinBytes { |
| 176 | return false |
| 177 | } |
| 178 | return compressibleType(h.Get("Content-Type")) |
| 179 | } |
| 180 | |
| 181 | // compressibleType is the allow-list of types worth gzipping. Anything else — |
| 182 | // images, archives, packs, LFS objects, git protocol output — is already |
| 183 | // compressed or is a byte range where compression cannot help. |
| 184 | func compressibleType(contentType string) bool { |
| 185 | ct := strings.ToLower(strings.TrimSpace(contentType)) |
| 186 | if i := strings.IndexByte(ct, ';'); i >= 0 { |
| 187 | ct = strings.TrimSpace(ct[:i]) |
| 188 | } |
| 189 | switch ct { |
| 190 | case "text/event-stream": |
| 191 | return false |
| 192 | case "image/svg+xml", |
| 193 | "application/json", |
| 194 | "application/javascript", |
| 195 | "application/x-javascript", |
| 196 | "application/xml", |
| 197 | "application/xhtml+xml", |
| 198 | "application/manifest+json", |
| 199 | "application/rss+xml", |
| 200 | "application/atom+xml": |
| 201 | return true |
| 202 | } |
| 203 | return strings.HasPrefix(ct, "text/") || |
| 204 | strings.HasSuffix(ct, "+json") || |
| 205 | strings.HasSuffix(ct, "+xml") |
| 206 | } |
| 207 |