summaryrefslogtreecommitdiff
path: root/contrib/fs/gopher.go
diff options
context:
space:
mode:
authortjpcc <tjp@ctrl-c.club>2023-01-28 14:52:35 -0700
committertjpcc <tjp@ctrl-c.club>2023-01-28 15:01:41 -0700
commit66a1b1f39a1e1d5499b548b36d18c8daa872d7da (patch)
tree96471dbd5486ede1a908790ac23e0c55b226dfad /contrib/fs/gopher.go
parenta27b879accb191b6a6c6e76a6251ed751967f73a (diff)
gopher support.
Some of the contrib packages were originally built gemini-specific and had to be refactored into generic core functionality and thin protocol-specific wrappers for each of gemini and gopher.
Diffstat (limited to 'contrib/fs/gopher.go')
-rw-r--r--contrib/fs/gopher.go168
1 files changed, 168 insertions, 0 deletions
diff --git a/contrib/fs/gopher.go b/contrib/fs/gopher.go
new file mode 100644
index 0000000..7b0d8bd
--- /dev/null
+++ b/contrib/fs/gopher.go
@@ -0,0 +1,168 @@
+package fs
+
+import (
+ "context"
+ "io/fs"
+ "mime"
+ "path"
+ "strings"
+ "text/template"
+
+ "tildegit.org/tjp/gus"
+ "tildegit.org/tjp/gus/gopher"
+)
+
+// GopherFileHandler builds a handler which serves up files from a file system.
+//
+// It only serves responses for paths which correspond to files, not directories.
+func GopherFileHandler(fileSystem fs.FS) gus.Handler {
+ return func(ctx context.Context, request *gus.Request) *gus.Response {
+ filepath, file, err := ResolveFile(request, fileSystem)
+ if err != nil {
+ return gopher.Error(err).Response()
+ }
+
+ if file == nil {
+ return nil
+ }
+
+ return gopher.File(GuessGopherItemType(filepath), file)
+ }
+}
+
+// GopherDirectoryDefault serves up default files for directory path requests.
+//
+// If any of the supported filenames are found in the requested directory, the
+// contents of that file is returned as the gopher response.
+//
+// It returns nil for any paths which don't correspond to a directory.
+//
+// It requires that files from the provided fs.FS implement fs.ReadDirFile. If
+// they don't, it will produce nil responses for all directory paths.
+func GopherDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler {
+ return func(ctx context.Context, request *gus.Request) *gus.Response {
+ dirpath, dir, err := ResolveDirectory(request, fileSystem)
+ if err != nil {
+ return gopher.Error(err).Response()
+ }
+ if dir == nil {
+ return nil
+ }
+ defer func() { _ = dir.Close() }()
+
+ _, file, err := ResolveDirectoryDefault(fileSystem, dirpath, dir, filenames)
+ if err != nil {
+ return gopher.Error(err).Response()
+ }
+ if file == nil {
+ return nil
+ }
+
+ return gopher.File(gopher.MenuType, file)
+ }
+}
+
+// GopherDirectoryListing produces a listing of the contents of any requested directories.
+//
+// It returns nil for any paths which don't correspond to a filesystem directory.
+//
+// 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.
+//
+// A template may be nil, in which case DefaultGopherDirectoryList is used instead. The
+// template is then processed with RenderDirectoryListing.
+func GopherDirectoryListing(fileSystem fs.FS, tpl *template.Template) gus.Handler {
+ return func(ctx context.Context, request *gus.Request) *gus.Response {
+ dirpath, dir, err := ResolveDirectory(request, fileSystem)
+ if err != nil {
+ return gopher.Error(err).Response()
+ }
+ if dir == nil {
+ return nil
+ }
+ defer func() { _ = dir.Close() }()
+
+ if tpl == nil {
+ tpl = DefaultGopherDirectoryList
+ }
+ body, err := RenderDirectoryListing(dirpath, dir, tpl, request.Server)
+ if err != nil {
+ return gopher.Error(err).Response()
+ }
+
+ return gopher.File(gopher.MenuType, body)
+ }
+}
+
+// GopherTemplateFunctions is a map for templates providing useful functions for gophermaps.
+//
+// - GuessItemType: return a gopher item type for a file based on its path/name.
+var GopherTemplateFunctions = template.FuncMap{
+ "GuessItemType": func(filepath string) string {
+ return string([]byte{byte(GuessGopherItemType(filepath))})
+ },
+}
+
+// DefaultGopherDirectoryList is a template which renders a directory listing as gophermap.
+var DefaultGopherDirectoryList = template.Must(
+ template.New("gopher_dirlist").Funcs(GopherTemplateFunctions).Parse(
+ strings.ReplaceAll(
+ `
+{{ $root := .FullPath -}}
+{{ if eq .FullPath "." }}{{ $root = "" }}{{ end -}}
+{{ $hostname := .Hostname -}}
+{{ $port := .Port -}}
+i{{ .DirName }} {{ $hostname }} {{ $port }}
+i {{ $hostname }} {{ $port }}
+{{ range .Entries -}}
+{{ if .IsDir -}}
+1{{ .Name }} {{ $root }}/{{ .Name }} {{ $hostname }} {{ $port }}
+{{- else -}}
+{{ GuessItemType .Name }}{{ .Name }} {{ $root }}/{{ .Name }} {{ $hostname }} {{ $port }}
+{{- end }}
+{{ end -}}
+.
+`[1:],
+ "\n",
+ "\r\n",
+ ),
+ ),
+)
+
+// GuessGopherItemType attempts to find the best gopher item type for a file based on its name.
+func GuessGopherItemType(filepath string) gus.Status {
+ ext := path.Ext(filepath)
+ switch ext {
+ case "txt", "gmi":
+ return gopher.TextFileType
+ case "gif", "png", "jpg", "jpeg":
+ return gopher.ImageFileType
+ case "mp4", "mov":
+ return gopher.MovieFileType
+ case "mp3", "aiff", "aif", "aac", "ogg", "flac", "alac", "wma":
+ return gopher.SoundFileType
+ case "bmp":
+ return gopher.BitmapType
+ case "doc", "docx", "odt":
+ return gopher.DocumentType
+ case "html", "htm":
+ return gopher.HTMLType
+ case "rtf":
+ return gopher.RtfDocumentType
+ case "wav":
+ return gopher.WavSoundFileType
+ case "pdf":
+ return gopher.PdfDocumentType
+ case "xml":
+ return gopher.XmlDocumentType
+ case "":
+ return gopher.BinaryFileType
+ }
+
+ mtype := mime.TypeByExtension(ext)
+ if strings.HasPrefix(mtype, "text/") {
+ return gopher.TextFileType
+ }
+
+ return gopher.BinaryFileType
+}