summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--contrib/cgi/cgi.go16
-rw-r--r--contrib/cgi/gopher.go139
-rw-r--r--contrib/fs/gopher.go183
-rw-r--r--examples/gopher_fileserver/main.go11
-rw-r--r--gopher/gophermap/extended.go47
-rw-r--r--gopher/gophermap/extended_test.go6
-rw-r--r--gopher/gophermap/listdir.go91
-rw-r--r--gopher/gophermap/testdata/customlist_output.gophermap4
-rw-r--r--gopher/gophermap/testdata/file4.txt (renamed from gopher/gophermap/testdata/file4)0
-rw-r--r--gopher/gophermap/testdata/subdir/gophertag1
-rw-r--r--gopher/gophermap/testdata/subdir2/gophermap1
-rw-r--r--gopher/response.go2
-rw-r--r--internal/users.go53
13 files changed, 396 insertions, 158 deletions
diff --git a/contrib/cgi/cgi.go b/contrib/cgi/cgi.go
index bcdd5e1..749a284 100644
--- a/contrib/cgi/cgi.go
+++ b/contrib/cgi/cgi.go
@@ -25,11 +25,11 @@ import (
// a request for /foo/bar/baz can run an executable found at /foo or /foo/bar. In such
// a case the PATH_INFO would include the remaining portion of the URI path.
func ResolveCGI(requestPath, fsRoot string) (string, string, error) {
+ fsRoot = strings.TrimRight(fsRoot, "/")
segments := strings.Split(strings.TrimLeft(requestPath, "/"), "/")
for i := range append(segments, "") {
filepath := strings.Join(append([]string{fsRoot}, segments[:i]...), "/")
- filepath = strings.TrimRight(filepath, "/")
isDir, isExecutable, err := executableFile(filepath)
if err != nil {
return "", "", err
@@ -52,26 +52,20 @@ func ResolveCGI(requestPath, fsRoot string) (string, string, error) {
}
func executableFile(filepath string) (bool, bool, error) {
- file, err := os.Open(filepath)
+ info, err := os.Stat(filepath)
if isNotExistError(err) {
return false, false, nil
}
if err != nil {
return false, false, err
}
- defer file.Close()
-
- info, err := file.Stat()
- if err != nil {
- return false, false, err
- }
if info.IsDir() {
return true, false, nil
}
// readable + executable by anyone
- return false, info.Mode()&0005 == 0005, nil
+ return false, info.Mode()&5 == 5, nil
}
func isNotExistError(err error) bool {
@@ -94,7 +88,7 @@ func RunCGI(
request *sr.Request,
executable string,
pathInfo string,
-) (io.Reader, int, error) {
+) (*bytes.Buffer, int, error) {
pathSegments := strings.Split(executable, "/")
dirPath := "."
@@ -105,7 +99,7 @@ func RunCGI(
infoLen := len(pathInfo)
if pathInfo == "/" {
- infoLen -= 1
+ infoLen = 0
}
scriptName := request.Path[:len(request.Path)-infoLen]
diff --git a/contrib/cgi/gopher.go b/contrib/cgi/gopher.go
index 290adfa..98a3c75 100644
--- a/contrib/cgi/gopher.go
+++ b/contrib/cgi/gopher.go
@@ -3,10 +3,14 @@ package cgi
import (
"context"
"fmt"
+ "os"
+ "path"
+ "path/filepath"
"strings"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gopher"
+ "tildegit.org/tjp/sliderule/gopher/gophermap"
)
// GopherCGIDirectory runs any executable files relative to a root directory on the file system.
@@ -15,31 +19,148 @@ import (
// a request for /foo/bar/baz can also run an executable found at /foo or /foo/bar. In
// such a case the PATH_INFO environment variable will include the remaining portion of
// the URI path.
-func GopherCGIDirectory(pathRoot, fsRoot string) sr.Handler {
+func GopherCGIDirectory(pathRoot, fsRoot string, settings *gophermap.FileSystemSettings) sr.Handler {
+ if settings == nil {
+ settings = &gophermap.FileSystemSettings{}
+ }
+
+ if !settings.Exec {
+ return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { return nil })
+ }
+
fsRoot = strings.TrimRight(fsRoot, "/")
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
if !strings.HasPrefix(request.Path, pathRoot) {
return nil
}
- filepath, pathinfo, err := ResolveCGI(request.Path[len(pathRoot):], fsRoot)
+ fullpath, pathinfo, err := resolveGopherCGI(fsRoot, request)
if err != nil {
return gopher.Error(err).Response()
}
- if filepath == "" {
+ if fullpath == "" {
+ return nil
+ }
+
+ return runGopherCGI(ctx, request, fullpath, pathinfo, *settings)
+ })
+}
+
+// ExecGopherMaps runs any gophermaps
+func ExecGopherMaps(pathRoot, fsRoot string, settings *gophermap.FileSystemSettings) sr.Handler {
+ if settings == nil {
+ settings = &gophermap.FileSystemSettings{}
+ }
+
+ if !settings.Exec {
+ return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { return nil })
+ }
+
+ fsRoot = strings.TrimRight(fsRoot, "/")
+ return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
+ if !strings.HasPrefix(request.Path, pathRoot) {
return nil
}
- stdout, exitCode, err := RunCGI(ctx, request, filepath, pathinfo)
+ fullpath := filepath.Join(fsRoot, strings.Trim(request.Path, "/"))
+ info, err := os.Stat(fullpath)
+ if isNotExistError(err) {
+ return nil
+ }
if err != nil {
return gopher.Error(err).Response()
}
- if exitCode != 0 {
- return gopher.Error(
- fmt.Errorf("CGI process exited with status %d", exitCode),
- ).Response()
+
+ if info.IsDir() {
+ for _, fname := range settings.DirMaps {
+ fpath := filepath.Join(fullpath, fname)
+ finfo, err := os.Stat(fpath)
+ if isNotExistError(err) {
+ continue
+ }
+ if err != nil {
+ return gopher.Error(err).Response()
+ }
+
+ m := finfo.Mode()
+ if m.IsDir() {
+ continue
+ }
+ if !m.IsRegular() || m&5 != 5 {
+ continue
+ }
+ return runGopherCGI(ctx, request, fpath, "/", *settings)
+ }
+
+ return nil
+ }
+
+ m := info.Mode()
+ if !m.IsRegular() || m&5 != 5 {
+ return nil
}
- return gopher.File(0, stdout)
+ return runGopherCGI(ctx, request, fullpath, "/", *settings)
})
}
+
+func runGopherCGI(
+ ctx context.Context,
+ request *sr.Request,
+ fullpath string,
+ pathinfo string,
+ settings gophermap.FileSystemSettings,
+) *sr.Response {
+ stdout, exitCode, err := RunCGI(ctx, request, fullpath, pathinfo)
+ if err != nil {
+ return gopher.Error(err).Response()
+ }
+ if exitCode != 0 {
+ return gopher.Error(
+ fmt.Errorf("CGI process exited with status %d", exitCode),
+ ).Response()
+ }
+
+ if settings.ParseExtended {
+ edoc, err := gophermap.ParseExtended(stdout, request.URL)
+ if err != nil {
+ return gopher.Error(err).Response()
+ }
+
+ doc, _, err := edoc.Compatible(filepath.Dir(fullpath), settings)
+ return doc.Response()
+ }
+
+ return gopher.File(gopher.MenuType, stdout)
+}
+
+func resolveGopherCGI(fsRoot string, request *sr.Request) (string, string, error) {
+ reqPath := strings.TrimLeft(request.Path, "/")
+
+ segments := append([]string{""}, strings.Split(reqPath, "/")...)
+ fullpath := fsRoot
+ for i, segment := range segments {
+ fullpath = filepath.Join(fullpath, segment)
+
+ info, err := os.Stat(fullpath)
+ if isNotExistError(err) {
+ return "", "", nil
+ }
+ if err != nil {
+ return "", "", err
+ }
+
+ if !info.IsDir() {
+ if info.Mode()&5 == 5 {
+ pathinfo := "/"
+ if len(segments) > i+1 {
+ pathinfo = path.Join(segments[i:]...)
+ }
+ return fullpath, pathinfo, nil
+ }
+ break
+ }
+ }
+
+ return "", "", nil
+}
diff --git a/contrib/fs/gopher.go b/contrib/fs/gopher.go
index 0594730..4d86ba6 100644
--- a/contrib/fs/gopher.go
+++ b/contrib/fs/gopher.go
@@ -2,29 +2,56 @@ package fs
import (
"context"
- "io/fs"
+ "os"
+ "path/filepath"
+ "slices"
"strings"
- "text/template"
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(fileSystem fs.FS) sr.Handler {
+func GopherFileHandler(rootpath string, settings *gophermap.FileSystemSettings) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
- filepath, file, err := ResolveFile(request, fileSystem)
+ path := filepath.Join(rootpath, strings.Trim(request.Path, "/"))
+ 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 file == nil {
- return nil
+ 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()
}
- return gopher.File(gopher.GuessItemType(filepath), file)
+ doc, _, err := edoc.Compatible(filepath.Dir(path), *settings)
+ if err != nil {
+ return gopher.Error(err).Response()
+ }
+ return doc.Response()
})
}
@@ -34,95 +61,111 @@ func GopherFileHandler(fileSystem fs.FS) sr.Handler {
// 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 {
+func GopherDirectoryDefault(rootpath string, settings *gophermap.FileSystemSettings) 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()
+ path := filepath.Join(rootpath, strings.Trim(request.Path, "/"))
+ if isPrivate(path) {
+ return nil
}
- if dir == nil {
+ if isd, err := isDir(path); err != nil {
+ return gopher.Error(err).Response()
+ } else if !isd {
return nil
}
- defer func() { _ = dir.Close() }()
- _, file, err := ResolveDirectoryDefault(fileSystem, dirpath, dir, filenames)
- if err != nil {
- return gopher.Error(err).Response()
+ if settings == nil {
+ settings = &gophermap.FileSystemSettings{}
}
- if file == nil {
- return nil
+
+ 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 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.
-//
-// 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 {
+func GopherDirectoryListing(rootpath string, settings *gophermap.FileSystemSettings) 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()
+ path := filepath.Join(rootpath, strings.Trim(request.Path, "/"))
+ if isPrivate(path) {
+ return nil
}
- if dir == nil {
+ if isd, err := isDir(path); err != nil {
+ return gopher.Error(err).Response()
+ } else if !isd {
return nil
}
- defer func() { _ = dir.Close() }()
- if tpl == nil {
- tpl = DefaultGopherDirectoryList
+ if settings == nil {
+ settings = &gophermap.FileSystemSettings{}
}
- body, err := RenderDirectoryListing(dirpath, dir, tpl, request.Server)
+ doc, err := gophermap.ListDir(path, request.URL, *settings)
if err != nil {
return gopher.Error(err).Response()
}
- return gopher.File(gopher.MenuType, body)
+ return doc.Response()
})
}
-// 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(gopher.GuessItemType(filepath))})
- },
+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
}
-// 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",
- ),
- ),
-)
+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 {
+ if strings.HasSuffix(path, ".gophermap") {
+ return true
+ }
+ return slices.Contains(settings.DirMaps, filepath.Base(path))
+}
diff --git a/examples/gopher_fileserver/main.go b/examples/gopher_fileserver/main.go
index 244f61b..057aa21 100644
--- a/examples/gopher_fileserver/main.go
+++ b/examples/gopher_fileserver/main.go
@@ -3,7 +3,6 @@ package main
import (
"context"
"log"
- "os"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/contrib/cgi"
@@ -13,13 +12,11 @@ import (
)
func main() {
- fileSystem := os.DirFS(".")
-
handler := sr.FallthroughHandler(
- fs.GopherDirectoryDefault(fileSystem, "index.gophermap"),
- fs.GopherDirectoryListing(fileSystem, nil),
- cgi.GopherCGIDirectory("/cgi-bin", "./cgi-bin"),
- fs.GopherFileHandler(fileSystem),
+ fs.GopherDirectoryDefault(".", nil),
+ fs.GopherDirectoryListing(".", nil),
+ cgi.GopherCGIDirectory("/cgi-bin", "./cgi-bin", nil),
+ fs.GopherFileHandler(".", nil),
)
_, infoLog, _, errLog := logging.DefaultLoggers()
diff --git a/gopher/gophermap/extended.go b/gopher/gophermap/extended.go
index d9fedd0..a0360fc 100644
--- a/gopher/gophermap/extended.go
+++ b/gopher/gophermap/extended.go
@@ -7,10 +7,12 @@ import (
"net/url"
"os"
"path/filepath"
+ "sort"
"strconv"
"strings"
"tildegit.org/tjp/sliderule/gopher"
+ "tildegit.org/tjp/sliderule/internal"
"tildegit.org/tjp/sliderule/internal/types"
)
@@ -139,6 +141,8 @@ const (
UserListType types.Status = '~'
// VHostListType generates a listing of virtual hosts.
+ //
+ // It is not supported in sliderule.
VHostListType types.Status = '%'
// InclusionType causes another gophermap to be included at this location.
@@ -151,8 +155,16 @@ const (
EndDocType types.Status = '.'
)
+type FileSystemSettings struct {
+ ParseExtended bool
+ Exec bool
+ ListUsers bool
+ DirMaps []string
+ DirTag string
+}
+
// Compatible builds a standards-compliant gophermap from the current extended menu.
-func (edoc ExtendedMapDocument) Compatible(cwd string) (gopher.MapDocument, string, error) {
+func (edoc ExtendedMapDocument) Compatible(cwd string, settings FileSystemSettings) (gopher.MapDocument, string, error) {
doc := gopher.MapDocument{}
title := ""
@@ -172,9 +184,32 @@ func (edoc ExtendedMapDocument) Compatible(cwd string) (gopher.MapDocument, stri
return nil, "", InvalidLine(num)
}
extensions[from] = types.Status(to[0])
- case UserListType: //TODO
- return nil, "", errors.New("User listings '~' are not supported")
- case VHostListType: //TODO
+ case UserListType:
+ if !settings.ListUsers {
+ doc = append(doc, gopher.MapItem{
+ Type: gopher.InfoMessageType,
+ Display: "~",
+ Selector: edoc.Location.Path,
+ Hostname: edoc.Location.Hostname(),
+ Port: edoc.Location.Port(),
+ })
+ }
+
+ users, err := internal.ListUsersWithHomeSubdir("public_gopher", 4)
+ if err != nil {
+ return nil, "", err
+ }
+ sort.Strings(users)
+ for _, user := range users {
+ doc = append(doc, gopher.MapItem{
+ Type: gopher.MenuType,
+ Display: "~" + user,
+ Selector: "/~" + user,
+ Hostname: edoc.Location.Hostname(),
+ Port: edoc.Location.Port(),
+ })
+ }
+ case VHostListType:
return nil, "", errors.New("Virtual host listings '%' are not supported")
case InclusionType:
location := filepath.Join(cwd, item.Selector)
@@ -183,13 +218,13 @@ func (edoc ExtendedMapDocument) Compatible(cwd string) (gopher.MapDocument, stri
return nil, "", err
}
- lines, _, err := subEdoc.Compatible(filepath.Dir(location))
+ lines, _, err := subEdoc.Compatible(filepath.Dir(location), settings)
if err != nil {
return nil, "", err
}
doc = append(doc, lines...)
case DirListType:
- dirlist, err := listDir(cwd, edoc.Location, hidden, extensions)
+ dirlist, err := listDir(cwd, edoc.Location, settings, hidden, extensions)
if err != nil {
return nil, "", err
}
diff --git a/gopher/gophermap/extended_test.go b/gopher/gophermap/extended_test.go
index e956df1..2d9e9b1 100644
--- a/gopher/gophermap/extended_test.go
+++ b/gopher/gophermap/extended_test.go
@@ -31,7 +31,11 @@ func TestExtendedDoc(t *testing.T) {
t.Fatal(err)
}
- doc, _, err := edoc.Compatible("testdata")
+ doc, _, err := edoc.Compatible("testdata", FileSystemSettings{
+ ParseExtended: true,
+ DirMaps: []string{"gophermap"},
+ DirTag: "gophertag",
+ })
if err != nil {
t.Fatal(err)
}
diff --git a/gopher/gophermap/listdir.go b/gopher/gophermap/listdir.go
index 8d66277..a2c5214 100644
--- a/gopher/gophermap/listdir.go
+++ b/gopher/gophermap/listdir.go
@@ -6,6 +6,7 @@ import (
"os"
"path"
"path/filepath"
+ "slices"
"strings"
"tildegit.org/tjp/sliderule/gopher"
@@ -13,11 +14,11 @@ import (
)
// ListDir builds a gopher menu representing the contents of a directory.
-func ListDir(dir string, location *url.URL) (gopher.MapDocument, error) {
- return listDir(dir, location, nil, nil)
+func ListDir(dir string, location *url.URL, settings FileSystemSettings) (gopher.MapDocument, error) {
+ return listDir(dir, location, settings, nil, nil)
}
-func listDir(dir string, location *url.URL, hidden map[string]struct{}, extensions map[string]types.Status) (gopher.MapDocument, error) {
+func listDir(dir string, location *url.URL, settings FileSystemSettings, hidden map[string]struct{}, extensions map[string]types.Status) (gopher.MapDocument, error) {
contents, err := os.ReadDir(dir)
if err != nil {
return nil, err
@@ -28,55 +29,36 @@ func listDir(dir string, location *url.URL, hidden map[string]struct{}, extensio
for _, entry := range contents {
name := entry.Name()
- if _, ok := hidden[name]; ok || name == "gophermap" {
+ inf, err := entry.Info()
+ if err != nil {
+ return nil, err
+ }
+ if inf.Mode()&4 == 0 {
+ continue
+ }
+
+ if _, ok := hidden[name]; ok || slices.Contains(settings.DirMaps, name) {
continue
}
var code types.Status
- ext := strings.TrimPrefix(filepath.Ext(name), ".")
if entry.IsDir() {
code = gopher.MenuType
- } else if c, ok := extensions[ext]; ok && ext != "" {
- code = c
} else {
- switch ext {
- case "gophermap":
- code = gopher.MenuType
- case "exe", "bin", "out":
- code = gopher.BinaryFileType
- case "gif":
- code = gopher.GifFileType
- case "jpg", "jpeg", "tif", "tiff":
- code = gopher.ImageFileType
- case "bmp":
- code = gopher.BitmapType
- case "mp4", "mov", "avi", "wmv", "webm":
- code = gopher.MovieFileType
- case "pcm", "aiff", "mp3", "aac", "ogg", "wma", "flac", "alac":
- code = gopher.SoundFileType
- case "doc", "docx", "odt", "fodt":
- code = gopher.DocumentType
- case "html":
- code = gopher.HTMLType
- case "png":
- code = gopher.PngImageFileType
- case "rtf":
- code = gopher.RtfDocumentType
- case "wav":
- code = gopher.WavSoundFileType
- case "pdf":
- code = gopher.PdfDocumentType
- case "xml", "atom":
- code = gopher.XmlDocumentType
- default:
- code = gopher.TextFileType
+ ext := strings.TrimPrefix(filepath.Ext(name), ".")
+ if c, ok := extensions[ext]; ok {
+ code = c
+ } else if c, ok := extensions[name]; ok {
+ code = c
+ } else {
+ code = gopher.GuessItemType(name)
}
}
doc = append(doc, gopher.MapItem{
Type: code,
- Display: displayName(dir, entry),
+ Display: displayName(dir, entry, settings),
Selector: path.Join(path.Dir(location.Path), name),
Hostname: location.Hostname(),
Port: location.Port(),
@@ -86,32 +68,37 @@ func listDir(dir string, location *url.URL, hidden map[string]struct{}, extensio
return doc, nil
}
-func displayName(dir string, entry os.DirEntry) string {
+func displayName(dir string, entry os.DirEntry, settings FileSystemSettings) string {
fname := entry.Name()
+ fullpath := filepath.Join(dir, fname)
- // if is a gophermap, use !title or filename
- if strings.HasSuffix(fname, ".gophermap") {
- if title := gophermapTitle(dir, fname); title != "" {
+ if entry.Type().IsRegular() && settings.ParseExtended && (strings.HasSuffix(fname, ".gophermap") || slices.Contains(settings.DirMaps, fname)) {
+ if title := gophermapTitle(fullpath); title != "" {
return title
}
- return fname
}
if entry.IsDir() {
- if tag := tagTitle(dir, fname); tag != "" {
- return tag
+ if settings.DirTag != "" {
+ if tag := tagTitle(filepath.Join(fullpath, settings.DirTag)); tag != "" {
+ return tag
+ }
}
- if title := gophermapTitle(dir, filepath.Join(fname, "gophermap")); title != "" {
- return title
+ if settings.ParseExtended {
+ for _, mapname := range settings.DirMaps {
+ if title := gophermapTitle(filepath.Join(fullpath, mapname)); title != "" {
+ return title
+ }
+ }
}
}
return fname
}
-func gophermapTitle(dir, name string) string {
- file, err := os.Open(filepath.Join(dir, name))
+func gophermapTitle(path string) string {
+ file, err := os.Open(path)
if err != nil {
return ""
}
@@ -131,8 +118,8 @@ func gophermapTitle(dir, name string) string {
return strings.TrimRight(line[1:], "\r\n")
}
-func tagTitle(parent, name string) string {
- file, err := os.Open(filepath.Join(parent, name, "gophertag"))
+func tagTitle(path string) string {
+ file, err := os.Open(path)
if err != nil {
return ""
}
diff --git a/gopher/gophermap/testdata/customlist_output.gophermap b/gopher/gophermap/testdata/customlist_output.gophermap
index 1a96644..e5cc99d 100644
--- a/gopher/gophermap/testdata/customlist_output.gophermap
+++ b/gopher/gophermap/testdata/customlist_output.gophermap
@@ -8,5 +8,7 @@ i /customlist.gophermap localhost.localdomain 70
1This is the document title /customlist.gophermap localhost.localdomain 70
1customlist_output.gophermap /customlist_output.gophermap localhost.localdomain 70
9file3.executablething /file3.executablething localhost.localdomain 70
-0file4 /file4 localhost.localdomain 70
+0file4.txt /file4.txt localhost.localdomain 70
+1subdir title /subdir localhost.localdomain 70
+1subdir2 title /subdir2 localhost.localdomain 70
.
diff --git a/gopher/gophermap/testdata/file4 b/gopher/gophermap/testdata/file4.txt
index e69de29..e69de29 100644
--- a/gopher/gophermap/testdata/file4
+++ b/gopher/gophermap/testdata/file4.txt
diff --git a/gopher/gophermap/testdata/subdir/gophertag b/gopher/gophermap/testdata/subdir/gophertag
new file mode 100644
index 0000000..73a248e
--- /dev/null
+++ b/gopher/gophermap/testdata/subdir/gophertag
@@ -0,0 +1 @@
+subdir title
diff --git a/gopher/gophermap/testdata/subdir2/gophermap b/gopher/gophermap/testdata/subdir2/gophermap
new file mode 100644
index 0000000..bba9538
--- /dev/null
+++ b/gopher/gophermap/testdata/subdir2/gophermap
@@ -0,0 +1 @@
+!subdir2 title
diff --git a/gopher/response.go b/gopher/response.go
index 1ad7f1d..46f9db0 100644
--- a/gopher/response.go
+++ b/gopher/response.go
@@ -121,7 +121,7 @@ func Error(err error) *MapItem {
// File builds a minimal response delivering a file's contents.
//
-// Meta is nil and Status is 0 in this response.
+// Meta is nil in this response.
func File(status types.Status, contents io.Reader) *types.Response {
return &types.Response{Status: status, Body: contents}
}
diff --git a/internal/users.go b/internal/users.go
new file mode 100644
index 0000000..e7a25a3
--- /dev/null
+++ b/internal/users.go
@@ -0,0 +1,53 @@
+package internal
+
+import (
+ "bufio"
+ "errors"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// ListUsersWithHomeSubdir provides a list of user names on the current host.
+//
+// The users returned will only be those with a specifically named subdirectory
+// of their home dir, which has the given permission bits.
+func ListUsersWithHomeSubdir(subdir string, required_perms os.FileMode) ([]string, error) {
+ file, err := os.Open("/etc/passwd")
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ _ = file.Close()
+ }()
+
+ users := []string{}
+ rdr := bufio.NewReader(file)
+ for {
+ line, err := rdr.ReadString('\n')
+ isEOF := errors.Is(err, io.EOF)
+ if err != nil && !isEOF {
+ return nil, err
+ }
+
+ if len(line) != 0 && line[0] == '#' {
+ continue
+ }
+
+ spl := strings.Split(line, ":")
+ home := spl[len(spl)-2]
+ st, err := os.Stat(filepath.Join(home, subdir))
+ if err != nil || !st.IsDir() || st.Mode()&required_perms != required_perms {
+ continue
+ }
+
+ users = append(users, spl[0])
+
+ if isEOF {
+ break
+ }
+ }
+
+ return users, nil
+}