176 lines
4.3 KiB
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
|
|
}
|