summaryrefslogtreecommitdiff
path: root/gemini.go
diff options
context:
space:
mode:
authortjpcc <tjp@ctrl-c.club>2023-09-16 20:43:19 -0600
committertjpcc <tjp@ctrl-c.club>2023-09-16 20:43:19 -0600
commit90fa2795ad177b9add2fe46382576993e96ece4b (patch)
tree0e2847f8694fa66a914680329c4515101b5a45ab /gemini.go
Initial commit
Gemini repository browsing
Diffstat (limited to 'gemini.go')
-rw-r--r--gemini.go118
1 files changed, 118 insertions, 0 deletions
diff --git a/gemini.go b/gemini.go
new file mode 100644
index 0000000..6222684
--- /dev/null
+++ b/gemini.go
@@ -0,0 +1,118 @@
+package syw
+
+import (
+ "bytes"
+ "context"
+ "mime"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "text/template"
+
+ "tildegit.org/tjp/sliderule"
+ "tildegit.org/tjp/sliderule/gemini"
+)
+
+const (
+ repokey = "syw_repo"
+ reponamekey = "syw_reponame"
+)
+
+func GeminiRouter(repodir string) *sliderule.Router {
+ repoRouter := &sliderule.Router{}
+ repoRouter.Use(assignRepo(repodir))
+ repoRouter.Route("/", gmiTemplate(geminiTemplate, "repo_home.gmi"))
+ repoRouter.Route("/branches", gmiTemplate(geminiTemplate, "branch_list.gmi"))
+ repoRouter.Route("/tags", gmiTemplate(geminiTemplate, "tag_list.gmi"))
+ repoRouter.Route("/refs/:ref/", gmiTemplate(geminiTemplate, "ref.gmi"))
+ repoRouter.Route("/refs/:ref/tree/*path", sliderule.HandlerFunc(geminiTreePath))
+ repoRouter.Route("/diffstat/:fromref/:toref", runTemplate(geminiTemplate, "diffstat.gmi", "text/plain"))
+ repoRouter.Route("/diff/:fromref/:toref", runTemplate(geminiTemplate, "diff.gmi", "text/x-diff"))
+
+ router := &sliderule.Router{}
+ router.Route("/", geminiRoot(repodir))
+ router.Mount("/:"+reponamekey, repoRouter)
+
+ return router
+}
+
+func assignRepo(repodir string) sliderule.Middleware {
+ return func(h sliderule.Handler) sliderule.Handler {
+ return sliderule.HandlerFunc(func(ctx context.Context, request *sliderule.Request) *sliderule.Response {
+ repo := Open(filepath.Join(repodir, sliderule.RouteParams(ctx)[reponamekey]))
+ return h.Handle(context.WithValue(ctx, repokey, repo), request)
+ })
+ }
+}
+
+func geminiRoot(repodir string) sliderule.Handler {
+ return sliderule.HandlerFunc(func(ctx context.Context, request *sliderule.Request) *sliderule.Response {
+ entries, err := os.ReadDir(repodir)
+ if err != nil {
+ return gemini.Failure(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 := geminiTemplate.ExecuteTemplate(buf, "repo_root.gmi", names); err != nil {
+ return gemini.Failure(err)
+ }
+
+ return gemini.Success("text/gemini; charset=utf-8", buf)
+ })
+}
+
+func geminiTreePath(ctx context.Context, request *sliderule.Request) *sliderule.Response {
+ params := sliderule.RouteParams(ctx)
+ if params["path"] == "" || strings.HasSuffix(params["path"], "/") {
+ return gmiTemplate(geminiTemplate, "tree.gmi").Handle(ctx, request)
+ }
+
+ repo := ctx.Value(repokey).(*Repository)
+
+ body, err := repo.Blob(ctx, params["ref"], params["path"])
+ if err != nil {
+ return gemini.Failure(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 gemini.Success(mediaType, bytes.NewBuffer(body))
+}
+
+func gmiTemplate(tmpl *template.Template, name string) sliderule.Handler {
+ return runTemplate(tmpl, name, "text/gemini; charset=utf-8")
+}
+
+func runTemplate(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 gemini.Failure(err)
+ }
+
+ return gemini.Success(mimetype, buf)
+ })
+}