Compare commits
2 Commits
3ff04cf885
...
21e2758145
Author | SHA1 | Date |
---|---|---|
tjpcc | 21e2758145 | |
tjpcc | 7a021631cd |
|
@ -0,0 +1,46 @@
|
|||
package cgi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"tildegit.org/tjp/gus"
|
||||
"tildegit.org/tjp/gus/spartan"
|
||||
)
|
||||
|
||||
// SpartanCGIDirectory runs executable files relative to a root directory in the file system.
|
||||
//
|
||||
// It will also find any run any executable _part way_ through the path, so for example a
|
||||
// request for /foo/bar/baz can also run an executable found at /foo or /foo/bar. In such
|
||||
// a case the PATH_INFO environment variable will include the remaining portion of the URI.
|
||||
func SpartanCGIDirectory(pathRoot, fsRoot string) gus.Handler {
|
||||
fsRoot = strings.TrimRight(fsRoot, "/")
|
||||
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
|
||||
if !strings.HasPrefix(request.Path, pathRoot) {
|
||||
return nil
|
||||
}
|
||||
|
||||
filepath, pathinfo, err := ResolveCGI(request.Path[len(pathRoot):], fsRoot)
|
||||
if err != nil {
|
||||
return spartan.ServerError(err)
|
||||
}
|
||||
if filepath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
stdout, exitCode, err := RunCGI(ctx, request, filepath, pathinfo)
|
||||
if err != nil {
|
||||
return spartan.ServerError(err)
|
||||
}
|
||||
if exitCode != 0 {
|
||||
return spartan.ServerError(fmt.Errorf("CGI process exited with status %d", exitCode))
|
||||
}
|
||||
|
||||
response, err := spartan.ParseResponse(stdout)
|
||||
if err != nil {
|
||||
return spartan.ServerError(err)
|
||||
}
|
||||
return response
|
||||
})
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
Reference in New Issue