engine/dirhandler.go
Rama main · réplica de lectura del repositorio original.
package engine
// Directory Index
import (
"bytes"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/go-gcfg/gcfg"
"github.com/sirupsen/logrus"
"github.com/xyproto/algernon/platformdep"
"github.com/xyproto/algernon/themes"
"github.com/xyproto/algernon/utils"
)
const (
dotSlash = "." + utils.Pathsep /* ./ */
doubleP = utils.Pathsep + utils.Pathsep /* // */
)
// List of filenames that should be displayed instead of a directory listing
var indexFilenames = []string{"index.lua", "index.html", "index.md", "index.txt", "index.pongo2", "index.tmpl", "index.po2", "index.amber", "index.jsx", "index.tsx", "index.happ", "index.hyper", "index.hyper.js", "index.hyper.jsx", "index.tl", "index.prompt"}
// DirConfig keeps a directory listing configuration
type DirConfig struct {
Main struct {
Title string
Theme string
}
}
// dirConfigEntry is a cached parsed .algernon configuration with mtime
type dirConfigEntry struct {
mtime time.Time
config DirConfig
}
// dirConfigCache caches parsed .algernon configurations by filename and mtime
type dirConfigCache struct {
entries map[string]dirConfigEntry
mu sync.RWMutex
}
// DirectoryListing serves the given directory as a web page with links the the contents
func (ac *Config) DirectoryListing(w http.ResponseWriter, req *http.Request, rootdir, dirname, theme string) {
var (
buf bytes.Buffer
fullFilename string
URLpath string
title = dirname
)
// Remove a trailing slash after the root directory, if present
rootdir = strings.TrimSuffix(rootdir, "/")
// Read ignore patterns if ignore.txt is present
var ignorePatterns []string
ignoreFilePath := filepath.Join(dirname, platformdep.IgnoreFilename)
if ac.fs.Exists(ignoreFilePath) {
patterns, err := ReadIgnoreFile(ignoreFilePath)
if err != nil {
logrus.Warn("Could not read ignore.txt: ", err)
} else {
ignorePatterns = patterns
}
}
// Fill the coming HTML body with a list of all the filenames in `dirname`
for _, filename := range utils.GetFilenames(dirname) {
if filename == platformdep.DirConfFilename || filename == platformdep.IgnoreFilename {
// Skip
continue
}
// Skip dotfiles/dot-directories in listings if --hide-dotfiles is set
if ac.hideDotfiles && strings.HasPrefix(filename, ".") {
continue
}
// Check if the filename matches any of the ignore patterns
if ShouldIgnore(filename, ignorePatterns) {
continue
}
// Find the full name
fullFilename = dirname
// Add a "/" after the directory name, if missing
if !strings.HasSuffix(fullFilename, utils.Pathsep) {
fullFilename += utils.Pathsep
}
// Add the filename at the end
fullFilename += filename
// Remove the root directory from the link path
URLpath = fullFilename[len(rootdir)+1:]
// Output different entries for files and directories
buf.WriteString(themes.HTMLLinkWithBase(filename, ac.dirBaseURL, URLpath, ac.fs.IsDir(fullFilename)))
}
// Read directory configuration, if present
if fullDirConfFilename := filepath.Join(dirname, platformdep.DirConfFilename); ac.fs.Exists(fullDirConfFilename) {
dirConf := ac.readDirConfig(fullDirConfFilename)
if dirConf.Main.Title != "" {
title = dirConf.Main.Title
}
if dirConf.Main.Theme != "" {
theme = dirConf.Main.Theme
}
} else {
// Strip the leading "./" from the current directory
title = strings.TrimPrefix(title, dotSlash)
// Replace "//" with just "/"
title = strings.ReplaceAll(title, doubleP, utils.Pathsep)
}
// Check if the current page contents are empty
if buf.Len() == 0 {
buf.WriteString("Empty directory")
}
htmldata := themes.MessagePageBytes(title, buf.Bytes(), theme)
// If the auto-refresh feature has been enabled
if ac.autoRefresh {
// Insert JavaScript for refreshing the page into the generated HTML
htmldata = ac.InsertAutoRefresh(req, htmldata)
}
// Serve the page
w.Header().Add(contentType, htmlUTF8)
ac.DataToClient(w, req, dirname, htmldata)
}
// DirPage serves a directory, using index.* files, if present.
// The directory must exist.
// rootdir is the base directory (can be ".")
// dirname is the specific directory that is to be served (should never be ".")
func (ac *Config) DirPage(w http.ResponseWriter, req *http.Request, rootdir, dirname, theme, luaDataFilename string) {
// Check if we are instructed to quit after serving the first file
if ac.quitAfterFirstRequest {
go ac.quitSoon("Quit after first request", defaultSoonDuration)
}
// If the URL does not end with a slash, redirect to an URL that does
if !strings.HasSuffix(req.URL.Path, "/") {
if req.Method == "POST" {
logrus.Warn("Redirecting a POST request: " + req.URL.Path + " -> " + req.URL.Path + "/.")
logrus.Warn("Header data may be lost! Please add the missing slash.")
}
http.Redirect(w, req, req.URL.Path+"/", http.StatusMovedPermanently)
return
}
// Handle the serving of index files, if needed
var filename string
for _, indexfile := range indexFilenames {
filename = filepath.Join(dirname, indexfile)
if ac.fs.Exists(filename) && (!ac.strictSymlinks || utils.ResolvesWithinDir(rootdir, filename)) {
ac.FilePage(w, req, filename, luaDataFilename)
return
}
}
// Serve handler.lua, if found in parent directories (within server root only)
rootAbs, err := filepath.Abs(rootdir)
if err != nil {
rootAbs = rootdir
}
ancestor, err := filepath.Abs(dirname)
if err != nil {
ancestor = dirname
}
for {
// Stop before leaving the configured server root
rel, relErr := filepath.Rel(rootAbs, ancestor)
if relErr != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
break
}
filename = filepath.Join(ancestor, "handler.lua")
if ac.fs.Exists(filename) && (!ac.strictSymlinks || utils.ResolvesWithinDir(rootAbs, filename)) {
ac.FilePage(w, req, filename, luaDataFilename)
return
}
if ancestor == rootAbs {
break
}
parent := filepath.Dir(ancestor)
if parent == ancestor { // hit filesystem root
break
}
ancestor = parent
}
// Serve a directory listing if no index file is found
ac.DirectoryListing(w, req, rootdir, dirname, theme)
}
// readDirConfig reads and parses the .algernon configuration file, caching the
// parsed result by filename and mtime. Returns an empty DirConfig if the file
// cannot be read or parsed.
func (ac *Config) readDirConfig(filename string) DirConfig {
// Check mtime
info, err := os.Stat(filename)
if err != nil {
return DirConfig{}
}
mtime := info.ModTime()
// Return cached entry if mtime has not changed
if ac.dirConfCache != nil {
ac.dirConfCache.mu.RLock()
if entry, ok := ac.dirConfCache.entries[filename]; ok && entry.mtime.Equal(mtime) {
ac.dirConfCache.mu.RUnlock()
return entry.config
}
ac.dirConfCache.mu.RUnlock()
}
// Read and parse
var dirConf DirConfig
block, err := ac.cache.Read(filename, ac.shouldCache(".algernon"))
if err != nil {
return dirConf
}
gcfg.ReadStringInto(&dirConf, string(block.Bytes()))
// Store in cache
if ac.dirConfCache == nil {
ac.dirConfCache = &dirConfigCache{entries: make(map[string]dirConfigEntry)}
}
ac.dirConfCache.mu.Lock()
ac.dirConfCache.entries[filename] = dirConfigEntry{config: dirConf, mtime: mtime}
ac.dirConfCache.mu.Unlock()
return dirConf
}