Compare commits
2 Commits
3ff04cf885
...
21e2758145
Author | SHA1 | Date | |
---|---|---|---|
21e2758145 | |||
7a021631cd |
46
contrib/cgi/spartan.go
Normal file
46
contrib/cgi/spartan.go
Normal file
|
@ -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
|
// 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
|
// 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
|
// 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.
|
// 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
|
// 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
|
// 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
|
// 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.
|
// don't, it will produce "51 Not Found" responses for any directory paths.
|
||||||
|
|
124
contrib/fs/spartan.go
Normal file
124
contrib/fs/spartan.go
Normal file
|
@ -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
Block a user