package fs import ( "context" "os" "path/filepath" "slices" "strings" sr "tildegit.org/tjp/sliderule" "tildegit.org/tjp/sliderule/gopher" "tildegit.org/tjp/sliderule/gopher/gophermap" ) // 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(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() }) } // 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. 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) } } return nil }) } // 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. func GopherDirectoryListing(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{} } doc, err := gophermap.ListDir(path, request.URL, *settings) if err != nil { return gopher.Error(err).Response() } 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)) }