package syw import ( "bytes" "context" "os" "path" "path/filepath" "text/template" "tildegit.org/tjp/sliderule" "tildegit.org/tjp/sliderule/gopher" ) // GopherRouter builds a router that will handle gopher requests in a directory of git repositories. // // The routes it defines are: // // / .gph listing of the repositories in the directory // /:repository .gph overview of the repository // /:repository/branches .gph list of branches/head // /:repository/tags .gph listing of tags // /:repository/refs/:ref .gph overview of a ref // /:repository/refs/:ref/tree .gph listing of a ref's root directory // /:repository/refs/:ref/tree/*path for directories:.gph list of contents // for files: raw files (guessed item type text/binary/image/etc) // /:repository/diffstat/:fromref/:toref text diffstat between two refs // /:repository/diff/:fromref/:toref text 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 be nil, but otherwise the template names // used are: // // repo_root.gph gophermap at / // repo_home.gph gophermap at /:repository // branch_list.gph gophermap at /:repository/branches // tag_list.gph gophermap at /:repository/tags // ref.gph gophermap at /:repository/refs/:ref // tree.gph gophermap at direcotry paths under /:repository/refs/:ref/tree/*path // (file paths return the raw files without any template involved) // diffstat.gph.txt plain text diffstat at /:repository/diffstat/:fromref/:toref // diff.gph.txt plain text diff at /:repository/diff/:fromref/:toref // // Most of the templates above are rendered with an object with 6 fields: // // Ctx: the context.Context from the request // Repo: a *syw.Repository corresponding to /:repository // Params: the map[string]string of the route parameters // Host: the hostname of the running server // Port: the port number of the running server // Selector: the selector in the current request // // The only exception is repo_root.gph, which is instead rendered with a slice of the repo names. // // All templates have 3 additional functions made available to them: // // combine: func(string, ...string) string - successively combines paths using url.URL.ResolveReference // join: func(string, ...string) string - successively joins path segments // rawtext: func(selector, host, port, text string) string renders text lines as gopher info-message lines. func GopherRouter(repodir string, overrides *template.Template) *sliderule.Router { tmpl, err := addTemplates(gopherTemplate, overrides) if err != nil { panic(err) } repoRouter := &sliderule.Router{} repoRouter.Use(assignRepo(repodir)) repoRouter.Route("/branches", runGopherTemplate(tmpl, "branch_list.gph", gopher.MenuType)) repoRouter.Route("/tags", runGopherTemplate(tmpl, "tag_list.gph", gopher.MenuType)) repoRouter.Route("/refs/:ref", runGopherTemplate(tmpl, "ref.gph", gopher.MenuType)) repoRouter.Route("/refs/:ref/tree", gopherTreePath(tmpl, false)) repoRouter.Route("/refs/:ref/tree/*path", gopherTreePath(tmpl, true)) repoRouter.Route("/diffstat/:fromref/:toref", runGopherTemplate(tmpl, "diffstat.gph.txt", gopher.TextFileType)) repoRouter.Route("/diff/:fromref/:toref", runGopherTemplate(tmpl, "diff.gph.txt", gopher.TextFileType)) router := &sliderule.Router{} router.Route("/", gopherRoot(repodir, tmpl)) router.Route("/:"+reponamekey, assignRepo(repodir)(runGopherTemplate(tmpl, "repo_home.gph", gopher.MenuType))) router.Mount("/:"+reponamekey, repoRouter) return router } func gopherRoot(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 gopher.Error(err).Response() } names := []string{} for _, item := range entries { if Open(filepath.Join(repodir, item.Name())) != nil { names = append(names, item.Name()) } } buf := &bytes.Buffer{} obj := map[string]any{ "Repos": names, "Host": request.Hostname(), "Port": request.Port(), "Selector": request.Path, } if err := tmpl.ExecuteTemplate(buf, "repo_root.gph", obj); err != nil { return gopher.Error(err).Response() } return gopher.File(gopher.MenuType, buf) }) } func gopherTreePath(tmpl *template.Template, haspath bool) sliderule.Handler { return sliderule.HandlerFunc(func(ctx context.Context, request *sliderule.Request) *sliderule.Response { repo := ctx.Value(repokey).(*Repository) params := sliderule.RouteParams(ctx) t := "tree" if haspath { var err error t, err = repo.Type(ctx, params["ref"]+":"+params["path"]) if err != nil { return gopher.Error(err).Response() } } if t != "blob" { if !haspath { params["path"] = "" } return runGopherTemplate(tmpl, "tree.gph", gopher.MenuType).Handle(ctx, request) } body, err := repo.Blob(ctx, params["ref"], params["path"]) if err != nil { return gopher.Error(err).Response() } filetype := gopher.MenuType if path.Base(params["path"]) != "gophermap" { filetype = gopher.GuessItemType(params["path"]) } return gopher.File(filetype, bytes.NewBuffer(body)) }) } func runGopherTemplate(tmpl *template.Template, name string, filetype sliderule.Status) 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), "Host": request.Hostname(), "Port": request.Port(), "Selector": request.Path, } buf := &bytes.Buffer{} if err := tmpl.ExecuteTemplate(buf, name, obj); err != nil { return gopher.Error(err).Response() } return gopher.File(filetype, buf) }) }