package main
import (
"fmt"
"html/template"
"net/http"
"net/url"
"path"
"slices"
"strings"
"time"
)
// crumbs link the root and each segment of a path that no tree serves.
func webCrumbs(key string) []crumb {
out := []crumb{indexCrumb}
segs := strings.Split(key, "/")
for i, s := range segs {
c := crumb{Label: s}
if i < len(segs)-1 {
c.Href = (&url.URL{Path: "/" + strings.Join(segs[:i+1], "/") + "/"}).String()
}
out = append(out, c)
}
return out
}
func (w *web) render(rw http.ResponseWriter, r *http.Request, status int, title string, crumbs []crumb, kind string, body string) {
render(rw, r, status, page{Title: title, Crumbs: crumbs, Kind: kind, Body: template.HTML(body), NoStop: true})
}
func (w *web) fail(rw http.ResponseWriter, r *http.Request, status int, msg string) {
w.render(rw, r, status, "not found", []crumb{indexCrumb, {Label: "not found"}}, "error",
"<pre class=\"error\">"+template.HTMLEscapeString(msg)+"</pre>")
}
// serveRoot lists the cache: each repo, its size and its last request. A
// public server says only how to read a repo.
func (w *web) serveRoot(rw http.ResponseWriter, r *http.Request) {
esc := template.HTMLEscapeString
repos := w.onDisk()
slices.SortFunc(repos, func(a, b cachedRepo) int { return strings.Compare(a.key, b.key) })
var total int64
var body strings.Builder
body.WriteString("<p class=\"own\">Type a forge's host and a path after this address, eg <a href=\"/github.com/junegunn/fzf/\">/github.com/junegunn/fzf</a>, to read that repo, or <a href=\"/git.suckless.org/\">/git.suckless.org</a> to list a host's repos.</p>\n")
if w.public {
w.render(rw, r, http.StatusOK, "web", []crumb{{Label: "index"}}, "index", body.String())
return
}
if len(repos) > 0 {
var sizes []string
for _, c := range repos {
sizes = append(sizes, sizeText(c.size))
}
// The size leads, aligned right as numbers are; when it was
// read follows the name.
body.WriteString("<table class=\"locks own\"" + leadStyle(sizes) + ">\n")
for i, c := range repos {
total += c.size
fmt.Fprintf(&body, "<tr><td><span class=\"since lead num\">%s</span> <a href=\"%s\">%s</a> <span class=\"since\">last read %s</span></td></tr>\n",
sizes[i], esc((&url.URL{Path: "/" + c.key + "/"}).String()), segments(c.key), clock(c.used))
}
body.WriteString("</table>\n")
}
fmt.Fprintf(&body, "<p class=\"own\">The cache in %s holds %d repos in %s of its %s.</p>\n", esc(tildePath(w.root)), len(repos), sizeText(total), sizeText(w.limit))
if u := w.srv.homeURL; u != "" {
fmt.Fprintf(&body, "<p class=\"own\">Trees: <a href=\"%s/\">%s/</a></p>\n", esc(u), esc(strings.TrimPrefix(u, "http://")))
}
w.render(rw, r, http.StatusOK, "web", []crumb{{Label: "index"}}, "index", body.String())
}
// serveJob shows what a visit set going: the reading page.
func (w *web) serveJob(rw http.ResponseWriter, r *http.Request, j *webJob) {
esc := template.HTMLEscapeString
w.mu.Lock()
steps, line, failed, started := slices.Clone(j.steps), j.line, j.err, j.started
w.mu.Unlock()
var body strings.Builder
status := http.StatusAccepted
if failed != "" {
status = http.StatusBadGateway
body.WriteString("<pre class=\"error\">" + esc(failed) + "</pre>\n")
} else {
body.WriteString("<p class=\"own reading\">reading</p>\n")
}
body.WriteString("<pre class=\"own steps\">")
for _, s := range steps {
body.WriteString(esc(s) + "\n")
}
if line != "" {
body.WriteString(esc(line) + "\n")
}
body.WriteString("</pre>\n")
fmt.Fprintf(&body, "<p class=\"own\">Started %s, %d s ago.", clock(started), int(time.Since(started).Seconds()))
if failed != "" {
body.WriteString(" Reload after 30 s to try again.")
} else {
body.WriteString(" The page turns into the repo when it is read.")
}
body.WriteString("</p>\n")
key := strings.TrimPrefix(j.urlPath, "/")
w.render(rw, r, status, "reading "+key, webCrumbs(key), "reading", body.String())
}
// serveConfirm asks before a download that another site set going.
func (w *web) serveConfirm(rw http.ResponseWriter, r *http.Request, key string) {
esc := template.HTMLEscapeString
u := (&url.URL{Path: "/" + key}).String()
body := fmt.Sprintf("<p class=\"own\">Another site sent you here, so nothing is fetched yet. <a href=\"%s\">Read %s</a> fetches it from %s into the cache.</p>\n",
esc(u), esc(key), esc(webScheme+"://"+key))
// The page runs the script, which carries the link's fragment, eg
// #L10-L12, onto the read link, but holds no stream: a reload must
// not fetch what the user has not asked for.
render(rw, r, http.StatusOK, page{Title: key, Crumbs: webCrumbs(key), Kind: "confirm", Body: template.HTML(body), NoStop: true, Live: "-", Fixed: true})
}
// serveListing lists a host's or account's repos, and names its source.
func (w *web) serveListing(rw http.ResponseWriter, r *http.Request, l *listing) {
esc := template.HTMLEscapeString
key := strings.Trim(path.Clean(r.URL.Path), "/")
var body strings.Builder
status := http.StatusOK
if len(l.repos) == 0 {
status = http.StatusNotFound
fmt.Fprintf(&body, "<pre class=\"error\">No repo answers at %s, and nothing lists one there.</pre>\n", esc(webScheme+"://"+key))
} else {
body.WriteString("<ul class=\"listing\">\n")
for _, e := range l.repos {
name := strings.TrimPrefix(e.key, key+"/")
fmt.Fprintf(&body, "<li><a href=\"%s\">%s/</a>", esc((&url.URL{Path: "/" + e.key + "/"}).String()), esc(name))
if e.desc != "" {
fmt.Fprintf(&body, " <span class=\"own desc\">%s</span>", esc(e.desc))
}
body.WriteString("</li>\n")
}
body.WriteString("</ul>\n")
}
src := l.source
if l.asked != "" {
src += ", " + l.asked
}
fmt.Fprintf(&body, "<p class=\"own\">%d repos, listed by %s at %s.", len(l.repos), esc(src), clock(l.at))
if l.source == "the cache" {
body.WriteString(" The cache holds only what was read here, so the host may hold more.")
}
body.WriteString("</p>\n")
if len(l.failed) > 0 {
body.WriteString("<p class=\"own\">Failed first: " + esc(strings.Join(l.failed, "; ")) + ".</p>\n")
}
w.render(rw, r, status, key, webCrumbs(key), "index", body.String())
}