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.
Bound and stream the anonymous read path
4581690d1ae9d87c272428d101a0fbe3538d7f44 · Keenan Nemetz · 13d ago · parent e05f1c48
Bound and stream the anonymous read path Every anonymous read path read a whole git object before deciding what to do with it: the code browser truncated after allocating, the overview rendered the README it had just read in full, and the site pool allocated the entire object for every request. `max_blob_bytes` allows a 100 MiB blob, so one pushed file made every concurrent request for it a 100 MiB allocation, with no account needed to ask for it. Read the header first instead. `core/git` gains `BlobSize`, `BlobSizes` (one `cat-file --batch-check` for a whole tree) and `BlobStream`, which parses the batch header before touching the payload and stops the process when a caller wants only a prefix. Then use it: `/raw` streams with the object's own `Content-Length`, the inline viewer reads a one-MiB prefix and reports the file's real size, the README is capped and named instead of rendered when it is too large, site blobs are capped at 25 MiB with a 413, and the commit and tree objects read to resolve a site path are capped separately. Metadata gets the same treatment on both sides: 4 MiB per file, 2 MiB per body, 20,000 files and 64 MiB for `issues/` and `requests/` together. The loader asks git for every size in one call and refuses the tree before reading it; the push boundary refuses the same tree, so the author learns why instead of finding that their issue list stopped loading. Counting is now per subtree, so the machine written `runs/` tree no longer eats the issue file budget.
server/web/codebrowser.go +22−9
| @@ -10,8 +10,10 @@ package web | |||
| 10 | 10 | import ( | |
| 11 | 11 | "fmt" | |
| 12 | 12 | "html/template" | |
| 13 | "io" | ||
| 13 | 14 | "net/http" | |
| 14 | 15 | "sort" | |
| 16 | "strconv" | ||
| 15 | 17 | "strings" | |
| 16 | 18 | "unicode/utf8" | |
| 17 | 19 | ||
| @@ -193,23 +195,34 @@ func (w *Web) blob(rw http.ResponseWriter, r *http.Request, raw bool) { | |||
| 193 | 195 | w.notFound(rw, r) | |
| 194 | 196 | return | |
| 195 | 197 | } | |
| 196 | data, err := g.BlobAt(commit, p) | ||
| 198 | stream, err := g.OpenBlobAt(commit, p) | ||
| 197 | 199 | if err != nil { | |
| 198 | 200 | w.notFound(rw, r) | |
| 199 | 201 | return | |
| 200 | 202 | } | |
| 203 | defer stream.Close() | ||
| 201 | 204 | if raw { | |
| 202 | rw.Header().Set("Content-Type", "text/plain; charset=utf-8") | ||
| 203 | rw.Write(data) | ||
| 205 | // The download is the one place that should carry every byte, so it is | ||
| 206 | // streamed rather than read and written: a 100 MiB asset costs one copy | ||
| 207 | // buffer per request, not 100 MiB of heap. | ||
| 208 | h := rw.Header() | ||
| 209 | h.Set("Content-Type", "text/plain; charset=utf-8") | ||
| 210 | h.Set("Content-Length", strconv.FormatInt(stream.Size, 10)) | ||
| 211 | if _, err := io.Copy(rw, stream); err != nil { | ||
| 212 | // The client hung up mid-download; headers are already out. | ||
| 213 | w.Logger("raw %s %s: %v", m.Name, p, err) | ||
| 214 | } | ||
| 215 | return | ||
| 216 | } | ||
| 217 | // Everything the page shows is a preview: the tree, the line numbers and the | ||
| 218 | // highlighter are all bounded, so there is no reason to hold the whole file. | ||
| 219 | data, truncated, err := stream.ReadPrefix(maxInlineBytes) | ||
| 220 | if err != nil { | ||
| 221 | w.serverError(rw, r, "cannot read "+p+": "+err.Error()) | ||
| 204 | 222 | return | |
| 205 | 223 | } | |
| 206 | 224 | content := string(data) | |
| 207 | 225 | binary := !utf8.Valid(data) | |
| 208 | truncated := false | ||
| 209 | if len(data) > maxInlineBytes { | ||
| 210 | content = string(data[:maxInlineBytes]) | ||
| 211 | truncated = true | ||
| 212 | } | ||
| 213 | 226 | // A table row per line is pleasant to read and cheap for a source file, but a | |
| 214 | 227 | // million blank lines would be neither. Past the cap the page falls back to a | |
| 215 | 228 | // preformatted block. | |
| @@ -249,7 +262,7 @@ func (w *Web) blob(rw http.ResponseWriter, r *http.Request, raw bool) { | |||
| 249 | 262 | view.LineCut = lineCut | |
| 250 | 263 | view.Lines = lines | |
| 251 | 264 | view.Highlight = highlighted | |
| 252 | view.Size = int64(len(data)) | ||
| 265 | view.Size = stream.Size | ||
| 253 | 266 | view.IsMarkdown = markdown | |
| 254 | 267 | view.Preview = preview | |
| 255 | 268 | if !full { | |