From 66a1b1f39a1e1d5499b548b36d18c8daa872d7da Mon Sep 17 00:00:00 2001 From: tjpcc Date: Sat, 28 Jan 2023 14:52:35 -0700 Subject: gopher support. Some of the contrib packages were originally built gemini-specific and had to be refactored into generic core functionality and thin protocol-specific wrappers for each of gemini and gopher. --- contrib/fs/dir.go | 221 ++++++++++++++++++++++++------------------------------ 1 file changed, 100 insertions(+), 121 deletions(-) (limited to 'contrib/fs/dir.go') diff --git a/contrib/fs/dir.go b/contrib/fs/dir.go index 4328c8f..5659804 100644 --- a/contrib/fs/dir.go +++ b/contrib/fs/dir.go @@ -2,112 +2,123 @@ package fs import ( "bytes" - "context" + "io" "io/fs" "sort" "strings" "text/template" "tildegit.org/tjp/gus" - "tildegit.org/tjp/gus/gemini" ) -// DirectoryDefault handles directory path requests by looking for specific filenames. +// ResolveDirectory opens the directory corresponding to a request path. // -// If any of the supported filenames are found, the contents of the file is returned -// as the gemini response. -// -// It returns "51 Not Found" for any paths which don't correspond to a filesystem directory. -// -// When it encounters a directory path which doesn't end in a trailing slash (/) it -// redirects to a URL with the trailing slash appended. This is necessary for relative -// links into the directory's contents to function. -// -// It requires that files from the provided fs.FS implement fs.ReadDirFile. If they don't, -// it will also produce "51 Not Found" responses for directory paths. -func DirectoryDefault(fileSystem fs.FS, fileNames ...string) gus.Handler { - return func(ctx context.Context, req *gus.Request) *gus.Response { - path, dirFile, resp := handleDir(req, fileSystem) - if dirFile == nil { - return resp - } - defer dirFile.Close() +// The string is the full path to the directory. If the returned ReadDirFile +// is not nil, it will be open and must be closed by the caller. +func ResolveDirectory( + request *gus.Request, + fileSystem fs.FS, +) (string, fs.ReadDirFile, error) { + path := strings.Trim(request.Path, "/") + if path == "" { + path = "." + } - entries, err := dirFile.ReadDir(0) - if err != nil { - return gemini.Failure(err) - } + file, err := fileSystem.Open(path) + if isNotFound(err) { + return "", nil, nil + } + if err != nil { + return "", nil, err + } - for _, fileName := range fileNames { - for _, entry := range entries { - if entry.Name() == fileName { - file, err := fileSystem.Open(path + "/" + fileName) - if err != nil { - return gemini.Failure(err) - } - - return gemini.Success(mediaType(fileName), file) - } - } - } + isDir, err := fileIsDir(file) + if err != nil { + _ = file.Close() + return "", nil, err + } + + if !isDir { + _ = file.Close() + return "", nil, nil + } - return nil + dirFile, ok := file.(fs.ReadDirFile) + if !ok { + _ = file.Close() + return "", nil, nil } + + return path, dirFile, nil } -// DirectoryListing produces a gemtext listing of the contents of any requested directories. +// ResolveDirectoryDefault finds any of the provided filenames within a directory. // -// It returns "51 Not Found" for any paths which don't correspond to a filesystem directory. -// -// When it encounters a directory path which doesn't end in a trailing slash (/) it -// redirects to a URL with the trailing slash appended. This is necessary for relative -// links into the directory's contents to function. -// -// It requires that files from the provided fs.FS implement fs.ReadDirFile. If they don't, -// it will also produce "51 Not Found" responses for directory paths. +// If it does not find any of the filenames it returns "", nil, nil. +func ResolveDirectoryDefault( + fileSystem fs.FS, + dirPath string, + dir fs.ReadDirFile, + filenames []string, +) (string, fs.File, error) { + entries, err := dir.ReadDir(0) + if err != nil { + return "", nil, err + } + sort.Slice(entries, func(a, b int) bool { + return entries[a].Name() < entries[b].Name() + }) + + for _, filename := range filenames { + idx := sort.Search(len(entries), func(i int) bool { + return entries[i].Name() <= filename + }) + + if idx < len(entries) && entries[idx].Name() == filename { + path := dirPath + "/" + filename + file, err := fileSystem.Open(path) + return path, file, err + } + } + + return "", nil, nil +} + +// RenderDirectoryListing provides an io.Reader with the output of a directory listing template. // // The template is provided the following namespace: // - .FullPath: the complete path to the listed directory // - .DirName: the name of the directory itself // - .Entries: the []fs.DirEntry of the directory contents +// - .Hostname: the hostname of the server hosting the file +// - .Port: the port on which the server is listening // -// The template argument may be nil, in which case a simple default template is used. -func DirectoryListing(fileSystem fs.FS, template *template.Template) gus.Handler { - return func(ctx context.Context, req *gus.Request) *gus.Response { - path, dirFile, resp := handleDir(req, fileSystem) - if dirFile == nil { - return resp - } - defer dirFile.Close() - - if template == nil { - template = defaultDirListTemplate - } - - buf := &bytes.Buffer{} - - environ, err := dirlistNamespace(path, dirFile) - if err != nil { - return gemini.Failure(err) - } - - if err := template.Execute(buf, environ); err != nil { - gemini.Failure(err) - } +// Each entry in .Entries has the following fields: +// - .Name the string name of the item within the directory +// - .IsDir is a boolean +// - .Type is the FileMode bits +// - .Info is a method returning (fs.FileInfo, error) +func RenderDirectoryListing( + path string, + dir fs.ReadDirFile, + template *template.Template, + server gus.Server, +) (io.Reader, error) { + buf := &bytes.Buffer{} + + environ, err := dirlistNamespace(path, dir, server) + if err != nil { + return nil, err + } - return gemini.Success("text/gemini", buf) + if err := template.Execute(buf, environ); err != nil { + return nil, err } -} -var defaultDirListTemplate = template.Must(template.New("directory_listing").Parse(` -# {{ .DirName }} -{{ range .Entries }} -=> {{ .Name }}{{ if .IsDir }}/{{ end -}} -{{ end }} -=> ../ -`[1:])) + return buf, nil +} -func dirlistNamespace(path string, dirFile fs.ReadDirFile) (map[string]any, error) { +func dirlistNamespace(path string, dirFile fs.ReadDirFile, server gus.Server) (map[string]any, error) { entries, err := dirFile.ReadDir(0) if err != nil { return nil, err @@ -124,52 +135,20 @@ func dirlistNamespace(path string, dirFile fs.ReadDirFile) (map[string]any, erro dirname = path[strings.LastIndex(path, "/")+1:] } + hostname := "none" + port := "0" + if server != nil { + hostname = server.Hostname() + port = server.Port() + } + m := map[string]any{ "FullPath": path, "DirName": dirname, "Entries": entries, + "Hostname": hostname, + "Port": port, } return m, nil } - -func handleDir(req *gus.Request, fileSystem fs.FS) (string, fs.ReadDirFile, *gus.Response) { - path := strings.Trim(req.Path, "/") - if path == "" { - path = "." - } - - file, err := fileSystem.Open(path) - if isNotFound(err) { - return "", nil, nil - } - if err != nil { - return "", nil, gemini.Failure(err) - } - - isDir, err := fileIsDir(file) - if err != nil { - file.Close() - return "", nil, gemini.Failure(err) - } - - if !isDir { - file.Close() - return "", nil, nil - } - - if !strings.HasSuffix(req.Path, "/") { - file.Close() - url := *req.URL - url.Path += "/" - return "", nil, gemini.Redirect(url.String()) - } - - dirFile, ok := file.(fs.ReadDirFile) - if !ok { - file.Close() - return "", nil, nil - } - - return path, dirFile, nil -} -- cgit v1.2.3