diff options
Diffstat (limited to 'spartan.go')
-rw-r--r-- | spartan.go | 145 |
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) + }) +} |