Support directory listing

Resolves #5.
This commit is contained in:
Trevor Slocum 2020-10-30 18:31:13 -07:00
parent d1f9c6d4aa
commit e7cebf095e
4 changed files with 114 additions and 21 deletions

View file

@ -68,6 +68,8 @@ When accessing a directory the file `index.gemini` or `index.gmi` is served.
Serve static files from specified root directory. Serve static files from specified root directory.
Directory listing may be enabled by adding `listdirectory: true`.
#### Proxy #### Proxy
Forward request to Gemini server at specified URL. Forward request to Gemini server at specified URL.
@ -96,6 +98,7 @@ hosts:
- -
path: /sites path: /sites
root: /home/gemini.rocks/data/sites root: /home/gemini.rocks/data/sites
listdirectory: true
- -
path: ^/(help|info)$ path: ^/(help|info)$
root: /home/gemini.rocks/data/help root: /home/gemini.rocks/data/help

View file

@ -22,6 +22,9 @@ type pathConfig struct {
Proxy string Proxy string
Command string Command string
// List directory entries
ListDirectory bool
r *regexp.Regexp r *regexp.Regexp
cmd []string cmd []string
} }

112
server.go
View file

@ -12,6 +12,8 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"path/filepath"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -48,7 +50,66 @@ func writeStatus(c net.Conn, code int) {
writeHeader(c, code, meta) 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) fi, err := os.Stat(filePath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
writeStatus(c, gemini.StatusNotFound) writeStatus(c, gemini.StatusNotFound)
@ -58,6 +119,9 @@ func serveFile(c net.Conn, requestData, filePath string) {
return return
} }
originalPath := filePath
var fetchIndex bool
if mode := fi.Mode(); mode.IsDir() { if mode := fi.Mode(); mode.IsDir() {
if requestData[len(requestData)-1] != '/' { if requestData[len(requestData)-1] != '/' {
// Add trailing slash // Add trailing slash
@ -66,6 +130,8 @@ func serveFile(c net.Conn, requestData, filePath string) {
return return
} }
fetchIndex = true
_, err := os.Stat(path.Join(filePath, "index.gemini")) _, err := os.Stat(path.Join(filePath, "index.gemini"))
if err == nil { if err == nil {
filePath = path.Join(filePath, "index.gemini") filePath = path.Join(filePath, "index.gemini")
@ -76,6 +142,10 @@ func serveFile(c net.Conn, requestData, filePath string) {
fi, err = os.Stat(filePath) fi, err = os.Stat(filePath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
if fetchIndex && listDir {
serveDirectory(c, request, originalPath)
return
}
writeStatus(c, gemini.StatusNotFound) writeStatus(c, gemini.StatusNotFound)
return return
} else if err != nil { } else if err != nil {
@ -268,29 +338,29 @@ func handleConn(c net.Conn) {
matchedHost = true matchedHost = true
for _, serve := range config.Hosts[hostname] { 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) { 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 return
} else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) { } 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 return
} }
} }

17
util.go Normal file
View file

@ -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])
}