summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortjpcc <tjp@ctrl-c.club>2023-10-30 17:23:40 -0600
committertjpcc <tjp@ctrl-c.club>2023-10-30 17:23:40 -0600
commitbbf702618435526e7e1a8554cdddfe0e2258120e (patch)
treeb9af08753ffa216240851a81c3cb5935d11fdf7c
parentdc1eeb2460e920fdacfc263741bc191b1422c647 (diff)
add spartan support
-rw-r--r--spartan.go145
1 files changed, 145 insertions, 0 deletions
diff --git a/spartan.go b/spartan.go
new file mode 100644
index 0000000..beb001e
--- /dev/null
+++ b/spartan.go
@@ -0,0 +1,145 @@
+package syw
+
+import (
+ "bytes"
+ "context"
+ "mime"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "text/template"
+
+ "tildegit.org/tjp/sliderule"
+ "tildegit.org/tjp/sliderule/spartan"
+)
+
+// SpartanRouter builds a router that will handle requests into a directory of git repositories.
+//
+// The routes it defines are:
+//
+// / gemtext listing of the repositories in the directory
+// /:repository/ gemtext overview of the repository
+// /:repository/branches gemtext list of branches/heads
+// /:repository/tags gemtext listing of tags
+// /:repository/refs/:ref/ gemtext overview of a ref
+// /:repository/refs/:ref/tree/*path gemtext listing of directories, raw files
+// /:repository/diffstat/:fromref/:toref text/plain diffstat between two refs
+// /:repository/diff/:fromref/:toref text/x-diff between two refs
+//
+// The overrides argument can provide templates to define the behavior of nearly all of the above
+// routes. All of them have default implementations so the argument can even be nil, but otherwise
+// the template names used are:
+//
+// repo_root.gmi gemtext at /
+// repo_home.gmi gemtext at /:repository/
+// branch_list.gmi gemtext at /:repository/branches
+// tag_list.gmi gemtext at /:repository/tags
+// ref.gmi gemtext at /:repository/refs/:ref/
+// tree.gmi gemtext for directories requested under /:repository/refs/:ref/tree/*path
+// (file paths return the raw files without any template involved)
+// diffstat.gmi.txt the plaintext diffstat at /:repository/diffstat/:fromref/:toref
+// diff.gmi.txt the text/x-diff at /:repository/diff/:fromref/:toref
+//
+// Most of the templates above are rendered with an object with 3 fields:
+//
+// Ctx: the context.Context from the request
+// Repo: a *syw.Repository object corresponding to <repodir>/:repository
+// Params: a map[string]string of the route parameters
+//
+// The only exception is repo_root.gmi, which is rendered with a slice of the repo names instead.
+func SpartanRouter(repodir string, overrides *template.Template) *sliderule.Router {
+ tmpl, err := addTemplates(geminiTemplate, overrides)
+ if err != nil {
+ panic(err)
+ }
+
+ repoRouter := &sliderule.Router{}
+ repoRouter.Use(assignRepo(repodir))
+ repoRouter.Route("/", sgmiTemplate(tmpl, "repo_home.gmi"))
+ repoRouter.Route("/branches", sgmiTemplate(tmpl, "branch_list.gmi"))
+ repoRouter.Route("/tags", sgmiTemplate(tmpl, "tag_list.gmi"))
+ repoRouter.Route("/refs/:ref/", sgmiTemplate(tmpl, "ref.gmi"))
+ repoRouter.Route("/refs/:ref/tree/*path", spartanTreePath(tmpl))
+ repoRouter.Route("/diffstat/:fromref/:toref", runSpartanTemplate(tmpl, "diffstat.gmi.txt", "text/plain"))
+ repoRouter.Route("/diff/:fromref/:toref", runSpartanTemplate(tmpl, "diff.gmi.txt", "text/x-diff"))
+
+ router := &sliderule.Router{}
+ router.Route("/", spartanRoot(repodir, tmpl))
+ router.Mount("/:"+reponamekey, repoRouter)
+
+ return router
+}
+
+func spartanRoot(repodir string, tmpl *template.Template) sliderule.Handler {
+ return sliderule.HandlerFunc(func(ctx context.Context, request *sliderule.Request) *sliderule.Response {
+ entries, err := os.ReadDir(repodir)
+ if err != nil {
+ return spartan.ServerError(err)
+ }
+
+ names := []string{}
+ for _, item := range entries {
+ if Open(filepath.Join(repodir, item.Name())) != nil {
+ names = append(names, item.Name())
+ }
+ }
+
+ buf := &bytes.Buffer{}
+ if err := tmpl.ExecuteTemplate(buf, "repo_root.gmi", names); err != nil {
+ return spartan.ServerError(err)
+ }
+
+ return spartan.Success("text/gemini; charset=utf-8", buf)
+ })
+}
+
+func spartanTreePath(tmpl *template.Template) sliderule.Handler {
+ return sliderule.HandlerFunc(func(ctx context.Context, request *sliderule.Request) *sliderule.Response {
+ params := sliderule.RouteParams(ctx)
+ if params["path"] == "" || strings.HasSuffix(params["path"], "/") {
+ return sgmiTemplate(tmpl, "tree.gmi").Handle(ctx, request)
+ }
+
+ repo := ctx.Value(repokey).(*Repository)
+
+ body, err := repo.Blob(ctx, params["ref"], params["path"])
+ if err != nil {
+ return spartan.ServerError(err)
+ }
+
+ mediaType := ""
+ ext := path.Ext(params["path"])
+ if ext == ".gmi" {
+ mediaType = "text/gemini; charset=utf-8"
+ } else {
+ mediaType = mime.TypeByExtension(ext)
+ }
+ if mediaType == "" {
+ mediaType = "application/octet-stream"
+ }
+
+ return spartan.Success(mediaType, bytes.NewBuffer(body))
+ })
+}
+
+func sgmiTemplate(tmpl *template.Template, name string) sliderule.Handler {
+ return runSpartanTemplate(tmpl, name, "text/gemini; charset=utf-8")
+}
+
+func runSpartanTemplate(tmpl *template.Template, name, mimetype string) sliderule.Handler {
+ return sliderule.HandlerFunc(func(ctx context.Context, request *sliderule.Request) *sliderule.Response {
+ obj := map[string]any{
+ "Ctx": ctx,
+ "Repo": ctx.Value(repokey),
+ "Params": sliderule.RouteParams(ctx),
+ }
+ buf := &bytes.Buffer{}
+
+ if err := tmpl.ExecuteTemplate(buf, name, obj); err != nil {
+ return spartan.ServerError(err)
+ }
+
+ return spartan.Success(mimetype, buf)
+ })
+}