261101.1 · 0ad750f7e905ca3494043be38b127f94a06eb874
server.go splits by purpose
server.go held the server's identity, request guarding, the index, tree pages and page rendering. Each now has a file whose name says what is in it: about.go, guard.go, index.go, treepage.go and page.go. bugOff joins bug.go. server.go keeps the server and tree types, mounting, the embedded assets, the handler and the stop button.
Declarations moved unchanged, as a sorted comparison of every line outside the import blocks confirms. Follow them with git log --follow or blame -C.
reading declarations
Changes to treepage.go. All files
@@ -0,0 +1,342 @@+package main++import (+ "bytes"+ "errors"+ "fmt"+ "html/template"+ "io"+ "io/fs"+ "net/http"+ "net/url"+ "path"+ "slices"+ "strings"+ "unicode/utf8"+)++// maxCode caps the size of a file shown as a code page; larger files go raw.+const maxCode = 1 << 20++// name is the file below the tree that urlPath names, or "." for its root.+func (t *tree) name(urlPath string) string {+ rest := strings.TrimPrefix(urlPath, t.prefix)+ name := strings.TrimPrefix(path.Clean("/"+rest), "/")+ if name == "" {+ return "."+ }+ return name+}++func (t *tree) serve(w http.ResponseWriter, r *http.Request) {+ if first, rest, ok := t.versionIn(r.URL.Path); ok {+ t.serveVersion(w, r, first, rest)+ return+ }+ name := t.name(r.URL.Path)+ // A first segment such as @5430ac3 named a version before versions+ // moved beside the tree's name, unless the tree holds an entry by that+ // name. It moves there for good, so old links still lead to it.+ if first, rest, _ := strings.Cut(name, "/"); t.hasVersions() && atSegment.MatchString(first) {+ if _, err := fs.Stat(t.fsys, first); err != nil {+ if t.prefix == "/" {+ t.serveVersion(w, r, first, rest)+ return+ }+ u := (&url.URL{Path: versionPrefix(t.prefix, first) + rest}).String()+ if strings.HasSuffix(r.URL.Path, "/") && rest != "" {+ u += "/"+ }+ if r.URL.RawQuery != "" {+ u += "?" + r.URL.RawQuery+ }+ http.Redirect(w, r, u, http.StatusMovedPermanently)+ return+ }+ }+ info, err := fs.Stat(t.fsys, name)+ if ref := (*refusal)(nil); errors.As(err, &ref) {+ t.fail(w, r, name, http.StatusForbidden, t.why(ref))+ return+ }+ if err != nil {+ t.notFound(w, r, name)+ return+ }+ if info.IsDir() && !strings.HasSuffix(r.URL.Path, "/") {+ // Without the slash, relative links in the listing resolve one level up.+ u := r.URL.EscapedPath() + "/"+ if r.URL.RawQuery != "" {+ u += "?" + r.URL.RawQuery+ }+ http.Redirect(w, r, u, http.StatusMovedPermanently)+ return+ }+ if q, ok := r.URL.Query()["search"]; ok && info.IsDir() {+ t.serveSearch(w, r, name, q[0])+ return+ }+ if _, ok := r.URL.Query()["backlinks"]; ok && !info.IsDir() {+ t.serveBacklinks(w, r, name)+ return+ }+ if _, log := r.URL.Query()["log"]; log {+ t.serveLog(w, r, name, info.IsDir())+ return+ }+ if _, ok := r.URL.Query()["timeline"]; ok {+ t.serveTimeline(w, r, name, info.IsDir())+ return+ }+ if hash, ok := r.URL.Query()["commit"]; ok {+ if q := r.URL.Query(); q.Has("decl") {+ t.serveDecl(w, r, name, info.IsDir(), hash[0], q.Get("file"), q.Get("decl"))+ return+ }+ t.serveCommit(w, r, name, info.IsDir(), hash[0])+ return+ }+ if info.IsDir() {+ t.serveDir(w, r, name)+ return+ }+ if _, raw := r.URL.Query()["raw"]; raw {+ t.serveRaw(w, r, name, info)+ return+ }+ switch {+ case isMarkdown(name):+ t.serveMarkdown(w, r, name)+ case info.Size() <= maxCode:+ t.serveCode(w, r, name, info)+ default:+ t.serveRaw(w, r, name, info)+ }+}++func isMarkdown(name string) bool {+ switch strings.ToLower(path.Ext(name)) {+ case ".md", ".markdown":+ return true+ }+ return false+}++func isText(b []byte) bool {+ return utf8.Valid(b) && bytes.IndexByte(b, 0) < 0+}++func (t *tree) serveMarkdown(w http.ResponseWriter, r *http.Request, name string) {+ b, err := fs.ReadFile(t.fsys, name)+ if err != nil {+ t.fail(w, r, name, http.StatusInternalServerError, err.Error())+ return+ }+ body, err := renderMarkdown(b, t.missing(name), t.webBase())+ if err != nil {+ t.fail(w, r, name, http.StatusInternalServerError, err.Error())+ return+ }+ body = template.HTML(t.logLine(r.Context(), name, b)) + body+ t.render(w, r, http.StatusOK, page{Title: name, Crumbs: t.crumbs(name), Kind: "doc", Body: body, Raw: true})+}++// webBase is where t's docs send a link to a forge: the web address, ""+// on the web address itself, or nil where no server here serves it, eg on+// a share.+func (t *tree) webBase() *string {+ if t.base != nil {+ return t.base.webBase()+ }+ switch s := t.srv; {+ case s == nil:+ return nil+ case s.web != nil:+ return new(string)+ case s.webURL != "":+ return &s.webURL+ }+ return nil+}++// missing reports, for the doc name, whether a relative link names+// nothing the reader could open. A link that climbs out of the tree may+// land in another tree, so it counts as found.+func (t *tree) missing(name string) func(string) bool {+ dir := path.Dir(name)+ return func(dest string) bool {+ target := path.Join(dir, dest)+ if target == ".." || strings.HasPrefix(target, "../") {+ return false+ }+ _, err := fs.Stat(t.fsys, target)+ return errors.Is(err, fs.ErrNotExist)+ }+}++// serveCode shows a text file as a highlighted code page with numbered,+// linkable lines; ?scopes draws each block of code as a box, ?tree+// shows the syntax tree, and ?gron shows a JSON file as one line per node.+// Binary files go raw.+func (t *tree) serveCode(w http.ResponseWriter, r *http.Request, name string, info fs.FileInfo) {+ b, err := fs.ReadFile(t.fsys, name)+ if err != nil {+ t.fail(w, r, name, http.StatusInternalServerError, err.Error())+ return+ }+ if !isText(b) {+ t.serveRaw(w, r, name, info)+ return+ }+ var body strings.Builder+ body.WriteString(t.logLine(r.Context(), name, b))+ // The links to the other views and to the raw file float over the+ // code's top right corner; div.code bounds how far they follow the+ // page down. A tree that cannot be had says why above the flat code.+ q := r.URL.Query()+ _, scoped := q["scopes"]+ if _, ok := q["gron"]; ok && isJSON(name) {+ out, err := gron(b)+ if err == nil {+ body.WriteString(`<div class="code">` + codeViews(name, "gron") + highlightPage(gronLexer, out, false) + `</div>`)+ t.render(w, r, http.StatusOK, page{Title: name, Crumbs: t.crumbs(name), Kind: "code", Body: template.HTML(body.String())})+ return+ }+ body.WriteString("<p class=\"own warn\">No gron view, since this is not valid JSON: " + template.HTMLEscapeString(err.Error()) + "</p>\n")+ }+ if _, ok := q["tree"]; ok {+ tree, err := syntaxPage(r.Context(), name, pageHref(r.URL.Path), b)+ if err == nil {+ // A large file's tree is megabytes, so it is written once,+ // into room made for it.+ body.Grow(len(tree) + 1024)+ body.WriteString(`<div class="code">` + codeViews(name, "tree"))+ body.WriteString(tree)+ body.WriteString(`</div>`)+ t.render(w, r, http.StatusOK, page{Title: name, Crumbs: t.crumbs(name), Kind: "tree", Body: template.HTML(body.String())})+ return+ }+ logErr("no syntax tree for %s: %v", name, err)+ body.WriteString(treeNote(name, err))+ }+ view := "flat"+ if scoped {+ view = "scopes"+ }+ body.WriteString(`<div class="code">` + codeViews(name, view))+ body.WriteString(highlightPage(codeLexer(name), b, scoped))+ body.WriteString(`</div>`)+ t.render(w, r, http.StatusOK, page{Title: name, Crumbs: t.crumbs(name), Kind: "code", Body: template.HTML(body.String())})+}++// serveRaw sends the file itself. Text goes as plain text, so a stray .html+// or .ts file shows as source instead of rendering or downloading.+func (t *tree) serveRaw(w http.ResponseWriter, r *http.Request, name string, info fs.FileInfo) {+ f, err := t.fsys.Open(name)+ if err != nil {+ t.notFound(w, r, name)+ return+ }+ defer f.Close()+ rs, ok := f.(io.ReadSeeker)+ if !ok {+ t.fail(w, r, name, http.StatusInternalServerError, "file is not seekable")+ return+ }+ if strings.ToLower(path.Ext(name)) != ".svg" {+ head := make([]byte, 512)+ n, _ := io.ReadFull(rs, head)+ if isText(head[:n]) {+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")+ }+ rs.Seek(0, io.SeekStart)+ }+ http.ServeContent(w, r, name, info.ModTime(), rs)+}++func (t *tree) serveDir(w http.ResponseWriter, r *http.Request, name string) {+ entries, err := fs.ReadDir(t.fsys, name)+ if err != nil {+ t.fail(w, r, name, http.StatusInternalServerError, err.Error())+ return+ }+ type item struct {+ name string+ dir bool+ ref *refusal+ }+ // Entries reached through a link out of the tree are checked one by+ // one, since each may be refused. So is every entry of a shared tree.+ // A version holds only what git stored, so nothing leads out of it.+ outside := false+ if t.at == nil {+ _, err = t.root.Stat(name)+ outside = err != nil || t.shared+ }+ var items []item+ for _, e := range entries {+ n := e.Name()+ if t.unlisted(name, n) {+ continue+ }+ dir := e.IsDir()+ var ref *refusal+ if e.Type()&fs.ModeSymlink != 0 || outside {+ ref = t.refused(path.Join(name, n))+ if ref != nil {+ if ref.link == path.Join(name, n) {+ dir = ref.dir+ }+ } else if info, err := fs.Stat(t.fsys, path.Join(name, n)); err == nil {+ dir = info.IsDir()+ }+ }+ items = append(items, item{n, dir, ref})+ }+ slices.SortStableFunc(items, func(a, b item) int {+ if a.dir != b.dir {+ if a.dir {+ return -1+ }+ return 1+ }+ return strings.Compare(a.name, b.name)+ })++ var body strings.Builder+ if t.at != nil {+ body.WriteString(t.versionLine(r.Context(), name, true))+ }+ body.WriteString("<ul class=\"listing\">\n")+ if t.lastCommit(r.Context(), name) != nil {+ body.WriteString("<li class=\"log\"><a href=\"?log\">" + template.HTMLEscapeString(logLabel) + "</a></li>\n")+ }+ if name != "." {+ body.WriteString("<li><a href=\"../\">../</a></li>\n")+ }+ for _, it := range items {+ label := it.name+ if it.dir {+ label += "/"+ }+ href := (&url.URL{Path: label}).String()+ if it.ref != nil && (t.shared || t.public) {+ fmt.Fprintf(&body, "<li class=\"refused\">%s <span class=\"own\">refused</span></li>\n", template.HTMLEscapeString(label))+ continue+ }+ if it.ref != nil {+ fmt.Fprintf(&body, "<li class=\"refused\"><a href=\"%s\" title=\"%s\">%s</a> <span class=\"own\">refused</span></li>\n",+ template.HTMLEscapeString(href), template.HTMLEscapeString(it.ref.Error()), template.HTMLEscapeString(label))+ continue+ }+ fmt.Fprintf(&body, "<li><a href=\"%s\">%s</a></li>\n",+ template.HTMLEscapeString(href), template.HTMLEscapeString(label))+ }+ body.WriteString("</ul>\n")+ t.render(w, r, http.StatusOK, page{+ Title: t.title(name),+ Crumbs: t.crumbs(name),+ Kind: "dir",+ Body: template.HTML(body.String()),+ })+}