package fs import ( "bytes" "io" "io/fs" "sort" "strings" "text/template" sr "tildegit.org/tjp/sliderule" ) // ResolveDirectory opens the directory corresponding to a request path. // // The string is the full path to the directory. If the returned ReadDirFile // is not nil, it will be open and must be closed by the caller. func ResolveDirectory( request *sr.Request, fileSystem fs.FS, ) (string, fs.ReadDirFile, error) { path := strings.Trim(request.Path, "/") if path == "" { path = "." } file, err := fileSystem.Open(path) if isNotFound(err) { return "", nil, nil } if err != nil { return "", nil, err } isDir, err := fileIsDir(file) if err != nil { _ = file.Close() return "", nil, err } if !isDir { _ = file.Close() return "", nil, nil } dirFile, ok := file.(fs.ReadDirFile) if !ok { _ = file.Close() return "", nil, nil } return path, dirFile, nil } // ResolveDirectoryDefault finds any of the provided filenames within a directory. // // If it does not find any of the filenames it returns "", nil, nil. func ResolveDirectoryDefault( fileSystem fs.FS, dirPath string, dir fs.ReadDirFile, filenames []string, ) (string, fs.File, error) { entries, err := dir.ReadDir(0) if err != nil { return "", nil, err } sort.Slice(entries, func(a, b int) bool { return entries[a].Name() < entries[b].Name() }) for _, filename := range filenames { idx := sort.Search(len(entries), func(i int) bool { return entries[i].Name() <= filename }) if idx < len(entries) && entries[idx].Name() == filename { path := dirPath + "/" + filename file, err := fileSystem.Open(path) return path, file, err } } return "", nil, nil } // RenderDirectoryListing provides an io.Reader with the output of a directory listing template. // // The template is provided the following namespace: // - .FullPath: the complete path to the listed directory // - .DirName: the name of the directory itself // - .Entries: the []fs.DirEntry of the directory contents // - .Hostname: the hostname of the server hosting the file // - .Port: the port on which the server is listening // // Each entry in .Entries has the following fields: // - .Name the string name of the item within the directory // - .IsDir is a boolean // - .Type is the FileMode bits // - .Info is a method returning (fs.FileInfo, error) func RenderDirectoryListing( path string, dir fs.ReadDirFile, template *template.Template, server sr.Server, ) (io.Reader, error) { buf := &bytes.Buffer{} environ, err := dirlistNamespace(path, dir, server) if err != nil { return nil, err } if err := template.Execute(buf, environ); err != nil { return nil, err } return buf, nil } func dirlistNamespace(path string, dirFile fs.ReadDirFile, server sr.Server) (map[string]any, error) { entries, err := dirFile.ReadDir(0) if err != nil { return nil, err } sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) var dirname string if path == "." { dirname = "(root)" } else { dirname = path[strings.LastIndex(path, "/")+1:] } hostname := "none" port := "0" if server != nil { hostname = server.Hostname() port = server.Port() } m := map[string]any{ "FullPath": path, "DirName": dirname, "Entries": entries, "Hostname": hostname, "Port": port, } return m, nil }