This repository has been archived on 2023-05-01. You can view files and clone it, but cannot push or open issues or pull requests.
gus/contrib/fs/dir.go

176 lines
4.3 KiB
Go

package fs
import (
"bytes"
"context"
"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.
//
// 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()
entries, err := dirFile.ReadDir(0)
if err != nil {
return gemini.Failure(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)
}
}
}
return nil
}
}
// DirectoryListing produces a gemtext listing of the contents of any requested directories.
//
// 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.
//
// 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
//
// 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)
}
return gemini.Success("text/gemini", buf)
}
}
var defaultDirListTemplate = template.Must(template.New("directory_listing").Parse(`
# {{ .DirName }}
{{ range .Entries }}
=> {{ .Name }}{{ if .IsDir }}/{{ end -}}
{{ end }}
=> ../
`[1:]))
func dirlistNamespace(path string, dirFile fs.ReadDirFile) (map[string]any, error) {
entries, err := dirFile.ReadDir(0)
if err != nil {
return nil, err
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].Name() < entries[j].Name()
})
var dirname string
if path == "." {
dirname = "(root)"
} else {
dirname = path[strings.LastIndex(path, "/")+1:]
}
m := map[string]any{
"FullPath": path,
"DirName": dirname,
"Entries": entries,
}
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
}