vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software: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/overview.go +30−3
| @@ -5,7 +5,9 @@ package web | |||
| 5 | 5 | // happened lately" from git and the queue, with no stored counters to go stale. | |
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | "fmt" | ||
| 8 | 9 | "html/template" | |
| 10 | "io" | ||
| 9 | 11 | "net/http" | |
| 10 | 12 | "path" | |
| 11 | 13 | "sort" | |
| @@ -14,8 +16,15 @@ import ( | |||
| 14 | 16 | "github.com/rodent-software/vole/ci/runs" | |
| 15 | 17 | "github.com/rodent-software/vole/core/git" | |
| 16 | 18 | "github.com/rodent-software/vole/forge/repo" | |
| 19 | "github.com/rodent-software/vole/internal/format" | ||
| 17 | 20 | ) | |
| 18 | 21 | ||
| 22 | // maxReadmeBytes is the largest README rendered on the overview. A file past it | ||
| 23 | // is not a README but a payload, and the page says so instead of rendering it: | ||
| 24 | // the markdown renderer and the DOM both scale with the input, and neither is | ||
| 25 | // worth spending on a file nobody reads. | ||
| 26 | const maxReadmeBytes = 1 << 20 | ||
| 27 | |||
| 19 | 28 | type languageShare struct { | |
| 20 | 29 | Name string | |
| 21 | 30 | Bytes int64 | |
| @@ -121,10 +130,28 @@ func (w *Web) readmeAt(m *repo.Meta, ref, commit string) (template.HTML, string) | |||
| 121 | 130 | return "", "" | |
| 122 | 131 | } | |
| 123 | 132 | for _, name := range []string{"README.md", "readme.md", "Readme.md", "README.markdown"} { | |
| 124 | if data, err := m.Git().BlobAt(commit, name); err == nil { | ||
| 125 | doc := treeDoc{Repo: m.Name, Ref: ref, Path: name} | ||
| 126 | return w.renderTreeMarkdown(doc, string(data)), name | ||
| 133 | stream, err := m.Git().OpenBlobAt(commit, name) | ||
| 134 | if err != nil { | ||
| 135 | continue | ||
| 136 | } | ||
| 137 | size := stream.Size | ||
| 138 | if size > maxReadmeBytes { | ||
| 139 | stream.Close() | ||
| 140 | // Name the file and the size: a README block that is silently absent | ||
| 141 | // looks like a bug in the forge rather than a decision about the file. | ||
| 142 | return template.HTML(fmt.Sprintf( | ||
| 143 | `<p class="muted">%s is %s, past the %s this page renders. <a href="/r/%s/blob/%s/%s">View the source</a>.</p>`, | ||
| 144 | template.HTMLEscapeString(name), format.Bytes(size), format.Bytes(maxReadmeBytes), | ||
| 145 | template.HTMLEscapeString(m.Name), template.HTMLEscapeString(ref), | ||
| 146 | template.HTMLEscapeString(name))), name | ||
| 147 | } | ||
| 148 | data, err := io.ReadAll(io.LimitReader(stream, maxReadmeBytes)) | ||
| 149 | stream.Close() | ||
| 150 | if err != nil { | ||
| 151 | continue | ||
| 127 | 152 | } | |
| 153 | doc := treeDoc{Repo: m.Name, Ref: ref, Path: name} | ||
| 154 | return w.renderTreeMarkdown(doc, string(data)), name | ||
| 128 | 155 | } | |
| 129 | 156 | return "", "" | |
| 130 | 157 | } | |