From 7a021631cd9e02abe62610f66567f81062cecfbe Mon Sep 17 00:00:00 2001 From: tjpcc Date: Sat, 29 Apr 2023 20:56:15 -0600 Subject: spartan FS server --- contrib/fs/gemini.go | 4 +- contrib/fs/spartan.go | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 contrib/fs/spartan.go diff --git a/contrib/fs/gemini.go b/contrib/fs/gemini.go index 15677f1..21ca32d 100644 --- a/contrib/fs/gemini.go +++ b/contrib/fs/gemini.go @@ -37,7 +37,7 @@ func GeminiFileHandler(fileSystem fs.FS) gus.Handler { // // 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 inot the directory's contents to function properly. +// links not the directory's contents to function properly. // // It requires that files from the provided fs.FS implement fs.ReadDirFile. If they // don't, it will produce nil responses for any directory paths. @@ -70,7 +70,7 @@ func GeminiDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler { // // 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 inot the directory's contents to function properly. +// links not the directory's contents to function properly. // // It requires that files from the provided fs.FS implement fs.ReadDirFile. If they // don't, it will produce "51 Not Found" responses for any directory paths. diff --git a/contrib/fs/spartan.go b/contrib/fs/spartan.go new file mode 100644 index 0000000..550f549 --- /dev/null +++ b/contrib/fs/spartan.go @@ -0,0 +1,124 @@ +package fs + +import ( + "context" + "io/fs" + "strings" + "text/template" + + "tildegit.org/tjp/gus" + "tildegit.org/tjp/gus/spartan" +) + +// SpartanFileHandler builds a handler which serves up files from a filesystem. +// +// It only serves responses for paths which do not correspond to directories on disk. +func SpartanFileHandler(fileSystem fs.FS) gus.Handler { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { + filepath, file, err := ResolveFile(request, fileSystem) + if err != nil { + return spartan.ClientError(err) + } + + if file == nil { + return nil + } + + return spartan.Success(mediaType(filepath), file) + }) +} + +// SpartanDirectoryDefault serves up default files for directory path requests. +// +// If any of the supported filenames are found, the contents of the file is returned +// as the spartan response. +// +// It returns nil for any paths which don't correspond to a directory. +// +// When it encounters a directory path which doesn't end in a trailing slash (/) it +// redirects to the same URL with the slash appended. This is necessary for relative +// links not in the directory's contents to function properly. +// +// It requires that files from the provided fs.FS implement fs.ReadDirFile. If they +// don't, it will produce nil responses for any directory paths. +func SpartanDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { + dirpath, dir, response := handleDirSpartan(request, fileSystem) + if response != nil { + return response + } + if dir == nil { + return nil + } + defer func() { _ = dir.Close() }() + + filepath, file, err := ResolveDirectoryDefault(fileSystem, dirpath, dir, filenames) + if err != nil { + return spartan.ServerError(err) + } + if file == nil { + return nil + } + + return spartan.Success(mediaType(filepath), file) + }) +} + +// SpartanDirectoryListing produces a listing of the contents of any requested directories. +// +// It returns "4 Resource 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 not in the directory's +// contents to function properly. +// +// It requires that files provided by the fs.FS implement fs.ReadDirFile. If they don't, it will +// produce "4 Resource not found" responses for any directory paths. +// +// The tmeplate may be nil, in which cause DefaultSpartanDirectoryList is used instead. The +// template is then processed with RenderDirectoryListing. +func SpartanDirectoryListing(filesystem fs.FS, template *template.Template) gus.Handler { + return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response { + dirpath, dir, response := handleDirSpartan(request, filesystem) + if response != nil { + return response + } + if dir == nil { + return nil + } + defer func() { _ = dir.Close() }() + + if template == nil { + template = DefaultSpartanDirectoryList + } + body, err := RenderDirectoryListing(dirpath, dir, template, request.Server) + if err != nil { + return spartan.ServerError(err) + } + + return spartan.Success("text/gemini", body) + }) +} + +// DefaultSpartanDirectoryList is a template which renders a reasonable gemtext dir listing. +var DefaultSpartanDirectoryList = DefaultGeminiDirectoryList + +func handleDirSpartan(request *gus.Request, filesystem fs.FS) (string, fs.ReadDirFile, *gus.Response) { + path, dir, err := ResolveDirectory(request, filesystem) + if err != nil { + return "", nil, spartan.ServerError(err) + } + + if dir == nil { + return "", nil, nil + } + + if !strings.HasSuffix(request.Path, "/") { + _ = dir.Close() + url := *request.URL + url.Path += "/" + return "", nil, spartan.Redirect(url.String()) + } + + return path, dir, nil +} -- cgit v1.2.3