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 }