frierena personal git archive

frieren

a self-hosted git server in one binary — everyone reads, only the owner writes

fix: serve real content types for media on the raw endpoint

5a6be8ea963f9cf191e51dc163dd91f742637e55

justin06lee · Aug 18, 2026, 11:24 PM (4h ago)

e2e_test.go | 11 +++++++++++
 web.go      | 24 +++++++++++++++++++++---
 2 files changed, 32 insertions(+), 3 deletions(-)
e2e_test.go+110
@@ -149,6 +149,7 @@ func TestWebPages(t *testing.T) {
149149 os.MkdirAll(filepath.Join(dir, "docs"), 0o755)
150150 os.WriteFile(filepath.Join(dir, "README.md"), []byte("# site\nreadme body here\n"), 0o644)
151151 os.WriteFile(filepath.Join(dir, "docs", "guide.txt"), []byte("guide line one\n"), 0o644)
152+ os.WriteFile(filepath.Join(dir, "logo.svg"), []byte("<svg xmlns=\"http://www.w3.org/2000/svg\"/>\n"), 0o644)
152153 mustGit(t, dir, "add", ".")
153154 mustGit(t, dir, "commit", "-m", "add docs")
154155 mustGit(t, dir, "push", withToken(t, ts.URL+"/site.git"), "master")
@@ -182,6 +183,16 @@ func TestWebPages(t *testing.T) {
182183 t.Errorf("commit page: code %d, diff shown: %v", code, strings.Contains(body, "guide line one"))
183184 }
184185
186+ // Media files get their real content type so README embeds render.
187+ resp, err := http.Get(ts.URL + "/site/raw/master/logo.svg")
188+ if err != nil {
189+ t.Fatal(err)
190+ }
191+ resp.Body.Close()
192+ if ct := resp.Header.Get("Content-Type"); ct != "image/svg+xml" {
193+ t.Errorf("svg raw content-type = %q", ct)
194+ }
195+
185196 // Traversal and junk stay 404.
186197 for _, path := range []string{"/nope", "/site/blob/master/../../etc/passwd", "/site/raw/master/%2e%2e/x"} {
187198 if code, _ := get(t, ts.URL+path); code != http.StatusNotFound {
web.go+213
@@ -244,6 +244,20 @@ func (srv *Server) blobPage(w http.ResponseWriter, r *http.Request) {
244244 srv.render(w, "blob", data)
245245}
246246
247+var rawTypes = map[string]string{
248+ ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg",
249+ ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp",
250+ ".ico": "image/x-icon", ".avif": "image/avif", ".pdf": "application/pdf",
251+ ".mp4": "video/mp4", ".webm": "video/webm", ".mp3": "audio/mpeg",
252+}
253+
254+func extOf(path string) string {
255+ if i := strings.LastIndexByte(path, '.'); i >= 0 && !strings.ContainsRune(path[i:], '/') {
256+ return strings.ToLower(path[i:])
257+ }
258+ return ""
259+}
260+
247261func (srv *Server) rawFile(w http.ResponseWriter, r *http.Request) {
248262 repo := srv.openRepo(w, r)
249263 if repo == nil {
@@ -259,11 +273,15 @@ func (srv *Server) rawFile(w http.ResponseWriter, r *http.Request) {
259273 http.NotFound(w, r)
260274 return
261275 }
262 // Never let the browser interpret repository content as HTML.
276+ // Never let the browser interpret repository content as HTML. Known
277+ // media types get their real content type so <img>/<video> embeds work.
263278 w.Header().Set("X-Content-Type-Options", "nosniff")
264 if isBinary(blob) {
279+ switch {
280+ case rawTypes[extOf(path)] != "":
281+ w.Header().Set("Content-Type", rawTypes[extOf(path)])
282+ case isBinary(blob):
265283 w.Header().Set("Content-Type", "application/octet-stream")
266 } else {
284+ default:
267285 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
268286 }
269287 w.Write(blob)
270288