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.
Answer byte ranges on raw downloads
26c9be338872126250f581f47c16ae5619f2cd2c · Keenan Nemetz · 9d ago · parent 1dfa8cbd
Answer byte ranges on raw downloads `/r/<name>/raw/<ref>/<path>` streamed the whole blob and ignored `Range`, so a client resuming a download or asking for a few KiB received the whole file. The artifact route already answers `206`; this does the same for repository content. A blob is streamed from `git cat-file`, which cannot seek, so a range reads and discards the bytes before it and copies only the requested part. The win is what does not cross the network: a 64 KiB range of an 8 MiB file is still 8 MiB of reading on the server, but 64 KiB of transfer instead of 8 MiB. - A single `Range` is answered with `206`, `Content-Range`, and only the requested bytes; prefix, open-ended and suffix forms all work. - A range that starts past the end is `416` with `Content-Range: bytes */<size>`. - Multiple ranges, an unknown unit, and a malformed header get the whole file with `200`, which RFC 9110 allows. - The no-range response keeps its bytes and gains `Accept-Ranges: bytes`. - `HEAD` stops after the headers instead of reading the blob. The load harness's `raw-range` scenario now reports `206`: 365.6 req/s at concurrency 4, p50 11 ms, against 265.6 req/s and p50 15 ms when the full file was sent.
server/web/codebrowser.go +33−2
| @@ -224,13 +224,44 @@ func (w *Web) blob(rw http.ResponseWriter, r *http.Request, raw bool) { | |||
| 224 | 224 | if raw { | |
| 225 | 225 | // The download is the one place that should carry every byte, so it is | |
| 226 | 226 | // streamed rather than read and written: a 100 MiB asset costs one copy | |
| 227 | // buffer per request, not 100 MiB of heap. | ||
| 227 | // buffer per request, not 100 MiB of heap. A byte range skips the bytes | ||
| 228 | // before it in the stream (git cannot seek) and copies only the requested | ||
| 229 | // part, so the win is what does not cross the network. | ||
| 228 | 230 | h := rw.Header() | |
| 229 | 231 | h.Set("Content-Type", "text/plain; charset=utf-8") | |
| 230 | h.Set("Content-Length", strconv.FormatInt(stream.Size, 10)) | ||
| 231 | 232 | // The announced length is part of the contract, so a text blob must not be | |
| 232 | 233 | // re-encoded on the way out; a compressed body has no such length. | |
| 233 | 234 | h.Set("Cache-Control", "no-transform") | |
| 235 | h.Set("Accept-Ranges", "bytes") | ||
| 236 | br, partial, unsatisfiable := parseByteRange(r.Header.Get("Range"), stream.Size) | ||
| 237 | if unsatisfiable { | ||
| 238 | h.Set("Content-Range", fmt.Sprintf("bytes */%d", stream.Size)) | ||
| 239 | rw.WriteHeader(http.StatusRequestedRangeNotSatisfiable) | ||
| 240 | return | ||
| 241 | } | ||
| 242 | if partial { | ||
| 243 | h.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", br.start, br.start+br.length-1, stream.Size)) | ||
| 244 | h.Set("Content-Length", strconv.FormatInt(br.length, 10)) | ||
| 245 | rw.WriteHeader(http.StatusPartialContent) | ||
| 246 | if r.Method == http.MethodHead { | ||
| 247 | return | ||
| 248 | } | ||
| 249 | if br.start > 0 { | ||
| 250 | if _, err := io.CopyN(io.Discard, stream, br.start); err != nil { | ||
| 251 | w.Logger("raw %s %s: skip: %v", m.Name, p, err) | ||
| 252 | return | ||
| 253 | } | ||
| 254 | } | ||
| 255 | if _, err := io.CopyN(rw, stream, br.length); err != nil { | ||
| 256 | // The client hung up mid-download; headers are already out. | ||
| 257 | w.Logger("raw %s %s: %v", m.Name, p, err) | ||
| 258 | } | ||
| 259 | return | ||
| 260 | } | ||
| 261 | h.Set("Content-Length", strconv.FormatInt(stream.Size, 10)) | ||
| 262 | if r.Method == http.MethodHead { | ||
| 263 | return | ||
| 264 | } | ||
| 234 | 265 | if _, err := io.Copy(rw, stream); err != nil { | |
| 235 | 266 | // The client hung up mid-download; headers are already out. | |
| 236 | 267 | w.Logger("raw %s %s: %v", m.Name, p, err) | |