summaryrefslogtreecommitdiff
path: root/contrib/fs/dir.go
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/fs/dir.go')
-rw-r--r--contrib/fs/dir.go221
1 files changed, 100 insertions, 121 deletions
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
-}