123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 | package render
import (
"net/url"
"path/filepath"
"strconv"
"strings"
"code.bruce-hill.com/repo-renderer/internal/config"
)
// Layout maps logical locations to paths. Each directory comes from its own
// config key (git_dir, htdocs, spool_dir, state_dir).
type Layout struct {
GitBase string // bare repositories, written by the git user
Htdocs string // the website: htdocs/<host>, served by httpd
QueueDir string // the job queue: the only place the git user writes for us
State string // render state and the lock: the renderer's alone
}
// LayoutOf is the layout a config describes.
func LayoutOf(cfg *config.Config) Layout {
return Layout{GitBase: cfg.GitDir, Htdocs: cfg.Htdocs, QueueDir: cfg.SpoolDir, State: cfg.StateDir}
}
// private is the layout of the private site whose root is htdocs: the
// same repositories and queue, and a site and state of its own, which
// keeps it apart from the public site's. httpd serves the root, and the
// site lives in its config.PrivateDir, the URL path it is served at.
func (l Layout) private(htdocs string) Layout {
l.Htdocs = filepath.Join(htdocs, config.PrivateDir)
l.State = filepath.Join(l.State, config.PrivateDir)
return l
}
func (l Layout) GitDir(repo string) string { return filepath.Join(l.GitBase, repo+".git") }
func (l Layout) QueuePath() string { return filepath.Join(l.QueueDir, "queue") }
func (l Layout) StateDir() string { return filepath.Join(l.State, "state") }
func (l Layout) StatePath(repo string) string { return filepath.Join(l.StateDir(), repo) }
func (l Layout) LockPath() string { return filepath.Join(l.State, "render.lock") }
// PubBase is the public clone export, inside the site root so
// /git/<repo>.git is served by the same httpd server block as everything
// else. (The bare repositories themselves live outside it, under GitBase.)
func (l Layout) PubBase() string { return filepath.Join(l.Htdocs, "git") }
func (l Layout) PubDir(repo string) string { return filepath.Join(l.PubBase(), repo+".git") }
func (l Layout) StaticDir() string { return filepath.Join(l.Htdocs, "static") }
func (l Layout) IndexPage() string { return filepath.Join(l.Htdocs, "index.html") }
// ErrorPage is the page httpd's errdocs shows for an HTTP status.
func (l Layout) ErrorPage(code int) string {
return filepath.Join(l.ErrDir(), strconv.Itoa(code)+".html")
}
// ErrDir holds the error pages, which httpd's errdocs names by status.
func (l Layout) ErrDir() string { return filepath.Join(l.Htdocs, "err") }
func (l Layout) NotFoundPage() string { return l.ErrorPage(404) }
func (l Layout) RobotsTxt() string { return filepath.Join(l.Htdocs, "robots.txt") }
func (l Layout) AdsTxt() string { return filepath.Join(l.Htdocs, "ads.txt") }
func (l Layout) CodeBase() string { return filepath.Join(l.Htdocs, "code") }
func (l Layout) RepoPage(repo string) string { return filepath.Join(l.CodeBase(), repo+".html") }
func (l Layout) RepoDir(repo string) string { return filepath.Join(l.CodeBase(), repo) }
func (l Layout) RawDir(repo string) string { return filepath.Join(l.Htdocs, "raw", repo) }
func (l Layout) ArchiveDir(repo string) string {
return filepath.Join(l.Htdocs, "archive", repo)
}
func (l Layout) SnapshotDir(key string) string {
return filepath.Join(l.Htdocs, "snapshot", key)
}
// SearchDir holds a view's search page, as its index.html, and the corpus
// it searches.
func (l Layout) SearchDir(key string) string { return filepath.Join(l.Htdocs, "search", key) }
func (l Layout) SearchPage(key string) string { return filepath.Join(l.SearchDir(key), "index.html") }
func (l Layout) SearchData(key string) string { return filepath.Join(l.SearchDir(key), "corpus.json") }
// Page is the HTML file for a repo-relative path; "" is the repo root.
// Directories are files too: dir "src" → code/<repo>/src.html.
func (l Layout) Page(repo, p string) string {
if p == "" {
return l.RepoPage(repo)
}
return filepath.Join(l.RepoDir(repo), filepath.FromSlash(p)+".html")
}
// IframesDir holds the documents a view's pages show in iframes.
func (l Layout) IframesDir(key string) string { return filepath.Join(l.Htdocs, "iframes", key) }
// FileTree is a view's file tree document, the drawer on every page.
func (l Layout) FileTree(key string) string { return filepath.Join(l.IframesDir(key), "filetree.html") }
// Downloads is a view's download list document: its latest tarball, the
// commit it was made from, and the repo's tags.
func (l Layout) Downloads(key string) string {
return filepath.Join(l.IframesDir(key), "downloads.html")
}
// SiteSitemap indexes the repositories' sitemaps.
func (l Layout) SiteSitemap() string { return filepath.Join(l.Htdocs, "sitemap.xml") }
// Sitemap lists the pages of a repository's default branch.
func (l Layout) Sitemap(repo string) string {
return filepath.Join(l.Htdocs, "sitemap", repo+".xml")
}
// Raw is the on-disk path of a raw file given its served name.
func (l Layout) Raw(repo, name string) string {
return filepath.Join(l.RawDir(repo), filepath.FromSlash(name))
}
// SVGDir holds derived copies of SVGs for <img> embedding: the raw copy
// stays byte-exact, the themed copy gets the theme colour injected so that
// currentColor resolves to the page foreground.
func (l Layout) SVGDir(repo string) string { return filepath.Join(l.Htdocs, "svg", repo) }
// SVG is the on-disk path of the themed copy of an SVG.
func (l Layout) SVG(repo, p string) string {
return filepath.Join(l.SVGDir(repo), filepath.FromSlash(p))
}
// TypesDir holds the pages listing a view's files by type.
func (l Layout) TypesDir(repo string) string { return filepath.Join(l.Htdocs, "types", repo) }
// TypePage lists the files of one type in a size summary row.
func (l Layout) TypePage(repo, row, slug string) string {
return filepath.Join(l.TypesDir(repo), row, slug+".html")
}
// Archive is the tarball path for a (sanitized) tag.
func (l Layout) Archive(repo, tag string) string {
return filepath.Join(l.ArchiveDir(repo), repo+"-"+tag+".tar.gz")
}
// TipArchive is the path of a view's branch-tip tarball named by
// tipArchiveName. They have a directory of their own, apart from the tag
// tarballs, so httpd can cache them for good by their path alone.
func (l Layout) TipArchive(key, name string) string {
return filepath.Join(l.SnapshotDir(key), name)
}
// tipArchiveName names the tarball of a branch tip after its commit, so
// its contents never change and it can be cached for good.
func tipArchiveName(repo, sha string) string {
return repo + "@" + sha[:12] + ".tar.gz"
}
// ---- URLs ----
// CodeURL is the URL of a file or directory page.
func CodeURL(repo, p string) string {
if p == "" {
return "/code/" + repo
}
return "/code/" + repo + "/" + escapePath(p)
}
// FileTreeURL is the URL of a view's file tree document.
func FileTreeURL(key string) string { return "/iframes/" + key + "/filetree.html" }
// DownloadsURL is the URL of a view's download list document.
func DownloadsURL(key string) string { return "/iframes/" + key + "/downloads.html" }
// SitemapURL is the URL of a repository's sitemap.
func SitemapURL(repo string) string { return "/sitemap/" + repo + ".xml" }
// RawURL is the URL of a raw file given its served name.
func RawURL(repo, name string) string {
return "/raw/" + repo + "/" + escapePath(name)
}
// SVGURL is the URL of the themed copy of an SVG.
func SVGURL(repo, p string) string {
return "/svg/" + repo + "/" + escapePath(p)
}
// TypeURL is the URL of the page listing the files of one type.
func TypeURL(repo, row, slug string) string { return "/types/" + repo + "/" + row + "/" + slug }
// SearchURL is the URL of a view's search page: its directory, which httpd
// serves the index.html of. Without the slash, httpd redirects and drops
// the query, so the search form must submit here.
func SearchURL(key string) string { return "/search/" + key + "/" }
// SearchDataURL is the URL of a view's search corpus.
func SearchDataURL(key string) string { return "/search/" + key + "/corpus.json" }
// ArchiveURL is the URL of a tag tarball.
func ArchiveURL(repo, tag string) string {
return "/archive/" + repo + "/" + escapePath(repo+"-"+tag+".tar.gz")
}
// TipArchiveURL is the URL of a view's branch-tip tarball.
func TipArchiveURL(key, name string) string {
return "/snapshot/" + key + "/" + name
}
// escapePath percent-encodes each segment of a slash-separated path.
func escapePath(p string) string {
segs := strings.Split(p, "/")
for i, s := range segs {
segs[i] = url.PathEscape(s)
}
return strings.Join(segs, "/")
}
// ValidPath rejects tree paths that could escape the output directory or
// break the filesystem: "..", empty components, leading "/", control chars.
func ValidPath(p string) bool {
if p == "" || p[0] == '/' {
return false
}
for _, c := range p {
if c < 0x20 || c == 0x7f {
return false
}
}
for comp := range strings.SplitSeq(p, "/") {
if comp == "" || comp == "." || comp == ".." {
return false
}
}
return true
}
// ---- URLs on a site ----
// A site's URLs are those above under its prefix: "" for the public site,
// config.PrivatePrefix for the private one.
func (s *Site) CodeURL(repo, p string) string { return s.prefix + CodeURL(repo, p) }
func (s *Site) FileTreeURL(key string) string { return s.prefix + FileTreeURL(key) }
func (s *Site) DownloadsURL(key string) string { return s.prefix + DownloadsURL(key) }
func (s *Site) RawURL(repo, name string) string { return s.prefix + RawURL(repo, name) }
func (s *Site) SVGURL(repo, p string) string { return s.prefix + SVGURL(repo, p) }
func (s *Site) SearchURL(key string) string { return s.prefix + SearchURL(key) }
func (s *Site) SearchDataURL(key string) string { return s.prefix + SearchDataURL(key) }
func (s *Site) ArchiveURL(repo, tag string) string { return s.prefix + ArchiveURL(repo, tag) }
func (s *Site) TipArchiveURL(key, name string) string {
return s.prefix + TipArchiveURL(key, name)
}
func (s *Site) TypeURL(repo, row, slug string) string {
return s.prefix + TypeURL(repo, row, slug)
}
|