@ 261101.7

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

index.go

new file mode 100644@@ -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+}