Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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 }