some updates
parent
0146f4324f
commit
4efb4f2a53
10
README.md
10
README.md
|
@ -2,3 +2,13 @@
|
|||
|
||||
[![community](https://img.shields.io/badge/community-forum-ff69b4.svg?style=flat-square)](https://forum.caddyserver.com)
|
||||
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/hacdias/caddy-filemanager)
|
||||
|
||||
This is just a concept of syntax:
|
||||
|
||||
```
|
||||
filemanager {
|
||||
show path
|
||||
on url
|
||||
styles filepath
|
||||
}
|
||||
```
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -5,9 +5,9 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="stylesheet" href="/_filemanager_internal/styles.css">
|
||||
<style>
|
||||
|
||||
</style>
|
||||
<style>
|
||||
{{ .StyleSheet }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="0" width="0" style="position: absolute;">
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package filemanager
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// HumanSize returns the size of the file as a human-readable string
|
||||
// in IEC format (i.e. power of 2 or base 1024).
|
||||
func (fi FileInfo) HumanSize() string {
|
||||
return humanize.IBytes(uint64(fi.Size))
|
||||
}
|
||||
|
||||
// HumanModTime returns the modified time of the file as a human-readable string.
|
||||
func (fi FileInfo) HumanModTime(format string) string {
|
||||
return fi.ModTime.Format(format)
|
||||
}
|
229
filemanager.go
229
filemanager.go
|
@ -1,6 +1,6 @@
|
|||
//go:generate go get github.com/jteeuwen/go-bindata
|
||||
//go:generate go install github.com/jteeuwen/go-bindata/go-bindata
|
||||
//go:generate go-bindata -pkg assets -prefix "assets/source" -o assets/assets.go assets/source/...
|
||||
//go:generate go-bindata -debug -pkg assets -prefix "assets/source" -o assets/assets.go assets/source/...
|
||||
|
||||
// Package filemanager provides middleware for managing files in a directory
|
||||
// when directory path is requested instead of a specific file. Based on browse
|
||||
|
@ -10,22 +10,20 @@ package filemanager
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/hacdias/caddy-filemanager/assets"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
// Template used to show FileManager
|
||||
var Template *template.Template
|
||||
|
||||
// FileManager is an http.Handler that can show a file listing when
|
||||
// directories in the given paths are specified.
|
||||
type FileManager struct {
|
||||
|
@ -36,94 +34,11 @@ type FileManager struct {
|
|||
|
||||
// Config is a configuration for browsing in a particular path.
|
||||
type Config struct {
|
||||
PathScope string
|
||||
Root http.FileSystem
|
||||
Variables interface{}
|
||||
Template *template.Template
|
||||
}
|
||||
|
||||
// A Listing is the context 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
|
||||
|
||||
// The number of directories in the listing
|
||||
NumDirs int
|
||||
|
||||
// The number of files (items that aren't directories) in the listing
|
||||
NumFiles int
|
||||
|
||||
// Which sorting order is used
|
||||
Sort string
|
||||
|
||||
// And which order
|
||||
Order string
|
||||
|
||||
// If ≠0 then Items have been limited to that many elements
|
||||
ItemsLimitedTo int
|
||||
|
||||
// Optional custom variables for use in browse templates
|
||||
User interface{}
|
||||
|
||||
httpserver.Context
|
||||
}
|
||||
|
||||
// BreadcrumbMap returns l.Path where every element is a map
|
||||
// of URLs and path segment names.
|
||||
func (l Listing) BreadcrumbMap() map[string]string {
|
||||
result := map[string]string{}
|
||||
|
||||
if len(l.Path) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
// skip trailing slash
|
||||
lpath := l.Path
|
||||
if lpath[len(lpath)-1] == '/' {
|
||||
lpath = lpath[:len(lpath)-1]
|
||||
}
|
||||
|
||||
parts := strings.Split(lpath, "/")
|
||||
for i, part := range parts {
|
||||
if i == 0 && part == "" {
|
||||
// Leading slash (root)
|
||||
result["/"] = "/"
|
||||
continue
|
||||
}
|
||||
result[strings.Join(parts[:i+1], "/")] = part
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// HumanSize returns the size of the file as a human-readable string
|
||||
// in IEC format (i.e. power of 2 or base 1024).
|
||||
func (fi FileInfo) HumanSize() string {
|
||||
return humanize.IBytes(uint64(fi.Size))
|
||||
}
|
||||
|
||||
// HumanModTime returns the modified time of the file as a human-readable string.
|
||||
func (fi FileInfo) HumanModTime(format string) string {
|
||||
return fi.ModTime.Format(format)
|
||||
PathScope string
|
||||
Root http.FileSystem
|
||||
BaseURL string
|
||||
StyleSheet string
|
||||
Variables interface{}
|
||||
}
|
||||
|
||||
func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string) (Listing, bool) {
|
||||
|
@ -175,64 +90,62 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string) (Listin
|
|||
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
|
||||
// If so, control is handed over to ServeListing.
|
||||
func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var bc *Config
|
||||
var fmc *Config
|
||||
// See if there's a browse configuration to match the path
|
||||
for i := range f.Configs {
|
||||
if httpserver.Path(r.URL.Path).Matches(f.Configs[i].PathScope) {
|
||||
bc = &f.Configs[i]
|
||||
goto inScope
|
||||
if httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
|
||||
fmc = &f.Configs[i]
|
||||
|
||||
// Browse works on existing directories; delegate everything else
|
||||
requestedFilepath, err := fmc.Root.Open(strings.Replace(r.URL.Path, fmc.BaseURL, "", 1))
|
||||
if err != nil {
|
||||
switch {
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden, err
|
||||
case os.IsExist(err):
|
||||
return http.StatusNotFound, err
|
||||
default:
|
||||
return f.Next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
defer requestedFilepath.Close()
|
||||
|
||||
info, err := requestedFilepath.Stat()
|
||||
if err != nil {
|
||||
switch {
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden, err
|
||||
case os.IsExist(err):
|
||||
return http.StatusGone, err
|
||||
default:
|
||||
return f.Next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return f.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Do not reply to anything else because it might be nonsensical
|
||||
switch r.Method {
|
||||
case http.MethodGet, http.MethodHead:
|
||||
// proceed, noop
|
||||
case "PROPFIND", http.MethodOptions:
|
||||
return http.StatusNotImplemented, nil
|
||||
default:
|
||||
return f.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Browsing navigation gets messed up if browsing a directory
|
||||
// that doesn't end in "/" (which it should, anyway)
|
||||
if !strings.HasSuffix(r.URL.Path, "/") {
|
||||
http.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return f.ServeListing(w, r, requestedFilepath, fmc)
|
||||
}
|
||||
}
|
||||
return f.Next.ServeHTTP(w, r)
|
||||
inScope:
|
||||
|
||||
// Browse works on existing directories; delegate everything else
|
||||
requestedFilepath, err := bc.Root.Open(r.URL.Path)
|
||||
if err != nil {
|
||||
switch {
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden, err
|
||||
case os.IsExist(err):
|
||||
return http.StatusNotFound, err
|
||||
default:
|
||||
return f.Next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
defer requestedFilepath.Close()
|
||||
|
||||
info, err := requestedFilepath.Stat()
|
||||
if err != nil {
|
||||
switch {
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden, err
|
||||
case os.IsExist(err):
|
||||
return http.StatusGone, err
|
||||
default:
|
||||
return f.Next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return f.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Do not reply to anything else because it might be nonsensical
|
||||
switch r.Method {
|
||||
case http.MethodGet, http.MethodHead:
|
||||
// proceed, noop
|
||||
case "PROPFIND", http.MethodOptions:
|
||||
return http.StatusNotImplemented, nil
|
||||
default:
|
||||
return f.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Browsing navigation gets messed up if browsing a directory
|
||||
// that doesn't end in "/" (which it should, anyway)
|
||||
if !strings.HasSuffix(r.URL.Path, "/") {
|
||||
http.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return f.ServeListing(w, r, requestedFilepath, bc)
|
||||
}
|
||||
|
||||
func (f FileManager) loadDirectoryContents(requestedFilepath http.File, urlPath string) (*Listing, bool, error) {
|
||||
|
@ -316,26 +229,6 @@ func (f FileManager) ServeListing(w http.ResponseWriter, r *http.Request, reques
|
|||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
// serveAssets handles the /{admin}/assets requests
|
||||
func serveAssets(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
filename := strings.Replace(r.URL.Path, assetsURL, "", 1)
|
||||
file, err := assets.Asset(filename)
|
||||
|
||||
if err != nil {
|
||||
return 404, nil
|
||||
}
|
||||
|
||||
// Get the file extension ant its mime type
|
||||
extension := filepath.Ext(filename)
|
||||
mime := mime.TypeByExtension(extension)
|
||||
|
||||
// Write the header with the Content-Type and write the file
|
||||
// content to the buffer
|
||||
w.Header().Set("Content-Type", mime)
|
||||
w.Write(file)
|
||||
return 200, nil
|
||||
}
|
||||
|
||||
func (f FileManager) formatAsJSON(listing *Listing, bc *Config) (*bytes.Buffer, error) {
|
||||
marsh, err := json.Marshal(listing.Items)
|
||||
if err != nil {
|
||||
|
@ -349,6 +242,6 @@ func (f FileManager) formatAsJSON(listing *Listing, bc *Config) (*bytes.Buffer,
|
|||
|
||||
func (f FileManager) formatAsHTML(listing *Listing, bc *Config) (*bytes.Buffer, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
err := bc.Template.Execute(buf, listing)
|
||||
err := Template.Execute(buf, listing)
|
||||
return buf, err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
package filemanager
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// A Listing is the context 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
|
||||
|
||||
// The number of directories in the listing
|
||||
NumDirs int
|
||||
|
||||
// The number of files (items that aren't directories) in the listing
|
||||
NumFiles int
|
||||
|
||||
// Which sorting order is used
|
||||
Sort string
|
||||
|
||||
// And which order
|
||||
Order string
|
||||
|
||||
// If ≠0 then Items have been limited to that many elements
|
||||
ItemsLimitedTo int
|
||||
|
||||
// Optional custom variables for use in browse templates
|
||||
User interface{}
|
||||
|
||||
// StyleSheet to costumize the page
|
||||
StyleSheet string
|
||||
|
||||
httpserver.Context
|
||||
}
|
||||
|
||||
// BreadcrumbMap returns l.Path where every element is a map
|
||||
// of URLs and path segment names.
|
||||
func (l Listing) BreadcrumbMap() map[string]string {
|
||||
result := map[string]string{}
|
||||
|
||||
if len(l.Path) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
// skip trailing slash
|
||||
lpath := l.Path
|
||||
if lpath[len(lpath)-1] == '/' {
|
||||
lpath = lpath[:len(lpath)-1]
|
||||
}
|
||||
|
||||
parts := strings.Split(lpath, "/")
|
||||
for i, part := range parts {
|
||||
if i == 0 && part == "" {
|
||||
// Leading slash (root)
|
||||
result["/"] = "/"
|
||||
continue
|
||||
}
|
||||
result[strings.Join(parts[:i+1], "/")] = part
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
88
setup.go
88
setup.go
|
@ -22,6 +22,20 @@ func init() {
|
|||
|
||||
// setup configures a new Browse middleware instance.
|
||||
func setup(c *caddy.Controller) error {
|
||||
// Second argument would be the template file to use
|
||||
tplBytes, err := assets.Asset("template.tmpl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tplText := string(tplBytes)
|
||||
|
||||
// Build the template
|
||||
tpl, err := template.New("listing").Parse(tplText)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Template = tpl
|
||||
|
||||
configs, err := fileManagerParse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -43,65 +57,63 @@ func setup(c *caddy.Controller) error {
|
|||
func fileManagerParse(c *caddy.Controller) ([]Config, error) {
|
||||
var configs []Config
|
||||
|
||||
cfg := httpserver.GetConfig(c.Key)
|
||||
|
||||
appendCfg := func(bc Config) error {
|
||||
appendCfg := func(fmc Config) error {
|
||||
for _, c := range configs {
|
||||
if c.PathScope == bc.PathScope {
|
||||
return fmt.Errorf("duplicate browsing config for %s", c.PathScope)
|
||||
if c.PathScope == fmc.PathScope {
|
||||
return fmt.Errorf("duplicate file managing config for %s", c.PathScope)
|
||||
}
|
||||
}
|
||||
configs = append(configs, bc)
|
||||
configs = append(configs, fmc)
|
||||
return nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
var bc Config
|
||||
var fmc = Config{
|
||||
PathScope: ".",
|
||||
BaseURL: "/",
|
||||
StyleSheet: "",
|
||||
}
|
||||
var styles string
|
||||
|
||||
// First argument is directory to allow browsing; default is site root
|
||||
if c.NextArg() {
|
||||
bc.PathScope = c.Val()
|
||||
} else {
|
||||
bc.PathScope = "/"
|
||||
}
|
||||
bc.Root = http.Dir(cfg.Root)
|
||||
theRoot, err := bc.Root.Open("/") // catch a missing path early
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
defer theRoot.Close()
|
||||
_, err = theRoot.Readdir(-1)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "show":
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
fmc.PathScope = c.Val()
|
||||
case "on":
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
fmc.BaseURL = c.Val()
|
||||
case "styles":
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
styles = c.Val()
|
||||
}
|
||||
}
|
||||
|
||||
var tplBytes []byte
|
||||
|
||||
// Second argument would be the template file to use
|
||||
var tplText string
|
||||
if c.NextArg() {
|
||||
tplBytes, err = ioutil.ReadFile(c.Val())
|
||||
// Get StyleSheet
|
||||
if styles != "" {
|
||||
tplBytes, err := ioutil.ReadFile(c.Val())
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
tplText = string(tplBytes)
|
||||
fmc.StyleSheet = string(tplBytes)
|
||||
} else {
|
||||
tplBytes, err = assets.Asset(assetsURL + "template.tmpl")
|
||||
tplBytes, err := assets.Asset("template.tmpl")
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
tplText = string(tplBytes)
|
||||
fmc.StyleSheet = string(tplBytes)
|
||||
}
|
||||
|
||||
// Build the template
|
||||
tpl, err := template.New("listing").Parse(tplText)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
bc.Template = tpl
|
||||
fmc.Root = http.Dir(fmc.PathScope)
|
||||
|
||||
// Save configuration
|
||||
err = appendCfg(bc)
|
||||
err := appendCfg(fmc)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue