From e7cebf095efadad8fd862705c6aae128f669fe60 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Fri, 30 Oct 2020 18:31:13 -0700 Subject: [PATCH] Support directory listing Resolves #5. --- CONFIGURATION.md | 3 ++ config.go | 3 ++ server.go | 112 ++++++++++++++++++++++++++++++++++++++--------- util.go | 17 +++++++ 4 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 util.go diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 3031d3c..ff7d26b 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -68,6 +68,8 @@ When accessing a directory the file `index.gemini` or `index.gmi` is served. Serve static files from specified root directory. +Directory listing may be enabled by adding `listdirectory: true`. + #### Proxy Forward request to Gemini server at specified URL. @@ -96,6 +98,7 @@ hosts: - path: /sites root: /home/gemini.rocks/data/sites + listdirectory: true - path: ^/(help|info)$ root: /home/gemini.rocks/data/help diff --git a/config.go b/config.go index bcb2b1d..ee05aae 100644 --- a/config.go +++ b/config.go @@ -22,6 +22,9 @@ type pathConfig struct { Proxy string Command string + // List directory entries + ListDirectory bool + r *regexp.Regexp cmd []string } diff --git a/server.go b/server.go index 17c11d0..375253f 100644 --- a/server.go +++ b/server.go @@ -12,6 +12,8 @@ import ( "os" "os/exec" "path" + "path/filepath" + "sort" "strconv" "strings" "time" @@ -48,7 +50,66 @@ func writeStatus(c net.Conn, code int) { writeHeader(c, code, meta) } -func serveFile(c net.Conn, requestData, filePath string) { +func serveDirectory(c net.Conn, request *url.URL, dirPath string) { + var files []os.FileInfo + err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } else if path == dirPath { + return nil + } + files = append(files, info) + if info.IsDir() { + return filepath.SkipDir + } + return nil + }) + if err != nil { + writeStatus(c, gemini.StatusTemporaryFailure) + return + } + // List directories first + sort.Slice(files, func(i, j int) bool { + iDir := files[i].IsDir() || files[i].Mode()&os.ModeSymlink != 0 + jDir := files[j].IsDir() || files[j].Mode()&os.ModeSymlink != 0 + if iDir != jDir { + return iDir + } + return i < j + }) + + writeHeader(c, gemini.StatusSuccess, "text/gemini; charset=utf-8") + + c.Write([]byte("# " + request.Path + "\r\n\r\n")) + + if request.Path != "/" { + c.Write([]byte("=> ../ ../\r\n\r\n")) + } + + for _, info := range files { + fileName := info.Name() + filePath := url.PathEscape(info.Name()) + if info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + fileName += "/" + filePath += "/" + } + + c.Write([]byte("=> " + fileName + " " + filePath + "\r\n")) + + if info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + c.Write([]byte("\r\n")) + continue + } + + modified := "Never" + if !info.ModTime().IsZero() { + modified = info.ModTime().Format("2006-01-02 3:04 PM") + } + c.Write([]byte(modified + " - " + formatFileSize(info.Size()) + "\r\n\r\n")) + } +} + +func serveFile(c net.Conn, request *url.URL, requestData, filePath string, listDir bool) { fi, err := os.Stat(filePath) if os.IsNotExist(err) { writeStatus(c, gemini.StatusNotFound) @@ -58,6 +119,9 @@ func serveFile(c net.Conn, requestData, filePath string) { return } + originalPath := filePath + + var fetchIndex bool if mode := fi.Mode(); mode.IsDir() { if requestData[len(requestData)-1] != '/' { // Add trailing slash @@ -66,6 +130,8 @@ func serveFile(c net.Conn, requestData, filePath string) { return } + fetchIndex = true + _, err := os.Stat(path.Join(filePath, "index.gemini")) if err == nil { filePath = path.Join(filePath, "index.gemini") @@ -76,6 +142,10 @@ func serveFile(c net.Conn, requestData, filePath string) { fi, err = os.Stat(filePath) if os.IsNotExist(err) { + if fetchIndex && listDir { + serveDirectory(c, request, originalPath) + return + } writeStatus(c, gemini.StatusNotFound) return } else if err != nil { @@ -268,29 +338,29 @@ func handleConn(c net.Conn) { matchedHost = true for _, serve := range config.Hosts[hostname] { - if serve.Proxy != "" { - if serve.r != nil && serve.r.Match(pathBytes) { - serveProxy(c, requestData, serve.Proxy) - return - } else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) { - serveProxy(c, requestData, serve.Proxy) - return - } - } else if serve.cmd != nil { - if serve.r != nil && serve.r.Match(pathBytes) { - serveCommand(c, serve.cmd) - return - } else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) { - serveCommand(c, serve.cmd) - return - } - } - if serve.r != nil && serve.r.Match(pathBytes) { - serveFile(c, requestData, path.Join(serve.Root, strippedPath)) + if serve.Proxy != "" { + serveProxy(c, requestData, serve.Proxy) + return + } else if serve.cmd != nil { + serveCommand(c, serve.cmd) + return + } + serveFile(c, request, requestData, path.Join(serve.Root, strippedPath), serve.ListDirectory) return } else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) { - serveFile(c, requestData, path.Join(serve.Root, strippedPath[len(serve.Path)-1:])) + if serve.Proxy != "" { + serveProxy(c, requestData, serve.Proxy) + return + } else if serve.cmd != nil { + serveCommand(c, serve.cmd) + return + } + filePath := request.Path[len(serve.Path):] + if len(filePath) > 0 && filePath[0] == '/' { + filePath = filePath[1:] + } + serveFile(c, request, requestData, path.Join(serve.Root, filePath), serve.ListDirectory) return } } diff --git a/util.go b/util.go new file mode 100644 index 0000000..8809cdf --- /dev/null +++ b/util.go @@ -0,0 +1,17 @@ +package main + +import "fmt" + +func formatFileSize(b int64) string { + const unit = 1000 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.0f %cB", + float64(b)/float64(div), "KMGTPE"[exp]) +}