caddy/middleware/browse/browse.go

260 lines
5.0 KiB
Go
Raw Normal View History

// Package browse provides middleware for listing files in a directory
// when directory path is requested instead of a specific file.
package browse
import (
"fmt"
"html/template"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"
"github.com/mholt/caddy/middleware"
)
// Browse is an http.Handler that can show a file listing when
// directories in the given paths are specified.
type Browse struct {
Next http.HandlerFunc
Root string
Configs []BrowseConfig
}
// BrowseConfig is a configuration for browsing in a particular path.
type BrowseConfig struct {
PathScope string
Template *template.Template
}
// A Listing is used to fill out a template.
type Listing struct {
// The name of the directory (the last element of the path)
Name string
// The full path of the request
Path string
// Whether the parent directory is browsable
CanGoUp bool
// The items (files and folders) in the path
Items []FileInfo
}
// FileInfo is the info about a particular file or directory
type FileInfo struct {
IsDir bool
Name string
Size int64
URL string
ModTime time.Time
Mode os.FileMode
}
var IndexPages = []string{
"index.html",
"index.htm",
"default.html",
"default.htm",
}
// New creates a new instance of browse middleware.
func New(c middleware.Controller) (middleware.Middleware, error) {
configs, err := parse(c)
if err != nil {
return nil, err
}
browse := Browse{
Root: c.Root(),
Configs: configs,
}
return func(next http.HandlerFunc) http.HandlerFunc {
browse.Next = next
return browse.ServeHTTP
}, nil
}
// ServeHTTP implements the http.Handler interface.
func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) {
filename := b.Root + r.URL.Path
info, err := os.Stat(filename)
if err != nil {
// TODO: 404 Not Found
b.Next(w, r)
return
}
if !info.IsDir() {
b.Next(w, r)
return
}
// See if there's a browse configuration to match the path
for _, bc := range b.Configs {
if !middleware.Path(r.URL.Path).Matches(bc.PathScope) {
continue
}
// Browsing navigation gets messed up if browsing a directory
// that doesn't end in "/" (which it should, anyway)
if r.URL.Path[len(r.URL.Path)-1] != '/' {
http.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
return
}
// Load directory contents
file, err := os.Open(b.Root + r.URL.Path)
if err != nil {
panic(err) // TODO
}
defer file.Close()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
files, err := file.Readdir(-1)
if err != nil || len(files) == 0 {
// TODO - second condition may not be necessary? See docs...
}
// Assemble listing of directory contents
var fileinfos []FileInfo
var abort bool // we bail early if we find an index file
for _, f := range files {
name := f.Name()
// Directory is not browseable if it contains index file
for _, indexName := range IndexPages {
if name == indexName {
abort = true
break
}
}
if abort {
break
}
if f.IsDir() {
name += "/"
}
url := url.URL{Path: name}
fileinfos = append(fileinfos, FileInfo{
IsDir: f.IsDir(),
Name: f.Name(),
Size: f.Size(),
URL: url.String(),
ModTime: f.ModTime(),
Mode: f.Mode(),
})
}
if abort {
// this dir has an index file, so not browsable
continue
}
// Determine if user can browse up another folder
var canGoUp bool
curPath := strings.TrimSuffix(r.URL.Path, "/")
for _, other := range b.Configs {
if strings.HasPrefix(path.Dir(curPath), other.PathScope) {
canGoUp = true
break
}
}
listing := Listing{
Name: path.Base(r.URL.Path),
Path: r.URL.Path,
CanGoUp: canGoUp,
Items: fileinfos,
}
err = bc.Template.Execute(w, listing)
if err != nil {
panic(err) // TODO
}
return
}
// Didn't qualify; pass-thru
b.Next(w, r)
}
// parse returns a list of browsing configurations
func parse(c middleware.Controller) ([]BrowseConfig, error) {
var configs []BrowseConfig
appendCfg := func(bc BrowseConfig) error {
for _, c := range configs {
if c.PathScope == bc.PathScope {
return fmt.Errorf("Duplicate browsing config for %s", c.PathScope)
}
}
configs = append(configs, bc)
return nil
}
for c.Next() {
var bc BrowseConfig
if !c.NextArg() {
bc.PathScope = "/"
err := appendCfg(bc)
if err != nil {
return configs, err
}
continue
}
bc.PathScope = c.Val()
if !c.NextArg() {
err := appendCfg(bc)
if err != nil {
return configs, err
}
continue
}
tplFile := c.Val()
var tplText string
if tplFile != "" {
tplBytes, err := ioutil.ReadFile(tplFile)
if err != nil {
return configs, err
}
tplText = string(tplBytes)
} else {
tplText = defaultTemplate
}
tpl, err := template.New("listing").Parse(tplText)
if err != nil {
return configs, err
}
bc.Template = tpl
err = appendCfg(bc)
if err != nil {
return configs, err
}
}
return configs, nil
}
const defaultTemplate = `
{{range .}}
{{.Name}}<br>
{{end}}
`