summaryrefslogtreecommitdiff
path: root/contrib/fs
diff options
context:
space:
mode:
authortjp <tjp@ctrl-c.club>2023-11-13 07:25:39 -0700
committertjp <tjp@ctrl-c.club>2023-11-13 07:27:16 -0700
commit1e0f8e0aaeaf1bd2ee39c02e922238b641bcf88b (patch)
tree020e5de91f2343119fed10dede9d2c8262a3cd83 /contrib/fs
parenta808b4692656c10bb43e2d54a2f5ef2746d231d5 (diff)
refactor contribs to work with a Protocol interface
Diffstat (limited to 'contrib/fs')
-rw-r--r--contrib/fs/file.go52
-rw-r--r--contrib/fs/gemini.go109
-rw-r--r--contrib/fs/gopher.go150
-rw-r--r--contrib/fs/handlers.go162
-rw-r--r--contrib/fs/spartan.go110
5 files changed, 189 insertions, 394 deletions
diff --git a/contrib/fs/file.go b/contrib/fs/file.go
index 9f11f4f..4d79fea 100644
--- a/contrib/fs/file.go
+++ b/contrib/fs/file.go
@@ -1,36 +1,9 @@
package fs
import (
- "mime"
- "os"
"strings"
- "unicode/utf8"
)
-func mediaType(filePath string) string {
- if strings.HasSuffix(filePath, ".gmi") {
- // This may not be present in the listings searched by mime.TypeByExtension,
- // so provide a dedicated fast path for it here.
- return "text/gemini"
- }
-
- slashIdx := strings.LastIndex(filePath, "/")
- dotIdx := strings.LastIndex(filePath[slashIdx+1:], ".")
- if dotIdx == -1 {
- return "application/octet-stream"
- }
- ext := filePath[slashIdx+1+dotIdx:]
-
- mtype := mime.TypeByExtension(ext)
- if mtype == "" {
- if contentsAreText(filePath) {
- return "text/plain"
- }
- return "application/octet-stream"
- }
- return mtype
-}
-
func isPrivate(fullpath string) bool {
for _, segment := range strings.Split(fullpath, "/") {
if len(segment) > 1 && segment[0] == '.' {
@@ -39,28 +12,3 @@ func isPrivate(fullpath string) bool {
}
return false
}
-
-func contentsAreText(filepath string) bool {
- f, err := os.Open(filepath)
- if err != nil {
- return false
- }
- defer func() { _ = f.Close() }()
-
- var buf [1024]byte
- n, err := f.Read(buf[:])
- if err != nil {
- return false
- }
-
- for i, c := range string(buf[:n]) {
- if i+utf8.UTFMax > n {
- // incomplete last char
- break
- }
- if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' {
- return false
- }
- }
- return true
-}
diff --git a/contrib/fs/gemini.go b/contrib/fs/gemini.go
index 79dcc63..6f9c75d 100644
--- a/contrib/fs/gemini.go
+++ b/contrib/fs/gemini.go
@@ -7,7 +7,6 @@ import (
"net/url"
"os"
"path"
- "path/filepath"
"strings"
"text/template"
@@ -42,7 +41,7 @@ func TitanUpload(fsroot, urlroot string, approver tlsauth.Approver) sr.Middlewar
if _, err := io.Copy(tmpf, body); err != nil {
_ = os.Remove(tmpf.Name())
- return gemini.PermanentFailure(err)
+ return gemini.Failure(err)
}
request = cloneRequest(request)
@@ -87,30 +86,7 @@ func cloneRequest(start *sr.Request) *sr.Request {
//
// It only serves responses for paths which do not correspond to directories on disk.
func GeminiFileHandler(fsroot, urlroot string) sr.Handler {
- fsroot = strings.TrimRight(fsroot, "/")
-
- return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
- if !strings.HasPrefix(request.Path, urlroot) {
- return nil
- }
- requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")
-
- fpath := filepath.Join(fsroot, requestpath)
- if isPrivate(fpath) {
- return nil
- }
- if isf, err := isFile(fpath); err != nil {
- return gemini.Failure(err)
- } else if !isf {
- return nil
- }
-
- file, err := os.Open(fpath)
- if err != nil {
- return gemini.Failure(err)
- }
- return gemini.Success(mediaType(fpath), file)
- })
+ return fileHandler(gemini.ServerProtocol, fsroot, urlroot)
}
// GeminiDirectoryDefault serves up default files for directory path requests.
@@ -124,47 +100,7 @@ func GeminiFileHandler(fsroot, urlroot string) sr.Handler {
// redirects to a URL with the trailing slash appended. This is necessary for relative
// links in the directory's contents to function properly.
func GeminiDirectoryDefault(fsroot, urlroot string, filenames ...string) sr.Handler {
- fsroot = strings.TrimRight(fsroot, "/")
-
- return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
- if !strings.HasPrefix(request.Path, urlroot) {
- return nil
- }
-
- if !strings.HasSuffix(request.Path, "/") {
- u := *request.URL
- u.Path += "/"
- return gemini.PermanentRedirect(u.String())
- }
-
- requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")
- fpath := filepath.Join(fsroot, requestpath)
- if isPrivate(fpath) {
- return nil
- }
- if isd, err := isDir(fpath); err != nil {
- return gemini.Failure(err)
- } else if !isd {
- return nil
- }
-
- for _, fname := range filenames {
- candidatepath := filepath.Join(fpath, fname)
- if isf, err := isFile(candidatepath); err != nil {
- return gemini.Failure(err)
- } else if !isf {
- continue
- }
-
- file, err := os.Open(candidatepath)
- if err != nil {
- return gemini.Failure(err)
- }
- return gemini.Success(mediaType(candidatepath), file)
- }
-
- return nil
- })
+ return directoryDefault(gemini.ServerProtocol, fsroot, urlroot, true, filenames...)
}
// GeminiDirectoryListing produces a listing of the contents of any requested directories.
@@ -177,40 +113,11 @@ func GeminiDirectoryDefault(fsroot, urlroot string, filenames ...string) sr.Hand
//
// The template may be nil, in which case DefaultGeminiDirectoryList is used instead. The
// template is then processed with RenderDirectoryListing.
-func GeminiDirectoryListing(fsroot, urlroot string, template *template.Template) sr.Handler {
- fsroot = strings.TrimRight(fsroot, "/")
-
- return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
- if !strings.HasSuffix(request.Path, "/") {
- u := *request.URL
- u.Path += "/"
- return gemini.PermanentRedirect(u.String())
- }
- if !strings.HasPrefix(request.Path, urlroot) {
- return nil
- }
- requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")
-
- fpath := filepath.Join(fsroot, requestpath)
- if isPrivate(fpath) {
- return nil
- }
- if isd, err := isDir(fpath); err != nil {
- return gemini.Failure(err)
- } else if !isd {
- return nil
- }
-
- if template == nil {
- template = DefaultGeminiDirectoryList
- }
- body, err := RenderDirectoryListing(fpath, requestpath, template, request.Server)
- if err != nil {
- return gemini.Failure(err)
- }
-
- return gemini.Success("text/gemini", body)
- })
+func GeminiDirectoryListing(fsroot, urlroot string, tmpl *template.Template) sr.Handler {
+ if tmpl == nil {
+ tmpl = DefaultGeminiDirectoryList
+ }
+ return directoryListing(gemini.ServerProtocol, fsroot, urlroot, "file.gmi", true, tmpl)
}
// DefaultGeminiDirectoryList is a template which renders a reasonable gemtext dir list.
diff --git a/contrib/fs/gopher.go b/contrib/fs/gopher.go
index 0a0b482..209a4ec 100644
--- a/contrib/fs/gopher.go
+++ b/contrib/fs/gopher.go
@@ -2,9 +2,6 @@ package fs
import (
"context"
- "os"
- "path/filepath"
- "slices"
"strings"
sr "tildegit.org/tjp/sliderule"
@@ -16,50 +13,8 @@ import (
//
// It only serves responses for paths which correspond to files, not directories.
func GopherFileHandler(fsroot, urlroot string, settings *gophermap.FileSystemSettings) sr.Handler {
- fsroot = strings.TrimRight(fsroot, "/")
-
- return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
- if !strings.HasPrefix(request.Path, urlroot) {
- return nil
- }
- requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")
-
- path := filepath.Join(fsroot, requestpath)
- if isPrivate(path) {
- return nil
- }
- if isf, err := isFile(path); err != nil {
- return gopher.Error(err).Response()
- } else if !isf {
- return nil
- }
-
- if settings == nil {
- settings = &gophermap.FileSystemSettings{}
- }
-
- file, err := os.Open(path)
- if err != nil {
- return gopher.Error(err).Response()
- }
-
- if !(settings.ParseExtended && isMap(path, *settings)) {
- return gopher.File(gopher.GuessItemType(path), file)
- }
-
- defer func() { _ = file.Close() }()
-
- edoc, err := gophermap.ParseExtended(file, request.URL)
- if err != nil {
- return gopher.Error(err).Response()
- }
-
- doc, _, err := edoc.Compatible(filepath.Dir(path), *settings)
- if err != nil {
- return gopher.Error(err).Response()
- }
- return doc.Response()
- })
+ handler := fileHandler(gopher.ServerProtocol, fsroot, urlroot)
+ return gophermap.ExtendMiddleware(fsroot, urlroot, settings)(handler)
}
// GopherDirectoryDefault serves up default files for directory path requests.
@@ -69,61 +24,12 @@ func GopherFileHandler(fsroot, urlroot string, settings *gophermap.FileSystemSet
//
// It returns nil for any paths which don't correspond to a directory.
func GopherDirectoryDefault(fsroot, urlroot string, settings *gophermap.FileSystemSettings) sr.Handler {
- fsroot = strings.TrimRight(fsroot, "/")
-
- return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
- if !strings.HasPrefix(request.Path, urlroot) {
- return nil
- }
- requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")
-
- path := filepath.Join(fsroot, requestpath)
- if isPrivate(path) {
- return nil
- }
- if isd, err := isDir(path); err != nil {
- return gopher.Error(err).Response()
- } else if !isd {
- return nil
- }
-
- if settings == nil {
- settings = &gophermap.FileSystemSettings{}
- }
-
- for _, fname := range settings.DirMaps {
- fpath := filepath.Join(path, fname)
- if isf, err := isFile(fpath); err != nil {
- return gopher.Error(err).Response()
- } else if !isf {
- continue
- }
-
- file, err := os.Open(fpath)
- if err != nil {
- return gopher.Error(err).Response()
- }
-
- if settings.ParseExtended {
- defer func() { _ = file.Close() }()
-
- edoc, err := gophermap.ParseExtended(file, request.URL)
- if err != nil {
- return gopher.Error(err).Response()
- }
-
- doc, _, err := edoc.Compatible(path, *settings)
- if err != nil {
- return gopher.Error(err).Response()
- }
- return doc.Response()
- } else {
- return gopher.File(gopher.MenuType, file)
- }
- }
+ if settings == nil {
+ return sr.HandlerFunc(func(_ context.Context, _ *sr.Request) *sr.Response { return nil })
+ }
- return nil
- })
+ handler := directoryDefault(gopher.ServerProtocol, fsroot, urlroot, false, settings.DirMaps...)
+ return gophermap.ExtendMiddleware(fsroot, urlroot, settings)(handler)
}
// GopherDirectoryListing produces a listing of the contents of any requested directories.
@@ -136,13 +42,13 @@ func GopherDirectoryListing(fsroot, urlroot string, settings *gophermap.FileSyst
if !strings.HasPrefix(request.Path, urlroot) {
return nil
}
- requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")
- path := filepath.Join(fsroot, requestpath)
- if isPrivate(path) {
+ dpath, _ := rebasePath(fsroot, urlroot, request)
+
+ if isPrivate(dpath) {
return nil
}
- if isd, err := isDir(path); err != nil {
+ if isd, err := isDir(dpath); err != nil {
return gopher.Error(err).Response()
} else if !isd {
return nil
@@ -151,7 +57,7 @@ func GopherDirectoryListing(fsroot, urlroot string, settings *gophermap.FileSyst
if settings == nil {
settings = &gophermap.FileSystemSettings{}
}
- doc, err := gophermap.ListDir(path, request.URL, *settings)
+ doc, err := gophermap.ListDir(dpath, request.URL, *settings)
if err != nil {
return gopher.Error(err).Response()
}
@@ -159,35 +65,3 @@ func GopherDirectoryListing(fsroot, urlroot string, settings *gophermap.FileSyst
return doc.Response()
})
}
-
-func isDir(path string) (bool, error) {
- info, err := os.Stat(path)
- if err != nil {
- if isNotFound(err) {
- err = nil
- }
- return false, err
- }
- return info.IsDir() && info.Mode()&4 == 4, nil
-}
-
-func isFile(path string) (bool, error) {
- info, err := os.Stat(path)
- if err != nil {
- if isNotFound(err) {
- err = nil
- }
- return false, err
- }
- m := info.Mode()
-
- return m.IsRegular() && m&4 == 4, nil
-}
-
-func isMap(path string, settings gophermap.FileSystemSettings) bool {
- base := filepath.Base(path)
- if base == "gophermap" || strings.HasSuffix(base, ".gph") || strings.HasSuffix(base, ".gophermap") {
- return true
- }
- return slices.Contains(settings.DirMaps, filepath.Base(path))
-}
diff --git a/contrib/fs/handlers.go b/contrib/fs/handlers.go
new file mode 100644
index 0000000..75422d9
--- /dev/null
+++ b/contrib/fs/handlers.go
@@ -0,0 +1,162 @@
+package fs
+
+import (
+ "context"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+ "text/template"
+
+ sr "tildegit.org/tjp/sliderule"
+)
+
+func fileHandler(protocol sr.ServerProtocol, fsroot, urlroot string) sr.Handler {
+ fsroot = strings.TrimRight(fsroot, "/")
+
+ return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
+ if !strings.HasPrefix(request.Path, urlroot) {
+ return nil
+ }
+
+ fpath, _ := rebasePath(fsroot, urlroot, request)
+
+ if isPrivate(fpath) {
+ return nil
+ }
+ if isf, err := isFile(fpath); err != nil {
+ return protocol.TemporaryServerError(err)
+ } else if !isf {
+ return nil
+ }
+
+ file, err := os.Open(fpath)
+ if err != nil {
+ return protocol.TemporaryServerError(err)
+ }
+ return protocol.Success(filepath.Base(fpath), file)
+ })
+}
+
+func directoryDefault(
+ protocol sr.ServerProtocol,
+ fsroot string,
+ urlroot string,
+ redirectSlash bool,
+ filenames ...string,
+) sr.Handler {
+ fsroot = strings.TrimRight(fsroot, "/")
+
+ return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
+ if !strings.HasPrefix(request.Path, urlroot) {
+ return nil
+ }
+
+ fpath, _ := rebasePath(fsroot, urlroot, request)
+
+ if isPrivate(fpath) {
+ return nil
+ }
+ if isd, err := isDir(fpath); err != nil {
+ return protocol.TemporaryServerError(err)
+ } else if !isd {
+ return nil
+ }
+
+ if redirectSlash && !strings.HasSuffix(request.Path, "/") {
+ return protocol.PermanentRedirect(appendSlash(request.URL))
+ }
+
+ for _, fname := range filenames {
+ fpath := filepath.Join(fpath, fname)
+ if isf, err := isFile(fpath); err != nil {
+ return protocol.TemporaryServerError(err)
+ } else if !isf {
+ continue
+ }
+
+ file, err := os.Open(fpath)
+ if err != nil {
+ return protocol.TemporaryServerError(err)
+ }
+ return protocol.Success(filepath.Base(fpath), file)
+ }
+
+ return nil
+ })
+}
+
+func directoryListing(
+ protocol sr.ServerProtocol,
+ fsroot string,
+ urlroot string,
+ successFilename string,
+ redirectSlash bool,
+ tmpl *template.Template,
+) sr.Handler {
+ fsroot = strings.TrimRight(fsroot, "/")
+
+ return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
+ if !strings.HasPrefix(request.Path, urlroot) {
+ return nil
+ }
+
+ dpath, rpath := rebasePath(fsroot, urlroot, request)
+
+ if isPrivate(dpath) {
+ return nil
+ }
+ if isd, err := isDir(dpath); err != nil {
+ return protocol.TemporaryServerError(err)
+ } else if !isd {
+ return nil
+ }
+
+ if redirectSlash && !strings.HasSuffix(request.Path, "/") {
+ return protocol.PermanentRedirect(appendSlash(request.URL))
+ }
+
+ body, err := RenderDirectoryListing(dpath, rpath, tmpl, request.Server)
+ if err != nil {
+ return protocol.TemporaryServerError(err)
+ }
+
+ return protocol.Success(successFilename, body)
+ })
+}
+
+func rebasePath(fsroot, urlroot string, request *sr.Request) (string, string) {
+ p := strings.TrimPrefix(request.Path, urlroot)
+ p = strings.Trim(p, "/")
+ return filepath.Join(fsroot, p), p
+}
+
+func appendSlash(u *url.URL) *url.URL {
+ v := *u
+ v.Path += "/"
+ return &v
+}
+
+func isDir(path string) (bool, error) {
+ info, err := os.Stat(path)
+ if err != nil {
+ if isNotFound(err) {
+ err = nil
+ }
+ return false, err
+ }
+ return info.IsDir() && info.Mode()&4 == 4, nil
+}
+
+func isFile(path string) (bool, error) {
+ info, err := os.Stat(path)
+ if err != nil {
+ if isNotFound(err) {
+ err = nil
+ }
+ return false, err
+ }
+ m := info.Mode()
+
+ return m.IsRegular() && m&4 == 4, nil
+}
diff --git a/contrib/fs/spartan.go b/contrib/fs/spartan.go
index bee274a..d97edd1 100644
--- a/contrib/fs/spartan.go
+++ b/contrib/fs/spartan.go
@@ -1,10 +1,6 @@
package fs
import (
- "context"
- "os"
- "path/filepath"
- "strings"
"text/template"
sr "tildegit.org/tjp/sliderule"
@@ -15,30 +11,7 @@ import (
//
// It only serves responses for paths which correspond to regular files or symlinks to them.
func SpartanFileHandler(fsroot, urlroot string) sr.Handler {
- fsroot = strings.TrimRight(fsroot, "/")
-
- return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
- if !strings.HasPrefix(request.Path, urlroot) {
- return nil
- }
- requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")
-
- fpath := filepath.Join(fsroot, requestpath)
- if isPrivate(fpath) {
- return nil
- }
- if isf, err := isFile(fpath); err != nil {
- return spartan.ServerError(err)
- } else if !isf {
- return nil
- }
-
- file, err := os.Open(fpath)
- if err != nil {
- return spartan.ServerError(err)
- }
- return spartan.Success(mediaType(fpath), file)
- })
+ return fileHandler(spartan.ServerProtocol, fsroot, urlroot)
}
// SpartanDirectoryDefault serves up default files for directory path requests.
@@ -52,47 +25,7 @@ func SpartanFileHandler(fsroot, urlroot string) sr.Handler {
// redirects to the URL with the slash appended. This is necessary for relative links
// in the directory's contents to function properly.
func SpartanDirectoryDefault(fsroot, urlroot string, filenames ...string) sr.Handler {
- fsroot = strings.TrimRight(fsroot, "/")
-
- return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
- if !strings.HasPrefix(request.Path, urlroot) {
- return nil
- }
-
- if !strings.HasSuffix(request.Path, "/") {
- u := *request.URL
- u.Path += "/"
- return spartan.Redirect(u.String())
- }
-
- requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")
- fpath := filepath.Join(fsroot, requestpath)
- if isPrivate(fpath) {
- return nil
- }
- if isd, err := isDir(fpath); err != nil {
- return spartan.ServerError(err)
- } else if !isd {
- return nil
- }
-
- for _, fname := range filenames {
- candidatepath := filepath.Join(fpath, fname)
- if isf, err := isFile(candidatepath); err != nil {
- return spartan.ServerError(err)
- } else if !isf {
- continue
- }
-
- file, err := os.Open(candidatepath)
- if err != nil {
- return spartan.ServerError(err)
- }
- return spartan.Success(mediaType(candidatepath), file)
- }
-
- return nil
- })
+ return directoryDefault(spartan.ServerProtocol, fsroot, urlroot, true, filenames...)
}
// SpartanDirectoryListing produces a listing of the contents of any requested directories.
@@ -105,40 +38,11 @@ func SpartanDirectoryDefault(fsroot, urlroot string, filenames ...string) sr.Han
//
// The template may be nil, in which case DefaultSpartanDirectoryList is used instead. The
// template is then processed with RenderDirectoryListing.
-func SpartanDirectoryListing(fsroot, urlroot string, template *template.Template) sr.Handler {
- fsroot = strings.TrimRight(fsroot, "/")
-
- return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
- if !strings.HasSuffix(request.Path, "/") {
- u := *request.URL
- u.Path += "/"
- return spartan.Redirect(u.String())
- }
- if !strings.HasPrefix(request.Path, urlroot) {
- return nil
- }
- requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")
-
- fpath := filepath.Join(fsroot, requestpath)
- if isPrivate(fpath) {
- return nil
- }
- if isd, err := isDir(fpath); err != nil {
- return spartan.ServerError(err)
- } else if !isd {
- return nil
- }
-
- if template == nil {
- template = DefaultSpartanDirectoryList
- }
- body, err := RenderDirectoryListing(fpath, requestpath, template, request.Server)
- if err != nil {
- return spartan.ServerError(err)
- }
-
- return spartan.Success("text/gemini", body)
- })
+func SpartanDirectoryListing(fsroot, urlroot string, tmpl *template.Template) sr.Handler {
+ if tmpl == nil {
+ tmpl = DefaultSpartanDirectoryList
+ }
+ return directoryListing(spartan.ServerProtocol, fsroot, urlroot, "file.gmi", true, tmpl)
}
// DefaultSpartanDirectoryList is a tmeplate which renders a reasonable gemtext dir listing.