v0.2.0 41cf2ec

raw.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6	"context"
7	"mime"
8	"net/http"
9	"path"
10	"strconv"
11	"time"
12
13	"github.com/bnema/zerowrap"
14)
15
16// rawTimeout bounds the stream of one file to the visitor.
17const rawTimeout = 5 * time.Minute
18
19// rawCSP makes a raw file inert if a browser opens it as a document: it can
20// load nothing and run nothing, whatever it contains.
21const rawCSP = "default-src 'none'; sandbox"
22
23// raw streams the content of a file. Text is served as text/plain, the four
24// image types (by their bytes, never SVG) as images, and anything else as an
25// attachment of unknown type. A file is never served as HTML or SVG, so what
26// a repository holds cannot run in the origin of the forge.
27func (s *Server) raw(w http.ResponseWriter, r *http.Request) {
28	ctx, cancel := context.WithTimeout(r.Context(), rawTimeout)
29	defer cancel()
30	b, t, repo, err := s.rawOpen(ctx, r)
31	if err != nil {
32		s.fail(ctx, w, r, err)
33		return
34	}
35	defer b.close()
36
37	// The type needs the first bytes; headers must not be sent before.
38	if err := b.fill(512); err != nil {
39		s.fail(ctx, w, r, err)
40		return
41	}
42	typ, attach := rawType(b)
43	h := w.Header()
44	h.Set("Content-Type", typ)
45	h.Set("X-Content-Type-Options", "nosniff")
46	h.Set("Content-Security-Policy", rawCSP)
47	h.Set("Content-Length", strconv.FormatInt(b.info.GetSize(), 10))
48	if attach {
49		name := path.Base(t.path)
50		cd := mime.FormatMediaType("attachment", map[string]string{"filename": name})
51		if cd == "" {
52			cd = "attachment"
53		}
54		h.Set("Content-Disposition", cd)
55	}
56	if etag := b.info.GetId(); etag != "" {
57		h.Set("ETag", `"`+etag+`"`)
58	}
59	// Revalidated every time, even at a full commit id: a repository can turn
60	// private, and a shared cache must not keep serving its files (AC2). The
61	// ETag keeps the revalidation cheap.
62	cacheHeaders(ctx, h, "no-cache", sessionCache)
63	if match := r.Header.Get("If-None-Match"); match != "" && h.Get("ETag") != "" && match == h.Get("ETag") {
64		w.WriteHeader(http.StatusNotModified)
65		return
66	}
67	w.WriteHeader(http.StatusOK)
68	if r.Method == http.MethodHead {
69		return
70	}
71	if _, err := w.Write(b.buf); err != nil {
72		return
73	}
74	b.buf = nil
75	for b.stream.Receive() {
76		if _, err := w.Write(b.stream.Msg().GetChunk()); err != nil {
77			return
78		}
79	}
80	if err := b.stream.Err(); err != nil {
81		// Headers are gone: all that is left is to cut the answer short, so
82		// that the client sees it is incomplete.
83		log := zerowrap.FromCtx(ctx)
84		log.Error().Err(err).Str("repo", repo).Str("path", r.URL.Path).Msg("raw stream failed")
85		panic(http.ErrAbortHandler)
86	}
87}
88
89// rawOpen finds the file a raw URL designates and opens its stream.
90func (s *Server) rawOpen(ctx context.Context, r *http.Request) (*blobStream, target, string, error) {
91	d, err := s.loadRepo(ctx, r.PathValue("repo"))
92	if err != nil {
93		return nil, target{}, "", err
94	}
95	t, ok := d.locate(r.PathValue("rest"))
96	if !ok {
97		return nil, target{}, "", errNotFound
98	}
99	b, err := s.openBlob(ctx, d.name(), t)
100	if err != nil {
101		return nil, target{}, "", err
102	}
103	return b, t, d.name(), nil
104}
105
106// rawType is the Content-Type of a file and whether it is an attachment.
107// b.buf holds the first bytes (up to 512 when the file has that many).
108func rawType(b *blobStream) (typ string, attachment bool) {
109	if img := sniffImage(b.buf); img != "" {
110		return img, false
111	}
112	if !b.info.GetBinary() {
113		return "text/plain; charset=utf-8", false
114	}
115	return "application/octet-stream", true
116}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close