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)
})
}
|