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 index.go. All files
@@ -0,0 +1,254 @@+package main++import (+ "fmt"+ "html/template"+ "net/http"+ "net/netip"+ "os"+ "path/filepath"+ "slices"+ "strings"+ "time"+)++// serveIndex lists every server on the user's addresses in the order of+// their addresses, so the page reads the same whichever server answers.+// Each server leads with its address, binary, build and start, then its+// trees. Only this server's trees carry a Delete lock button, since a stop+// must come from the server's own origin; another server's address links+// to its own index. Installs that run no server follow, so the package+// and a make install build both show. It answers at / and for paths+// outside every tree.+func (s *server) serveIndex(w http.ResponseWriter, r *http.Request, status int, msg string) {+ // msg is HTML: an errorLine, or a 404's table.+ if s.shared {+ s.serveSharedIndex(w, r, status, msg)+ return+ }+ esc := template.HTMLEscapeString+ var body strings.Builder+ body.WriteString(msg)+ servers := s.servers()+ trees := s.list()+ // One lead width for every table, so the times line up down the page.+ var sinces []string+ ownSince := make([]string, len(trees))+ for i, t := range trees {+ if t.lock != nil && debugOn {+ ownSince[i] = "since " + clock(t.lock.info.ModTime())+ }+ }+ sinces = append(sinces, ownSince...)+ for _, srv := range servers {+ for _, t := range srv.trees {+ if debugOn {+ sinces = append(sinces, "since "+clock(t.since))+ }+ }+ }+ lead := leadStyle(sinces)+ for _, srv := range servers {+ if srv.ep.IsValid() {+ addr := esc(srv.ep.String())+ if !srv.own {+ addr = "<a href=\"" + esc(origin(srv.ep)) + "/\">" + addr + "</a>"+ } else {+ addr += ", this page"+ }+ body.WriteString("<h2 class=\"own\">" + addr + "</h2>\n")+ body.WriteString("<p class=\"own\">" + srv.account() + "</p>\n")+ }+ if srv.stale {+ fmt.Fprintf(&body, "<p class=\"own warn\">The binary changed after this server started. Run <code>%s</code>.</p>\n", esc(srv.restartCmd))+ }+ if !srv.own {+ body.WriteString("<table class=\"locks own\"" + lead + ">\n")+ for _, t := range srv.trees {+ since := ""+ if debugOn {+ since = "since " + clock(t.since)+ }+ fmt.Fprintf(&body, "<tr><td>%s<a href=\"%s\" title=\"%s\">%s</a></td></tr>\n",+ sinceLead(since), esc(t.url), esc(t.lockPath), segments(tildePath(t.dir)))+ }+ body.WriteString("</table>\n")+ continue+ }+ if len(trees) == 0 {+ body.WriteString("<p class=\"own\">No tree is served.</p>\n")+ continue+ }+ body.WriteString("<table class=\"locks own\"" + lead + ">\n")+ for i, t := range trees {+ lockPath := ""+ if t.lock != nil {+ lockPath = t.lock.path+ }+ fmt.Fprintf(&body, "<tr><td>%s<a href=\"%s\" title=\"%s\">%s</a></td>"++ "<td><form method=\"post\" action=\""+ownPath+"stop\"><input type=\"hidden\" name=\"tree\" value=\"%s\">"++ "<input type=\"hidden\" name=\"from\" value=\"index\"><button>Delete lock</button></form></td></tr>\n",+ sinceLead(ownSince[i]), esc(t.prefixURL()), esc(lockPath), segments(t.label), esc(t.prefix))+ }+ body.WriteString("</table>\n")+ }+ if idle := s.idleInstalls(servers); len(idle) > 0 {+ body.WriteString("<h2 class=\"own\">No server</h2>\n")+ for _, b := range idle {+ body.WriteString("<p class=\"own\">" + b.account() + "</p>\n")+ }+ }+ if s.webURL != "" {+ fmt.Fprintf(&body, "<p class=\"own\">Online repos: <a href=\"%s/\">%s/</a></p>\n", esc(s.webURL), esc(strings.TrimPrefix(s.webURL, "http://")))+ }+ render(w, r, status, page{Title: progName, Crumbs: []crumb{{Label: "index"}}, Kind: "index",+ Body: template.HTML(body.String())})+}++// sinceLead is a tree's lock time leading its row on the index, or ""+// when the index shows none.+func sinceLead(since string) string {+ if since == "" {+ return ""+ }+ return "<span class=\"since lead\">" + template.HTMLEscapeString(since) + "</span> "+}++// indexServer is a server, or an install with none, as the index shows+// it.+type indexServer struct {+ ep netip.AddrPort // invalid for an install with no server+ own bool // the server that renders the page+ binary string // "" when its registry has no record+ since time.Time // when the server started+ built time.Time // when its binary was written+ commit string+ stale bool // the binary on disk is newer than the server+ restartCmd string+ trees []servedTree // another server's trees+}++// account is the line under a server's address: its binary, and with+// --debug the build it runs and since when. A server whose binary changed+// on disk names no build, since the file on disk is not the code it runs.+func (b indexServer) account() string {+ esc := template.HTMLEscapeString+ var parts []string+ if b.binary == "" {+ parts = append(parts, "binary not recorded")+ } else {+ parts = append(parts, "<code>"+segments(tildePath(b.binary))+"</code>")+ }+ if debugOn && !b.stale && !b.built.IsZero() {+ parts = append(parts, "built "+esc(clock(b.built))+" from "+esc(b.commit))+ }+ if debugOn && !b.since.IsZero() {+ parts = append(parts, "up since "+esc(clock(b.since)))+ }+ return strings.Join(parts, ", ")+}++// servers lists this server and every other live one on the user's+// addresses, in the order of their addresses.+func (s *server) servers() []indexServer {+ own := indexServer{ep: s.about.ep, own: true, binary: s.about.exe, since: s.about.started,+ built: built, commit: buildCommit, stale: s.about.stale(), restartCmd: s.about.restartCmd()}+ out := []indexServer{own}+ if !s.about.ep.IsValid() {+ return out+ }+ eps, _ := endpoints()+ for _, ep := range eps {+ if ep == s.about.ep {+ continue+ }+ trees := peekTrees(ep)+ if len(trees) == 0 {+ continue+ }+ slices.SortFunc(trees, func(a, b servedTree) int { return strings.Compare(a.url, b.url) })+ srv := indexServer{ep: ep, binary: binaryOf(ep), trees: trees}+ if l, ok := serverOf(ep); ok {+ srv.since = l.since+ }+ if srv.binary != "" {+ srv.built, srv.commit = binInfo(srv.binary)+ srv.stale = !srv.since.IsZero() && srv.built.After(srv.since)+ srv.restartCmd = tildePath(srv.binary) + " --restart"+ }+ out = append(out, srv)+ }+ slices.SortFunc(out, func(a, b indexServer) int { return a.ep.Compare(b.ep) })+ return out+}++// idleInstalls are the installs that run no server: the package's binary+// and the one make install links, so the index shows both whether or not+// they serve.+func (s *server) idleInstalls(servers []indexServer) []indexServer {+ if !s.about.ep.IsValid() {+ return nil+ }+ paths := []string{"/usr/bin/" + progName}+ if home, err := os.UserHomeDir(); err == nil {+ paths = append(paths, filepath.Join(home, ".local", "bin", progName))+ }+ var out []indexServer+ for _, p := range paths {+ p, err := filepath.EvalSymlinks(p)+ if err != nil || slices.ContainsFunc(servers, func(b indexServer) bool { return b.binary == p }) ||+ slices.ContainsFunc(out, func(b indexServer) bool { return b.binary == p }) {+ continue+ }+ b := indexServer{binary: p}+ b.built, b.commit = binInfo(p)+ out = append(out, b)+ }+ return out+}++// binInfo reads when a binary on disk was written and the commit it came+// from.+func binInfo(path string) (time.Time, string) {+ info, err := os.Stat(path)+ if err != nil {+ return time.Time{}, ""+ }+ return info.ModTime(), fileRev(path)+}++// serveSharedIndex lists the trees shared without a secret, by name alone:+// no lock paths, buttons or other addresses.+func (s *server) serveSharedIndex(w http.ResponseWriter, r *http.Request, status int, msg string) {+ esc := template.HTMLEscapeString+ var body strings.Builder+ body.WriteString(msg)+ trees := slices.DeleteFunc(s.list(), func(t *tree) bool { return t.secret })+ if len(trees) == 0 {+ body.WriteString("<p class=\"own\">No tree is listed here.</p>\n")+ } else {+ body.WriteString("<ul class=\"listing\">\n")+ for _, t := range trees {+ fmt.Fprintf(&body, "<li><a href=\"%s\">%s/</a></li>\n", esc(t.prefixURL()), esc(t.label))+ }+ body.WriteString("</ul>\n")+ }+ render(w, r, status, page{Title: progName, Crumbs: []crumb{{Label: "index"}}, Kind: "index",+ Body: template.HTML(body.String()), Shared: true})+}++// otherTrees reads the registries of every other address, sorted by URL.+func (s *server) otherTrees() []servedTree {+ if !s.about.ep.IsValid() {+ return nil+ }+ eps, _ := endpoints()+ var out []servedTree+ for _, ep := range eps {+ if ep != s.about.ep {+ out = append(out, peekTrees(ep)...)+ }+ }+ slices.SortFunc(out, func(a, b servedTree) int { return strings.Compare(a.url, b.url) })+ return out+}