package fs import ( "context" "io/fs" "mime" "path" "strings" "text/template" sr "tildegit.org/tjp/sliderule" "tildegit.org/tjp/sliderule/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) sr.Handler { return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.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) sr.Handler { return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.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) sr.Handler { return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.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) sr.Status { ext := path.Ext(filepath) switch ext { case ".gophermap": return gopher.MenuType 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 }