summaryrefslogtreecommitdiff
path: root/spartan.go
blob: beb001e504afe58ce675016b7ee2b50c7b99b1aa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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)
	})
}