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
146
147
148
149
150
151
152
153
154
155
156
|
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 = "repository"
)
// GeminiRouter 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 GeminiRouter(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("/", gmiTemplate(tmpl, "repo_home.gmi"))
repoRouter.Route("/branches", gmiTemplate(tmpl, "branch_list.gmi"))
repoRouter.Route("/tags", gmiTemplate(tmpl, "tag_list.gmi"))
repoRouter.Route("/refs/:ref/", gmiTemplate(tmpl, "ref.gmi"))
repoRouter.Route("/refs/:ref/tree/*path", geminiTreePath(tmpl))
repoRouter.Route("/diffstat/:fromref/:toref", runGemiTemplate(tmpl, "diffstat.gmi.txt", "text/plain"))
repoRouter.Route("/diff/:fromref/:toref", runGemiTemplate(tmpl, "diff.gmi.txt", "text/x-diff"))
router := &sliderule.Router{}
router.Route("/", geminiRoot(repodir, tmpl))
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, 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 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 := tmpl.ExecuteTemplate(buf, "repo_root.gmi", names); err != nil {
return gemini.Failure(err)
}
return gemini.Success("text/gemini; charset=utf-8", buf)
})
}
func geminiTreePath(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 gmiTemplate(tmpl, "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 runGemiTemplate(tmpl, name, "text/gemini; charset=utf-8")
}
func runGemiTemplate(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)
})
}
|