- {{ end }}
-
-{{ end }}
-{{ end }}
diff --git a/config/config.go b/config/config.go
index c5f0145e..cf008598 100644
--- a/config/config.go
+++ b/config/config.go
@@ -3,7 +3,6 @@ package config
import (
"fmt"
"io/ioutil"
- "net/http"
"regexp"
"strconv"
"strings"
@@ -17,16 +16,14 @@ import (
// Config is a configuration for browsing in a particualr path.
type Config struct {
*User
- BaseURL string
- AbsoluteURL string
- AddrPath string
- Token string // Anti CSRF token
- HugoEnabled bool // Enables the Hugo plugin for File Manager
- Users map[string]*User
- WebDav bool
- WebDavURL string
- WebDavHandler *webdav.Handler
- CurrentUser *User
+ BaseURL string
+ AbsoluteURL string
+ AddrPath string
+ Token string // Anti CSRF token
+ HugoEnabled bool // Enables the Hugo plugin for File Manager
+ Users map[string]*User
+ WebDavURL string
+ CurrentUser *User
}
// Rule is a dissalow/allow rule
@@ -48,8 +45,8 @@ func Parse(c *caddy.Controller) ([]Config, error) {
appendConfig := func(cfg Config) error {
for _, c := range configs {
- if c.PathScope == cfg.PathScope {
- return fmt.Errorf("duplicate file managing config for %s", c.PathScope)
+ if c.Scope == cfg.Scope {
+ return fmt.Errorf("duplicate file managing config for %s", c.Scope)
}
}
configs = append(configs, cfg)
@@ -59,8 +56,8 @@ func Parse(c *caddy.Controller) ([]Config, error) {
for c.Next() {
// Initialize the configuration with the default settings
cfg := Config{User: &User{}}
- cfg.PathScope = "."
- cfg.Root = http.Dir(cfg.PathScope)
+ cfg.Scope = "."
+ cfg.FileSystem = webdav.Dir(cfg.Scope)
cfg.BaseURL = ""
cfg.FrontMatter = "yaml"
cfg.HugoEnabled = false
@@ -85,6 +82,7 @@ func Parse(c *caddy.Controller) ([]Config, error) {
cfg.BaseURL = strings.TrimPrefix(cfg.BaseURL, "/")
cfg.BaseURL = strings.TrimSuffix(cfg.BaseURL, "/")
cfg.BaseURL = "/" + cfg.BaseURL
+ cfg.WebDavURL = cfg.BaseURL + "webdav"
if cfg.BaseURL == "/" {
cfg.BaseURL = ""
@@ -105,31 +103,23 @@ func Parse(c *caddy.Controller) ([]Config, error) {
return configs, c.Err("frontmatter type not supported")
}
case "webdav":
- cfg.WebDav = true
-
- prefix := "webdav"
- if c.NextArg() {
- prefix = c.Val()
+ if !c.NextArg() {
+ return configs, c.ArgErr()
}
+ prefix := c.Val()
prefix = strings.TrimPrefix(prefix, "/")
prefix = strings.TrimSuffix(prefix, "/")
prefix = cfg.BaseURL + "/" + prefix
-
cfg.WebDavURL = prefix
- cfg.WebDavHandler = &webdav.Handler{
- Prefix: prefix,
- FileSystem: webdav.Dir(cfg.PathScope),
- LockSystem: webdav.NewMemLS(),
- }
case "show":
if !c.NextArg() {
return configs, c.ArgErr()
}
- user.PathScope = c.Val()
- user.PathScope = strings.TrimSuffix(user.PathScope, "/")
- user.Root = http.Dir(user.PathScope)
+ user.Scope = c.Val()
+ user.Scope = strings.TrimSuffix(user.Scope, "/")
+ user.FileSystem = webdav.Dir(user.Scope)
case "styles":
if !c.NextArg() {
return configs, c.ArgErr()
@@ -233,13 +223,19 @@ func Parse(c *caddy.Controller) ([]Config, error) {
user.AllowNew = cfg.AllowEdit
user.Commands = cfg.Commands
user.FrontMatter = cfg.FrontMatter
- user.PathScope = cfg.PathScope
- user.Root = cfg.Root
+ user.Scope = cfg.Scope
+ user.FileSystem = cfg.FileSystem
user.Rules = cfg.Rules
user.StyleSheet = cfg.StyleSheet
}
}
+ cfg.Handler = &webdav.Handler{
+ Prefix: cfg.WebDavURL,
+ FileSystem: cfg.FileSystem,
+ LockSystem: webdav.NewMemLS(),
+ }
+
caddyConf := httpserver.GetConfig(c)
cfg.AbsoluteURL = strings.TrimSuffix(caddyConf.Addr.Path, "/") + "/" + cfg.BaseURL
cfg.AbsoluteURL = strings.Replace(cfg.AbsoluteURL, "//", "/", -1)
diff --git a/config/user.go b/config/user.go
index abc07789..4f72d2ce 100644
--- a/config/user.go
+++ b/config/user.go
@@ -1,21 +1,23 @@
package config
import (
- "net/http"
"strings"
+
+ "golang.org/x/net/webdav"
)
// User contains the configuration for each user
type User struct {
- PathScope string `json:"-"` // Path the user have access
- Root http.FileSystem `json:"-"` // The virtual file system the user have access
- StyleSheet string `json:"-"` // Costum stylesheet
- FrontMatter string `json:"-"` // Default frontmatter to save files in
- AllowNew bool // Can create files and folders
- AllowEdit bool // Can edit/rename files
- AllowCommands bool // Can execute commands
- Commands []string // Available Commands
- Rules []*Rule `json:"-"` // Access rules
+ Scope string `json:"-"` // Path the user have access
+ FileSystem webdav.FileSystem `json:"-"` // The virtual file system the user have access
+ Handler *webdav.Handler `json:"-"` // The WebDav HTTP Handler
+ StyleSheet string `json:"-"` // Costum stylesheet
+ FrontMatter string `json:"-"` // Default frontmatter to save files in
+ AllowNew bool // Can create files and folders
+ AllowEdit bool // Can edit/rename files
+ AllowCommands bool // Can execute commands
+ Commands []string // Available Commands
+ Rules []*Rule `json:"-"` // Access rules
}
// Allowed checks if the user has permission to access a directory/file
diff --git a/directory/editor.go b/directory/editor.go
deleted file mode 100644
index 7ccb4ddd..00000000
--- a/directory/editor.go
+++ /dev/null
@@ -1,126 +0,0 @@
-package directory
-
-import (
- "bytes"
- "path/filepath"
- "strings"
-
- "github.com/hacdias/caddy-filemanager/frontmatter"
- "github.com/spf13/hugo/parser"
-)
-
-// Editor contains the information for the editor page
-type Editor struct {
- Class string
- Mode string
- Content string
- FrontMatter *frontmatter.Content
-}
-
-// GetEditor gets the editor based on a FileInfo struct
-func (i *Info) GetEditor() (*Editor, error) {
- // Create a new editor variable and set the mode
- editor := new(Editor)
- editor.Mode = strings.TrimPrefix(filepath.Ext(i.Name), ".")
-
- switch editor.Mode {
- case "md", "markdown", "mdown", "mmark":
- editor.Mode = "markdown"
- case "asciidoc", "adoc", "ad":
- editor.Mode = "asciidoc"
- case "rst":
- editor.Mode = "rst"
- case "html", "htm":
- editor.Mode = "html"
- case "js":
- editor.Mode = "javascript"
- }
-
- var page parser.Page
- var err error
-
- // Handle the content depending on the file extension
- switch editor.Mode {
- case "markdown", "asciidoc", "rst":
- if HasFrontMatterRune(i.Raw) {
- // Starts a new buffer and parses the file using Hugo's functions
- buffer := bytes.NewBuffer(i.Raw)
- page, err = parser.ReadFrom(buffer)
- if err != nil {
- return editor, err
- }
-
- // Parses the page content and the frontmatter
- editor.Content = strings.TrimSpace(string(page.Content()))
- editor.FrontMatter, _, err = frontmatter.Pretty(page.FrontMatter())
- editor.Class = "complete"
- } else {
- // The editor will handle only content
- editor.Class = "content-only"
- editor.Content = i.Content
- }
- case "json", "toml", "yaml":
- // Defines the class and declares an error
- editor.Class = "frontmatter-only"
-
- // Checks if the file already has the frontmatter rune and parses it
- if HasFrontMatterRune(i.Raw) {
- editor.FrontMatter, _, err = frontmatter.Pretty(i.Raw)
- } else {
- editor.FrontMatter, _, err = frontmatter.Pretty(AppendFrontMatterRune(i.Raw, editor.Mode))
- }
-
- // Check if there were any errors
- if err != nil {
- return editor, err
- }
- default:
- // The editor will handle only content
- editor.Class = "content-only"
- editor.Content = i.Content
- }
- return editor, nil
-}
-
-// HasFrontMatterRune checks if the file has the frontmatter rune
-func HasFrontMatterRune(file []byte) bool {
- return strings.HasPrefix(string(file), "---") ||
- strings.HasPrefix(string(file), "+++") ||
- strings.HasPrefix(string(file), "{")
-}
-
-// AppendFrontMatterRune appends the frontmatter rune to a file
-func AppendFrontMatterRune(frontmatter []byte, language string) []byte {
- switch language {
- case "yaml":
- return []byte("---\n" + string(frontmatter) + "\n---")
- case "toml":
- return []byte("+++\n" + string(frontmatter) + "\n+++")
- case "json":
- return frontmatter
- }
-
- return frontmatter
-}
-
-// CanBeEdited checks if the extension of a file is supported by the editor
-func CanBeEdited(filename string) bool {
- extensions := [...]string{
- "md", "markdown", "mdown", "mmark",
- "asciidoc", "adoc", "ad",
- "rst",
- ".json", ".toml", ".yaml",
- ".css", ".sass", ".scss",
- ".js",
- ".html",
- ".txt",
- }
-
- for _, extension := range extensions {
- if strings.HasSuffix(filename, extension) {
- return true
- }
- }
-
- return false
-}
diff --git a/directory/file.go b/directory/file.go
deleted file mode 100644
index a68936f1..00000000
--- a/directory/file.go
+++ /dev/null
@@ -1,314 +0,0 @@
-package directory
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/url"
- "os"
- "path"
- "path/filepath"
- "strings"
- "time"
-
- "github.com/dustin/go-humanize"
- "github.com/hacdias/caddy-filemanager/config"
- p "github.com/hacdias/caddy-filemanager/page"
- "github.com/hacdias/caddy-filemanager/utils/errors"
- "github.com/mholt/caddy/caddyhttp/httpserver"
-)
-
-// Info is the information about a particular file or directory
-type Info struct {
- IsDir bool
- Name string
- Size int64
- URL string
- Path string // The relative Path of the file/directory relative to Caddyfile.
- RootPath string // The Path of the file/directory on http.FileSystem.
- ModTime time.Time
- Mode os.FileMode
- Mimetype string
- Content string
- Raw []byte
- Type string
- UserAllowed bool // Indicates if the user has permissions to open this directory
-}
-
-// GetInfo gets the file information and, in case of error, returns the
-// respective HTTP error code
-func GetInfo(url *url.URL, c *config.Config, u *config.User) (*Info, int, error) {
- var err error
-
- rootPath := strings.Replace(url.Path, c.BaseURL, "", 1)
- rootPath = strings.TrimPrefix(rootPath, "/")
- rootPath = "/" + rootPath
-
- relpath := u.PathScope + rootPath
- relpath = strings.Replace(relpath, "\\", "/", -1)
- relpath = filepath.Clean(relpath)
-
- file := &Info{
- URL: url.Path,
- RootPath: rootPath,
- Path: relpath,
- }
- f, err := u.Root.Open(rootPath)
- if err != nil {
- return file, errors.ToHTTPCode(err), err
- }
- defer f.Close()
-
- info, err := f.Stat()
- if err != nil {
- return file, errors.ToHTTPCode(err), err
- }
-
- file.IsDir = info.IsDir()
- file.ModTime = info.ModTime()
- file.Name = info.Name()
- file.Size = info.Size()
- return file, 0, nil
-}
-
-// GetExtendedInfo is used to get extra parameters for FileInfo struct
-func (i *Info) GetExtendedInfo() error {
- err := i.Read()
- if err != nil {
- return err
- }
-
- i.Type = SimplifyMimeType(i.Mimetype)
- return nil
-}
-
-// Read is used to read a file and store its content
-func (i *Info) Read() error {
- raw, err := ioutil.ReadFile(i.Path)
- if err != nil {
- return err
- }
- i.Mimetype = http.DetectContentType(raw)
- i.Content = string(raw)
- i.Raw = raw
- return nil
-}
-
-// HumanSize returns the size of the file as a human-readable string
-// in IEC format (i.e. power of 2 or base 1024).
-func (i Info) HumanSize() string {
- return humanize.IBytes(uint64(i.Size))
-}
-
-// HumanModTime returns the modified time of the file as a human-readable string.
-func (i Info) HumanModTime(format string) string {
- return i.ModTime.Format(format)
-}
-
-// ServeAsHTML is used to serve single file pages
-func (i *Info) ServeAsHTML(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
- if i.IsDir {
- return i.serveListing(w, r, c, u)
- }
-
- return i.serveSingleFile(w, r, c, u)
-}
-
-func (i *Info) serveSingleFile(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
- err := i.GetExtendedInfo()
- if err != nil {
- return errors.ToHTTPCode(err), err
- }
-
- if i.Type == "blob" {
- return i.ServeRawFile(w, r, c)
- }
-
- page := &p.Page{
- Info: &p.Info{
- Name: i.Name,
- Path: i.RootPath,
- IsDir: false,
- Data: i,
- User: u,
- Config: c,
- },
- }
-
- if CanBeEdited(i.Name) && u.AllowEdit {
- editor, err := i.GetEditor()
-
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- page.Info.Data = editor
- return page.PrintAsHTML(w, "frontmatter", "editor")
- }
-
- return page.PrintAsHTML(w, "single")
-}
-
-func (i *Info) serveListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
- var err error
-
- file, err := u.Root.Open(i.RootPath)
- if err != nil {
- return errors.ToHTTPCode(err), err
- }
- defer file.Close()
-
- listing, err := i.loadDirectoryContents(file, r.URL.Path, u)
- if err != nil {
- fmt.Println(err)
- switch {
- case os.IsPermission(err):
- return http.StatusForbidden, err
- case os.IsExist(err):
- return http.StatusGone, err
- default:
- return http.StatusInternalServerError, err
- }
- }
-
- listing.Context = httpserver.Context{
- Root: c.Root,
- Req: r,
- URL: r.URL,
- }
-
- // Copy the query values into the Listing struct
- var limit int
- listing.Sort, listing.Order, limit, err = handleSortOrder(w, r, c.PathScope)
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- listing.applySort()
-
- if limit > 0 && limit <= len(listing.Items) {
- listing.Items = listing.Items[:limit]
- listing.ItemsLimitedTo = limit
- }
-
- if strings.Contains(r.Header.Get("Accept"), "application/json") {
- marsh, err := json.Marshal(listing.Items)
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- w.Header().Set("Content-Type", "application/json; charset=utf-8")
- if _, err := w.Write(marsh); err != nil {
- return http.StatusInternalServerError, err
- }
-
- return http.StatusOK, nil
- }
-
- page := &p.Page{
- Info: &p.Info{
- Name: listing.Name,
- Path: i.RootPath,
- IsDir: true,
- User: u,
- Config: c,
- Data: listing,
- },
- }
-
- if r.Header.Get("Minimal") == "true" {
- page.Minimal = true
- }
-
- return page.PrintAsHTML(w, "listing")
-}
-
-func (i Info) loadDirectoryContents(file http.File, path string, u *config.User) (*Listing, error) {
- files, err := file.Readdir(-1)
- if err != nil {
- return nil, err
- }
-
- listing := directoryListing(files, i.RootPath, path, u)
- return &listing, nil
-}
-
-func directoryListing(files []os.FileInfo, urlPath string, basePath string, u *config.User) Listing {
- var (
- fileinfos []Info
- dirCount, fileCount int
- )
-
- for _, f := range files {
- name := f.Name()
-
- if f.IsDir() {
- name += "/"
- dirCount++
- } else {
- fileCount++
- }
-
- // Absolute URL
- url := url.URL{Path: basePath + name}
- fileinfos = append(fileinfos, Info{
- IsDir: f.IsDir(),
- Name: f.Name(),
- Size: f.Size(),
- URL: url.String(),
- ModTime: f.ModTime().UTC(),
- Mode: f.Mode(),
- UserAllowed: u.Allowed(url.String()),
- })
- }
-
- return Listing{
- Name: path.Base(urlPath),
- Path: urlPath,
- Items: fileinfos,
- NumDirs: dirCount,
- NumFiles: fileCount,
- }
-}
-
-// ServeRawFile serves raw files
-func (i *Info) ServeRawFile(w http.ResponseWriter, r *http.Request, c *config.Config) (int, error) {
- err := i.GetExtendedInfo()
- if err != nil {
- return errors.ToHTTPCode(err), err
- }
-
- if i.Type != "text" {
- i.Read()
- }
-
- w.Header().Set("Content-Type", i.Mimetype)
- w.Write([]byte(i.Content))
- return 200, nil
-}
-
-// SimplifyMimeType returns the base type of a file
-func SimplifyMimeType(name string) string {
- if strings.HasPrefix(name, "video") {
- return "video"
- }
-
- if strings.HasPrefix(name, "audio") {
- return "audio"
- }
-
- if strings.HasPrefix(name, "image") {
- return "image"
- }
-
- if strings.HasPrefix(name, "text") {
- return "text"
- }
-
- if strings.HasPrefix(name, "application/javascript") {
- return "text"
- }
-
- return "blob"
-}
diff --git a/directory/listing.go b/directory/listing.go
deleted file mode 100644
index 0aa87fa8..00000000
--- a/directory/listing.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package directory
-
-import (
- "net/http"
- "sort"
- "strconv"
- "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
- // The items (files and folders) in the path
- Items []Info
- // 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
- httpserver.Context `json:"-"`
-}
-
-// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
-// and reads 'limit' if given. The latter is 0 if not given. Sets cookies.
-func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) {
- sort, order, limitQuery := r.URL.Query().Get("sort"), r.URL.Query().Get("order"), r.URL.Query().Get("limit")
-
- // If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies
- switch sort {
- case "":
- sort = "name"
- if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
- sort = sortCookie.Value
- }
- case "name", "size", "type":
- http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil})
- }
-
- switch order {
- case "":
- order = "asc"
- if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
- order = orderCookie.Value
- }
- case "asc", "desc":
- http.SetCookie(w, &http.Cookie{Name: "order", Value: order, Path: scope, Secure: r.TLS != nil})
- }
-
- if limitQuery != "" {
- limit, err = strconv.Atoi(limitQuery)
- if err != nil { // if the 'limit' query can't be interpreted as a number, return err
- return
- }
- }
-
- return
-}
-
-// Implement sorting for Listing
-type byName Listing
-type bySize Listing
-type byTime Listing
-
-// By Name
-func (l byName) Len() int { return len(l.Items) }
-func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
-
-// Treat upper and lower case equally
-func (l byName) Less(i, j int) bool {
- if l.Items[i].IsDir && !l.Items[j].IsDir {
- return true
- }
-
- if !l.Items[i].IsDir && l.Items[j].IsDir {
- return false
- }
-
- return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
-}
-
-// By Size
-func (l bySize) Len() int { return len(l.Items) }
-func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
-
-const directoryOffset = -1 << 31 // = math.MinInt32
-func (l bySize) Less(i, j int) bool {
- iSize, jSize := l.Items[i].Size, l.Items[j].Size
- if l.Items[i].IsDir {
- iSize = directoryOffset + iSize
- }
- if l.Items[j].IsDir {
- jSize = directoryOffset + jSize
- }
- return iSize < jSize
-}
-
-// By Time
-func (l byTime) Len() int { return len(l.Items) }
-func (l byTime) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
-func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j].ModTime) }
-
-// Add sorting method to "Listing"
-// it will apply what's in ".Sort" and ".Order"
-func (l Listing) applySort() {
- // Check '.Order' to know how to sort
- if l.Order == "desc" {
- switch l.Sort {
- case "name":
- sort.Sort(sort.Reverse(byName(l)))
- case "size":
- sort.Sort(sort.Reverse(bySize(l)))
- case "time":
- sort.Sort(sort.Reverse(byTime(l)))
- default:
- // If not one of the above, do nothing
- return
- }
- } else { // If we had more Orderings we could add them here
- switch l.Sort {
- case "name":
- sort.Sort(byName(l))
- case "size":
- sort.Sort(bySize(l))
- case "time":
- sort.Sort(byTime(l))
- default:
- sort.Sort(byName(l))
- return
- }
- }
-}
diff --git a/directory/update.go b/directory/update.go
deleted file mode 100644
index 7a0f5c3a..00000000
--- a/directory/update.go
+++ /dev/null
@@ -1,139 +0,0 @@
-package directory
-
-import (
- "bytes"
- "encoding/json"
- "errors"
- "fmt"
- "io/ioutil"
- "net/http"
- "path/filepath"
- "strings"
-
- "github.com/hacdias/caddy-filemanager/config"
- "github.com/spf13/hugo/parser"
-)
-
-// Update is used to update a file that was edited
-func (i *Info) Update(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
- var data map[string]interface{}
- kind := r.Header.Get("kind")
-
- if kind == "" {
- return http.StatusBadRequest, nil
- }
-
- // Get the JSON information
- rawBuffer := new(bytes.Buffer)
- rawBuffer.ReadFrom(r.Body)
- err := json.Unmarshal(rawBuffer.Bytes(), &data)
-
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- var file []byte
- var code int
-
- switch kind {
- case "frontmatter-only":
- if file, code, err = ParseFrontMatterOnlyFile(data, i.Name); err != nil {
- return http.StatusInternalServerError, err
- }
- case "content-only":
- mainContent := data["content"].(string)
- mainContent = strings.TrimSpace(mainContent)
- file = []byte(mainContent)
- case "complete":
- if file, code, err = ParseCompleteFile(data, i.Name, u.FrontMatter); err != nil {
- return http.StatusInternalServerError, err
- }
- default:
- return http.StatusBadRequest, nil
- }
-
- // Write the file
- err = ioutil.WriteFile(i.Path, file, 0666)
-
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- return code, nil
-}
-
-// ParseFrontMatterOnlyFile parses a frontmatter only file
-func ParseFrontMatterOnlyFile(data interface{}, filename string) ([]byte, int, error) {
- frontmatter := strings.TrimPrefix(filepath.Ext(filename), ".")
- f, code, err := ParseFrontMatter(data, frontmatter)
- fString := string(f)
-
- // If it's toml or yaml, strip frontmatter identifier
- if frontmatter == "toml" {
- fString = strings.TrimSuffix(fString, "+++\n")
- fString = strings.TrimPrefix(fString, "+++\n")
- }
-
- if frontmatter == "yaml" {
- fString = strings.TrimSuffix(fString, "---\n")
- fString = strings.TrimPrefix(fString, "---\n")
- }
-
- f = []byte(fString)
- return f, code, err
-}
-
-// ParseFrontMatter is the frontmatter parser
-func ParseFrontMatter(data interface{}, frontmatter string) ([]byte, int, error) {
- var mark rune
-
- switch frontmatter {
- case "toml":
- mark = rune('+')
- case "json":
- mark = rune('{')
- case "yaml":
- mark = rune('-')
- default:
- return []byte{}, http.StatusBadRequest, errors.New("Can't define the frontmatter.")
- }
-
- f, err := parser.InterfaceToFrontMatter(data, mark)
-
- if err != nil {
- return []byte{}, http.StatusInternalServerError, err
- }
-
- return f, http.StatusOK, nil
-}
-
-// ParseCompleteFile parses a complete file
-func ParseCompleteFile(data map[string]interface{}, filename string, frontmatter string) ([]byte, int, error) {
- mainContent := ""
-
- if _, ok := data["content"]; ok {
- // The main content of the file
- mainContent = data["content"].(string)
- mainContent = "\n\n" + strings.TrimSpace(mainContent) + "\n"
-
- // Removes the main content from the rest of the frontmatter
- delete(data, "content")
- }
-
- if _, ok := data["date"]; ok {
- data["date"] = data["date"].(string) + ":00"
- }
-
- front, code, err := ParseFrontMatter(data, frontmatter)
-
- if err != nil {
- fmt.Println(frontmatter)
- return []byte{}, code, err
- }
-
- // Generates the final file
- f := new(bytes.Buffer)
- f.Write(front)
- f.Write([]byte(mainContent))
- return f.Bytes(), http.StatusOK, nil
-}
diff --git a/file/info.go b/file/info.go
new file mode 100644
index 00000000..bec3901e
--- /dev/null
+++ b/file/info.go
@@ -0,0 +1,147 @@
+package file
+
+import (
+ "io/ioutil"
+ "mime"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+
+ humanize "github.com/dustin/go-humanize"
+ "github.com/hacdias/caddy-filemanager/config"
+ "github.com/hacdias/caddy-filemanager/utils/errors"
+)
+
+// Info contains the information about a particular file or directory
+type Info struct {
+ os.FileInfo
+ URL string
+ Path string // Relative path to Caddyfile
+ VirtualPath string // Relative path to u.FileSystem
+ Mimetype string
+ Content []byte
+ Type string
+ UserAllowed bool // Indicates if the user has enough permissions
+}
+
+// GetInfo gets the file information and, in case of error, returns the
+// respective HTTP error code
+func GetInfo(url *url.URL, c *config.Config, u *config.User) (*Info, int, error) {
+ var err error
+
+ i := &Info{URL: url.Path}
+ i.VirtualPath = strings.Replace(url.Path, c.BaseURL, "", 1)
+ i.VirtualPath = strings.TrimPrefix(i.VirtualPath, "/")
+ i.VirtualPath = "/" + i.VirtualPath
+
+ i.Path = u.Scope + i.VirtualPath
+ i.Path = strings.Replace(i.Path, "\\", "/", -1)
+ i.Path = filepath.Clean(i.Path)
+
+ i.FileInfo, err = os.Stat(i.Path)
+ if err != nil {
+ return i, errors.ErrorToHTTPCode(err, true), err
+ }
+
+ return i, 0, nil
+}
+
+// RetrieveFileType obtains the mimetype and a simplified internal Type
+// using the first 512 bytes from the file.
+func (i *Info) RetrieveFileType() error {
+ i.Mimetype = mime.TypeByExtension(filepath.Ext(i.Name()))
+
+ if i.Mimetype == "" {
+ err := i.Read()
+ if err != nil {
+ return err
+ }
+
+ i.Mimetype = http.DetectContentType(i.Content)
+ }
+
+ i.Type = simplifyMediaType(i.Mimetype)
+ return nil
+}
+
+// Reads the file.
+func (i *Info) Read() error {
+ if len(i.Content) != 0 {
+ return nil
+ }
+
+ var err error
+ i.Content, err = ioutil.ReadFile(i.Path)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// StringifyContent returns the string version of Raw
+func (i Info) StringifyContent() string {
+ return string(i.Content)
+}
+
+// HumanSize returns the size of the file as a human-readable string
+// in IEC format (i.e. power of 2 or base 1024).
+func (i Info) HumanSize() string {
+ return humanize.IBytes(uint64(i.Size()))
+}
+
+// HumanModTime returns the modified time of the file as a human-readable string.
+func (i Info) HumanModTime(format string) string {
+ return i.ModTime().Format(format)
+}
+
+// CanBeEdited checks if the extension of a file is supported by the editor
+func (i Info) CanBeEdited() bool {
+ if i.Type == "text" {
+ return true
+ }
+
+ extensions := [...]string{
+ "md", "markdown", "mdown", "mmark",
+ "asciidoc", "adoc", "ad",
+ "rst",
+ ".json", ".toml", ".yaml",
+ ".css", ".sass", ".scss",
+ ".js",
+ ".html",
+ ".txt",
+ }
+
+ for _, extension := range extensions {
+ if strings.HasSuffix(i.Name(), extension) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func simplifyMediaType(name string) string {
+ if strings.HasPrefix(name, "video") {
+ return "video"
+ }
+
+ if strings.HasPrefix(name, "audio") {
+ return "audio"
+ }
+
+ if strings.HasPrefix(name, "image") {
+ return "image"
+ }
+
+ if strings.HasPrefix(name, "text") {
+ return "text"
+ }
+
+ if strings.HasPrefix(name, "application/javascript") {
+ return "text"
+ }
+
+ return "blob"
+}
diff --git a/file/listing.go b/file/listing.go
new file mode 100644
index 00000000..91ff2b1a
--- /dev/null
+++ b/file/listing.go
@@ -0,0 +1,172 @@
+package file
+
+import (
+ "net/url"
+ "os"
+ "path"
+ "sort"
+ "strings"
+
+ "github.com/hacdias/caddy-filemanager/config"
+
+ "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 relatively to a File System
+ Path string
+ // The items (files and folders) in the path
+ Items []Info
+ // 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
+ httpserver.Context `json:"-"`
+}
+
+// GetListing gets the information about a specific directory and its files.
+func GetListing(u *config.User, filePath string, baseURL string) (*Listing, error) {
+ // Gets the directory information using the Virtual File System of
+ // the user configuration.
+ file, err := u.FileSystem.OpenFile(filePath, os.O_RDONLY, 0)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ // Reads the directory and gets the information about the files.
+ files, err := file.Readdir(-1)
+ if err != nil {
+ return nil, err
+ }
+
+ var (
+ fileinfos []Info
+ dirCount, fileCount int
+ )
+
+ for _, f := range files {
+ name := f.Name()
+
+ if f.IsDir() {
+ name += "/"
+ dirCount++
+ } else {
+ fileCount++
+ }
+
+ // Absolute URL
+ url := url.URL{Path: baseURL + name}
+ fileinfos = append(fileinfos, Info{
+ FileInfo: f,
+ URL: url.String(),
+ UserAllowed: u.Allowed(filePath),
+ })
+ }
+
+ return &Listing{
+ Name: path.Base(filePath),
+ Path: filePath,
+ Items: fileinfos,
+ NumDirs: dirCount,
+ NumFiles: fileCount,
+ }, nil
+}
+
+// ApplySort applies the sort order using .Order and .Sort
+func (l Listing) ApplySort() {
+ // Check '.Order' to know how to sort
+ if l.Order == "desc" {
+ switch l.Sort {
+ case "name":
+ sort.Sort(sort.Reverse(byName(l)))
+ case "size":
+ sort.Sort(sort.Reverse(bySize(l)))
+ case "time":
+ sort.Sort(sort.Reverse(byTime(l)))
+ default:
+ // If not one of the above, do nothing
+ return
+ }
+ } else { // If we had more Orderings we could add them here
+ switch l.Sort {
+ case "name":
+ sort.Sort(byName(l))
+ case "size":
+ sort.Sort(bySize(l))
+ case "time":
+ sort.Sort(byTime(l))
+ default:
+ sort.Sort(byName(l))
+ return
+ }
+ }
+}
+
+// Implement sorting for Listing
+type byName Listing
+type bySize Listing
+type byTime Listing
+
+// By Name
+func (l byName) Len() int {
+ return len(l.Items)
+}
+
+func (l byName) Swap(i, j int) {
+ l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
+}
+
+// Treat upper and lower case equally
+func (l byName) Less(i, j int) bool {
+ if l.Items[i].IsDir() && !l.Items[j].IsDir() {
+ return true
+ }
+
+ if !l.Items[i].IsDir() && l.Items[j].IsDir() {
+ return false
+ }
+
+ return strings.ToLower(l.Items[i].Name()) < strings.ToLower(l.Items[j].Name())
+}
+
+// By Size
+func (l bySize) Len() int {
+ return len(l.Items)
+}
+
+func (l bySize) Swap(i, j int) {
+ l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
+}
+
+const directoryOffset = -1 << 31 // = math.MinInt32
+func (l bySize) Less(i, j int) bool {
+ iSize, jSize := l.Items[i].Size(), l.Items[j].Size()
+ if l.Items[i].IsDir() {
+ iSize = directoryOffset + iSize
+ }
+ if l.Items[j].IsDir() {
+ jSize = directoryOffset + jSize
+ }
+ return iSize < jSize
+}
+
+// By Time
+func (l byTime) Len() int {
+ return len(l.Items)
+}
+func (l byTime) Swap(i, j int) {
+ l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
+}
+func (l byTime) Less(i, j int) bool {
+ return l.Items[i].ModTime().Before(l.Items[j].ModTime())
+}
diff --git a/filemanager.go b/filemanager.go
index 97811360..8827e772 100644
--- a/filemanager.go
+++ b/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 -ignore .jsbeautifyrc -prefix "assets/embed" -o assets/binary.go assets/embed/...
+//go:generate go-bindata -pkg assets -ignore .jsbeautifyrc -prefix "_embed" -o assets/binary.go _embed/...
// Package filemanager provides middleware for managing files in a directory
// when directory path is requested instead of a specific file. Based on browse
@@ -9,20 +9,13 @@ package filemanager
import (
e "errors"
- "io"
- "io/ioutil"
- "log"
- "mime/multipart"
"net/http"
- "os"
- "os/exec"
- "path/filepath"
"strings"
"github.com/hacdias/caddy-filemanager/assets"
"github.com/hacdias/caddy-filemanager/config"
- "github.com/hacdias/caddy-filemanager/directory"
- "github.com/hacdias/caddy-filemanager/errors"
+ "github.com/hacdias/caddy-filemanager/file"
+ "github.com/hacdias/caddy-filemanager/handlers"
"github.com/hacdias/caddy-filemanager/page"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
@@ -37,269 +30,174 @@ type FileManager struct {
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
var (
- c *config.Config
- fi *directory.Info
- code int
- err error
- serveAssets bool
- user *config.User
+ c *config.Config
+ fi *file.Info
+ code int
+ err error
+ user *config.User
)
for i := range f.Configs {
- if httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
- c = &f.Configs[i]
- serveAssets = httpserver.Path(r.URL.Path).Matches(c.BaseURL + assets.BaseURL)
- username, _, _ := r.BasicAuth()
+ // Checks if this Path should be handled by File Manager.
+ if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
+ return f.Next.ServeHTTP(w, r)
+ }
- if _, ok := c.Users[username]; ok {
- user = c.Users[username]
- } else {
- user = c.User
+ w.Header().Set("x-frame-options", "SAMEORIGIN")
+ w.Header().Set("x-content-type", "nosniff")
+ w.Header().Set("x-xss-protection", "1; mode=block")
+
+ c = &f.Configs[i]
+
+ // Checks if the URL matches the Assets URL. Returns the asset if the
+ // method is GET and Status Forbidden otherwise.
+ if httpserver.Path(r.URL.Path).Matches(c.BaseURL + assets.BaseURL) {
+ if r.Method == http.MethodGet {
+ return assets.Serve(w, r, c)
}
- if c.WebDav && strings.HasPrefix(r.URL.Path, c.WebDavURL) {
- //url := strings.TrimPrefix(r.URL.Path, c.WebDavURL)
+ return http.StatusForbidden, nil
+ }
- /*
- if !user.Allowed(url) {
- return http.StatusForbidden, nil
- }
+ // Obtains the user
+ username, _, _ := r.BasicAuth()
+ if _, ok := c.Users[username]; ok {
+ user = c.Users[username]
+ } else {
+ user = c.User
+ }
- switch r.Method {
- case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE":
- if !user.AllowEdit {
- return http.StatusForbidden, nil
- }
- case "MKCOL", "COPY":
- if !user.AllowNew {
- return http.StatusForbidden, nil
- }
- } */
-
- c.WebDavHandler.ServeHTTP(w, r)
- return 0, nil
- }
-
- // Checks if the user has permission to access the current directory.
- if !user.Allowed(r.URL.Path) {
- if r.Method == http.MethodGet {
- return errors.PrintHTML(w, http.StatusForbidden, e.New("You don't have permission to access this page."))
- }
+ // Checks if the request URL is for the WebDav server
+ if strings.HasPrefix(r.URL.Path, c.WebDavURL) {
+ // if !c.CheckToken(r) {
+ // return http.StatusForbidden, nil
+ // }
+ // Checks for user permissions relatively to this PATH
+ if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.WebDavURL)) {
return http.StatusForbidden, nil
}
- // If this request is neither to server assets, nor to upload/create
- // a new file or directory.
- if r.Method != http.MethodPost && !serveAssets {
- // Gets the information of the directory/file
- fi, code, err = directory.GetInfo(r.URL, c, user)
- if err != nil {
- if r.Method == http.MethodGet {
- return errors.PrintHTML(w, code, err)
- }
- return code, err
- }
-
- // If it's a dir and the path doesn't end with a trailing slash,
- // redirect the user.
- if fi.IsDir && !strings.HasSuffix(r.URL.Path, "/") {
- http.Redirect(w, r, c.AddrPath+r.URL.Path+"/", http.StatusTemporaryRedirect)
- return 0, nil
- }
- }
-
- // Security measures against CSRF attacks.
- if r.Method != http.MethodGet {
- if !c.CheckToken(r) {
- return http.StatusForbidden, nil
- }
- }
-
- // Route the request depending on the HTTP Method.
switch r.Method {
- case http.MethodGet:
- // Read and show directory or file.
- if serveAssets {
- return assets.Serve(w, r, c)
- }
-
- // Generate anti security token.
- c.GenerateToken()
-
- if !fi.IsDir {
- query := r.URL.Query()
- if val, ok := query["raw"]; ok && val[0] == "true" {
- return fi.ServeRawFile(w, r, c)
- }
-
- if val, ok := query["download"]; ok && val[0] == "true" {
- w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name)
- return fi.ServeRawFile(w, r, c)
- }
- }
-
- code, err := fi.ServeAsHTML(w, r, c, user)
- if err != nil {
- return errors.PrintHTML(w, code, err)
- }
- return code, err
- case http.MethodPut:
- if fi.IsDir {
- return http.StatusNotAcceptable, nil
- }
-
+ case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE":
if !user.AllowEdit {
return http.StatusForbidden, nil
}
-
- // Update a file.
- return fi.Update(w, r, c, user)
- case http.MethodPost:
- // Upload a new file.
- if r.Header.Get("Upload") == "true" {
- if !user.AllowNew {
- return http.StatusUnauthorized, nil
- }
-
- return upload(w, r, c)
+ case "MKCOL", "COPY":
+ if !user.AllowNew {
+ return http.StatusForbidden, nil
}
+ }
- // Search and git commands.
- if r.Header.Get("Search") == "true" {
- // TODO: search commands.
+ // Preprocess the PUT request if it's the case
+ if r.Method == http.MethodPut {
+ if handlers.PreProccessPUT(w, r, c, user, fi) != nil {
+ return http.StatusInternalServerError, err
}
+ }
- // VCS commands.
- if r.Header.Get("Command") != "" {
- if !user.AllowCommands {
- return http.StatusUnauthorized, nil
- }
+ c.Handler.ServeHTTP(w, r)
+ return 0, nil
+ }
- return command(w, r, c, user)
+ // Checks if the User is allowed to access this file
+ if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.BaseURL)) {
+ if r.Method == http.MethodGet {
+ return page.PrintErrorHTML(
+ w, http.StatusForbidden,
+ e.New("You don't have permission to access this page."),
+ )
+ }
+
+ return http.StatusForbidden, nil
+ }
+
+ if r.Method == http.MethodGet {
+ // Generate anti security token.
+ /* c.GenerateToken()
+
+ http.SetCookie(w, &http.Cookie{
+ Name: "token",
+ Value: c.Token,
+ Path: "/",
+ HttpOnly: true,
+ })
+
+ co, err := r.Cookie("token")
+ fmt.Println(co.Value) */
+
+ /* Name string
+ Value string
+
+ Path string // optional
+ Domain string // optional
+ Expires time.Time // optional
+ RawExpires string // for reading cookies only
+
+ // MaxAge=0 means no 'Max-Age' attribute specified.
+ // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
+ // MaxAge>0 means Max-Age attribute present and given in seconds
+ MaxAge int
+ Secure bool
+ HttpOnly bool
+ Raw string
+ Unparsed []string // Raw text of unparsed attribute-value pairs*/
+
+ // Gets the information of the directory/file
+ fi, code, err = file.GetInfo(r.URL, c, user)
+ if err != nil {
+ if r.Method == http.MethodGet {
+ return page.PrintErrorHTML(w, code, err)
}
+ return code, err
+ }
- // Creates a new folder.
- return newDirectory(w, r, c)
+ // If it's a dir and the path doesn't end with a trailing slash,
+ // redirect the user.
+ if fi.IsDir() && !strings.HasSuffix(r.URL.Path, "/") {
+ http.Redirect(w, r, c.AddrPath+r.URL.Path+"/", http.StatusTemporaryRedirect)
+ return 0, nil
+ }
+
+ switch {
+ case r.URL.Query().Get("download") != "":
+ code, err = handlers.Download(w, r, c, fi)
+ case r.URL.Query().Get("raw") == "true" && !fi.IsDir():
+ http.ServeFile(w, r, fi.Path)
+ code, err = 0, nil
+ case fi.IsDir():
+ code, err = handlers.ServeListing(w, r, c, user, fi)
default:
- return http.StatusNotImplemented, nil
+ code, err = handlers.ServeSingle(w, r, c, user, fi)
+ }
+
+ if err != nil {
+ code, err = page.PrintErrorHTML(w, code, err)
+ }
+
+ return code, err
+ }
+
+ if r.Method == http.MethodPost {
+ // TODO: This anti CSCF measure is not being applied to requests
+ // to the WebDav URL namespace. Anyone has ideas?
+ // if !c.CheckToken(r) {
+ // return http.StatusForbidden, nil
+ // }
+
+ // VCS commands.
+ if r.Header.Get("Command") != "" {
+ if !user.AllowCommands {
+ return http.StatusUnauthorized, nil
+ }
+
+ return handlers.Command(w, r, c, user)
}
}
+
+ return http.StatusNotImplemented, nil
+
}
return f.Next.ServeHTTP(w, r)
}
-
-// upload is used to handle the upload requests to the server
-func upload(w http.ResponseWriter, r *http.Request, c *config.Config) (int, error) {
- // Parse the multipart form in the request
- err := r.ParseMultipartForm(100000)
- if err != nil {
- log.Println(err)
- return http.StatusInternalServerError, err
- }
-
- // For each file header in the multipart form
- for _, headers := range r.MultipartForm.File {
- // Handle each file
- for _, header := range headers {
- // Open the first file
- var src multipart.File
- if src, err = header.Open(); nil != err {
- return http.StatusInternalServerError, err
- }
-
- filename := strings.Replace(r.URL.Path, c.BaseURL, c.PathScope, 1)
- filename = filename + header.Filename
- filename = filepath.Clean(filename)
-
- // Create the file
- var dst *os.File
- if dst, err = os.Create(filename); nil != err {
- if os.IsExist(err) {
- return http.StatusConflict, err
- }
- return http.StatusInternalServerError, err
- }
-
- // Copy the file content
- if _, err = io.Copy(dst, src); nil != err {
- return http.StatusInternalServerError, err
- }
-
- defer dst.Close()
- }
- }
-
- return http.StatusOK, nil
-}
-
-// newDirectory makes a new directory
-func newDirectory(w http.ResponseWriter, r *http.Request, c *config.Config) (int, error) {
- filename := r.Header.Get("Filename")
-
- if filename == "" {
- return http.StatusBadRequest, nil
- }
-
- path := strings.Replace(r.URL.Path, c.BaseURL, c.PathScope, 1) + filename
- path = filepath.Clean(path)
- extension := filepath.Ext(path)
-
- var err error
-
- if extension == "" {
- err = os.MkdirAll(path, 0775)
- } else {
- err = ioutil.WriteFile(path, []byte(""), 0775)
- }
-
- if err != nil {
- switch {
- case os.IsPermission(err):
- return http.StatusForbidden, err
- case os.IsExist(err):
- return http.StatusConflict, err
- default:
- return http.StatusInternalServerError, err
- }
- }
- return http.StatusCreated, nil
-}
-
-// command handles the requests for VCS related commands: git, svn and mercurial
-func command(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
- command := strings.Split(r.Header.Get("command"), " ")
-
- // Check if the command is allowed
- mayContinue := false
-
- for _, cmd := range u.Commands {
- if cmd == command[0] {
- mayContinue = true
- }
- }
-
- if !mayContinue {
- return http.StatusForbidden, nil
- }
-
- // Check if the program is talled is installed on the computer
- if _, err := exec.LookPath(command[0]); err != nil {
- return http.StatusNotImplemented, nil
- }
-
- path := strings.Replace(r.URL.Path, c.BaseURL, c.PathScope, 1)
- path = filepath.Clean(path)
-
- cmd := exec.Command(command[0], command[1:len(command)]...)
- cmd.Dir = path
- output, err := cmd.CombinedOutput()
-
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- page := &page.Page{Info: &page.Info{Data: string(output)}}
- return page.PrintAsJSON(w)
-}
diff --git a/frontmatter/frontmatter.go b/frontmatter/frontmatter.go
index 5703addc..d50df10e 100644
--- a/frontmatter/frontmatter.go
+++ b/frontmatter/frontmatter.go
@@ -14,6 +14,7 @@ import (
"github.com/BurntSushi/toml"
"github.com/hacdias/caddy-filemanager/utils/variables"
+
"github.com/spf13/cast"
)
diff --git a/frontmatter/runes.go b/frontmatter/runes.go
new file mode 100644
index 00000000..65d0ddde
--- /dev/null
+++ b/frontmatter/runes.go
@@ -0,0 +1,24 @@
+package frontmatter
+
+import "strings"
+
+// HasRune checks if the file has the frontmatter rune
+func HasRune(file []byte) bool {
+ return strings.HasPrefix(string(file), "---") ||
+ strings.HasPrefix(string(file), "+++") ||
+ strings.HasPrefix(string(file), "{")
+}
+
+// AppendRune appends the frontmatter rune to a file
+func AppendRune(frontmatter []byte, language string) []byte {
+ switch language {
+ case "yaml":
+ return []byte("---\n" + string(frontmatter) + "\n---")
+ case "toml":
+ return []byte("+++\n" + string(frontmatter) + "\n+++")
+ case "json":
+ return frontmatter
+ }
+
+ return frontmatter
+}
diff --git a/handlers/command.go b/handlers/command.go
new file mode 100644
index 00000000..e2690c42
--- /dev/null
+++ b/handlers/command.go
@@ -0,0 +1,48 @@
+package handlers
+
+import (
+ "net/http"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/hacdias/caddy-filemanager/config"
+ "github.com/hacdias/caddy-filemanager/page"
+)
+
+// Command handles the requests for VCS related commands: git, svn and mercurial
+func Command(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
+ command := strings.Split(r.Header.Get("command"), " ")
+
+ // Check if the command is allowed
+ mayContinue := false
+
+ for _, cmd := range u.Commands {
+ if cmd == command[0] {
+ mayContinue = true
+ }
+ }
+
+ if !mayContinue {
+ return http.StatusForbidden, nil
+ }
+
+ // Check if the program is talled is installed on the computer
+ if _, err := exec.LookPath(command[0]); err != nil {
+ return http.StatusNotImplemented, nil
+ }
+
+ path := strings.Replace(r.URL.Path, c.BaseURL, c.Scope, 1)
+ path = filepath.Clean(path)
+
+ cmd := exec.Command(command[0], command[1:len(command)]...)
+ cmd.Dir = path
+ output, err := cmd.CombinedOutput()
+
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ p := &page.Page{Info: &page.Info{Data: string(output)}}
+ return p.PrintAsJSON(w)
+}
diff --git a/handlers/download.go b/handlers/download.go
new file mode 100644
index 00000000..4b064499
--- /dev/null
+++ b/handlers/download.go
@@ -0,0 +1,83 @@
+package handlers
+
+import (
+ "io"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/hacdias/caddy-filemanager/config"
+ "github.com/hacdias/caddy-filemanager/file"
+ "github.com/mholt/archiver"
+)
+
+// Download creates an archieve in one of the supported formats (zip, tar,
+// tar.gz or tar.bz2) and sends it to be downloaded.
+func Download(w http.ResponseWriter, r *http.Request, c *config.Config, i *file.Info) (int, error) {
+ query := r.URL.Query().Get("download")
+
+ if !i.IsDir() {
+ w.Header().Set("Content-Disposition", "attachment; filename="+i.Name())
+ http.ServeFile(w, r, i.Path)
+ return 0, nil
+ }
+
+ files := []string{}
+ names := strings.Split(r.URL.Query().Get("files"), ",")
+
+ if len(names) != 0 {
+ for _, name := range names {
+ files = append(files, filepath.Join(i.Path, name))
+ }
+
+ } else {
+ files = append(files, i.Path)
+ }
+
+ if query == "true" {
+ query = "zip"
+ }
+
+ var (
+ extension string
+ temp string
+ err error
+ tempfile string
+ )
+
+ temp, err = ioutil.TempDir("", "")
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ defer os.RemoveAll(temp)
+ tempfile = filepath.Join(temp, "temp")
+
+ switch query {
+ case "zip":
+ extension, err = ".zip", archiver.Zip.Make(tempfile, files)
+ case "tar":
+ extension, err = ".tar", archiver.Tar.Make(tempfile, files)
+ case "targz":
+ extension, err = ".tar.gz", archiver.TarGz.Make(tempfile, files)
+ case "tarbz2":
+ extension, err = ".tar.bz2", archiver.TarBz2.Make(tempfile, files)
+ default:
+ return http.StatusNotImplemented, nil
+ }
+
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ file, err := os.Open(temp + "/temp")
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ w.Header().Set("Content-Disposition", "attachment; filename="+i.Name()+extension)
+ io.Copy(w, file)
+ return http.StatusOK, nil
+}
diff --git a/handlers/editor.go b/handlers/editor.go
new file mode 100644
index 00000000..f9343538
--- /dev/null
+++ b/handlers/editor.go
@@ -0,0 +1,84 @@
+package handlers
+
+import (
+ "bytes"
+ "path/filepath"
+ "strings"
+
+ "github.com/hacdias/caddy-filemanager/file"
+ "github.com/hacdias/caddy-filemanager/frontmatter"
+ "github.com/spf13/hugo/parser"
+)
+
+// Editor contains the information for the editor page
+type Editor struct {
+ Class string
+ Mode string
+ Content string
+ FrontMatter *frontmatter.Content
+}
+
+// GetEditor gets the editor based on a FileInfo struct
+func GetEditor(i *file.Info) (*Editor, error) {
+ // Create a new editor variable and set the mode
+ editor := new(Editor)
+ editor.Mode = strings.TrimPrefix(filepath.Ext(i.Name()), ".")
+
+ switch editor.Mode {
+ case "md", "markdown", "mdown", "mmark":
+ editor.Mode = "markdown"
+ case "asciidoc", "adoc", "ad":
+ editor.Mode = "asciidoc"
+ case "rst":
+ editor.Mode = "rst"
+ case "html", "htm":
+ editor.Mode = "html"
+ case "js":
+ editor.Mode = "javascript"
+ }
+
+ var page parser.Page
+ var err error
+
+ // Handle the content depending on the file extension
+ switch editor.Mode {
+ case "json", "toml", "yaml":
+ // Defines the class and declares an error
+ editor.Class = "frontmatter-only"
+
+ // Checks if the file already has the frontmatter rune and parses it
+ if frontmatter.HasRune(i.Content) {
+ editor.FrontMatter, _, err = frontmatter.Pretty(i.Content)
+ } else {
+ editor.FrontMatter, _, err = frontmatter.Pretty(frontmatter.AppendRune(i.Content, editor.Mode))
+ }
+
+ // Check if there were any errors
+ if err == nil {
+ break
+ }
+
+ fallthrough
+ case "markdown", "asciidoc", "rst":
+ if frontmatter.HasRune(i.Content) {
+ // Starts a new buffer and parses the file using Hugo's functions
+ buffer := bytes.NewBuffer(i.Content)
+ page, err = parser.ReadFrom(buffer)
+ editor.Class = "complete"
+
+ if err == nil {
+ // Parses the page content and the frontmatter
+ editor.Content = strings.TrimSpace(string(page.Content()))
+ editor.FrontMatter, _, err = frontmatter.Pretty(page.FrontMatter())
+ break
+ }
+ }
+
+ fallthrough
+ default:
+ editor.Class = "content-only"
+ editor.Content = i.StringifyContent()
+ }
+
+ return editor, nil
+}
diff --git a/handlers/listing.go b/handlers/listing.go
new file mode 100644
index 00000000..28d022db
--- /dev/null
+++ b/handlers/listing.go
@@ -0,0 +1,123 @@
+package handlers
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/hacdias/caddy-filemanager/config"
+ "github.com/hacdias/caddy-filemanager/file"
+ "github.com/hacdias/caddy-filemanager/page"
+ "github.com/hacdias/caddy-filemanager/utils/errors"
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+)
+
+// ServeListing presents the user with a listage of a directory folder.
+func ServeListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User, i *file.Info) (int, error) {
+ var err error
+
+ // Loads the content of the directory
+ listing, err := file.GetListing(u, i.VirtualPath, r.URL.Path)
+ if err != nil {
+ return errors.ErrorToHTTPCode(err, true), err
+ }
+
+ listing.Context = httpserver.Context{
+ Root: http.Dir(u.Scope),
+ Req: r,
+ URL: r.URL,
+ }
+
+ // Copy the query values into the Listing struct
+ var limit int
+ listing.Sort, listing.Order, limit, err = handleSortOrder(w, r, c.Scope)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ listing.ApplySort()
+
+ if limit > 0 && limit <= len(listing.Items) {
+ listing.Items = listing.Items[:limit]
+ listing.ItemsLimitedTo = limit
+ }
+
+ if strings.Contains(r.Header.Get("Accept"), "application/json") {
+ marsh, err := json.Marshal(listing.Items)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ if _, err := w.Write(marsh); err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ return http.StatusOK, nil
+ }
+
+ page := &page.Page{
+ Minimal: r.Header.Get("Minimal") == "true",
+ Info: &page.Info{
+ Name: listing.Name,
+ Path: i.VirtualPath,
+ IsDir: true,
+ User: u,
+ Config: c,
+ Data: listing,
+ },
+ }
+
+ return page.PrintAsHTML(w, "listing")
+}
+
+// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
+// and reads 'limit' if given. The latter is 0 if not given. Sets cookies.
+func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) {
+ sort = r.URL.Query().Get("sort")
+ order = r.URL.Query().Get("order")
+ limitQuery := r.URL.Query().Get("limit")
+
+ // If the query 'sort' or 'order' is empty, use defaults or any values
+ // previously saved in Cookies.
+ switch sort {
+ case "":
+ sort = "name"
+ if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
+ sort = sortCookie.Value
+ }
+ case "name", "size", "type":
+ http.SetCookie(w, &http.Cookie{
+ Name: "sort",
+ Value: sort,
+ Path: scope,
+ Secure: r.TLS != nil,
+ })
+ }
+
+ switch order {
+ case "":
+ order = "asc"
+ if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
+ order = orderCookie.Value
+ }
+ case "asc", "desc":
+ http.SetCookie(w, &http.Cookie{
+ Name: "order",
+ Value: order,
+ Path: scope,
+ Secure: r.TLS != nil,
+ })
+ }
+
+ if limitQuery != "" {
+ limit, err = strconv.Atoi(limitQuery)
+ // If the 'limit' query can't be interpreted as a number, return err.
+ if err != nil {
+ return
+ }
+ }
+
+ return
+}
diff --git a/handlers/put.go b/handlers/put.go
new file mode 100644
index 00000000..e13a463d
--- /dev/null
+++ b/handlers/put.go
@@ -0,0 +1,140 @@
+package handlers
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "path/filepath"
+ "strings"
+
+ "github.com/hacdias/caddy-filemanager/config"
+ "github.com/hacdias/caddy-filemanager/file"
+ "github.com/spf13/hugo/parser"
+)
+
+// PreProccessPUT is used to update a file that was edited
+func PreProccessPUT(
+ w http.ResponseWriter,
+ r *http.Request,
+ c *config.Config,
+ u *config.User,
+ i *file.Info,
+) (err error) {
+ var (
+ data map[string]interface{}
+ file []byte
+ kind string
+ rawBuffer = new(bytes.Buffer)
+ )
+
+ kind = r.Header.Get("kind")
+ rawBuffer.ReadFrom(r.Body)
+
+ if kind != "" {
+ err = json.Unmarshal(rawBuffer.Bytes(), &data)
+
+ if err != nil {
+ return
+ }
+ }
+
+ switch kind {
+ case "frontmatter-only":
+ if file, err = parseFrontMatterOnlyFile(data, i.Name()); err != nil {
+ return
+ }
+ case "content-only":
+ mainContent := data["content"].(string)
+ mainContent = strings.TrimSpace(mainContent)
+ file = []byte(mainContent)
+ case "complete":
+ if file, err = parseCompleteFile(data, i.Name(), u.FrontMatter); err != nil {
+ return
+ }
+ default:
+ file = rawBuffer.Bytes()
+ }
+
+ // Overwrite the request Body
+ r.Body = ioutil.NopCloser(bytes.NewReader(file))
+ return
+}
+
+// parseFrontMatterOnlyFile parses a frontmatter only file
+func parseFrontMatterOnlyFile(data interface{}, filename string) ([]byte, error) {
+ frontmatter := strings.TrimPrefix(filepath.Ext(filename), ".")
+ f, err := parseFrontMatter(data, frontmatter)
+ fString := string(f)
+
+ // If it's toml or yaml, strip frontmatter identifier
+ if frontmatter == "toml" {
+ fString = strings.TrimSuffix(fString, "+++\n")
+ fString = strings.TrimPrefix(fString, "+++\n")
+ }
+
+ if frontmatter == "yaml" {
+ fString = strings.TrimSuffix(fString, "---\n")
+ fString = strings.TrimPrefix(fString, "---\n")
+ }
+
+ f = []byte(fString)
+ return f, err
+}
+
+// parseFrontMatter is the frontmatter parser
+func parseFrontMatter(data interface{}, frontmatter string) ([]byte, error) {
+ var mark rune
+
+ switch frontmatter {
+ case "toml":
+ mark = rune('+')
+ case "json":
+ mark = rune('{')
+ case "yaml":
+ mark = rune('-')
+ default:
+ return []byte{}, errors.New("Can't define the frontmatter.")
+ }
+
+ f, err := parser.InterfaceToFrontMatter(data, mark)
+
+ if err != nil {
+ return []byte{}, err
+ }
+
+ return f, nil
+}
+
+// parseCompleteFile parses a complete file
+func parseCompleteFile(data map[string]interface{}, filename string, frontmatter string) ([]byte, error) {
+ mainContent := ""
+
+ if _, ok := data["content"]; ok {
+ // The main content of the file
+ mainContent = data["content"].(string)
+ mainContent = "\n\n" + strings.TrimSpace(mainContent) + "\n"
+
+ // Removes the main content from the rest of the frontmatter
+ delete(data, "content")
+ }
+
+ if _, ok := data["date"]; ok {
+ data["date"] = data["date"].(string) + ":00"
+ }
+
+ front, err := parseFrontMatter(data, frontmatter)
+
+ if err != nil {
+ fmt.Println(frontmatter)
+ return []byte{}, err
+ }
+
+ // Generates the final file
+ f := new(bytes.Buffer)
+ f.Write(front)
+ f.Write([]byte(mainContent))
+ return f.Bytes(), nil
+}
diff --git a/handlers/single.go b/handlers/single.go
new file mode 100644
index 00000000..85d0ff5a
--- /dev/null
+++ b/handlers/single.go
@@ -0,0 +1,48 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/hacdias/caddy-filemanager/config"
+ "github.com/hacdias/caddy-filemanager/file"
+ "github.com/hacdias/caddy-filemanager/page"
+ "github.com/hacdias/caddy-filemanager/utils/errors"
+)
+
+// ServeSingle serves a single file in an editor (if it is editable), shows the
+// plain file, or downloads it if it can't be shown.
+func ServeSingle(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User, i *file.Info) (int, error) {
+ var err error
+
+ if err = i.RetrieveFileType(); err != nil {
+ return errors.ErrorToHTTPCode(err, true), err
+ }
+
+ if i.Type == "text" {
+ if err = i.Read(); err != nil {
+ return errors.ErrorToHTTPCode(err, true), err
+ }
+ }
+
+ p := &page.Page{
+ Info: &page.Info{
+ Name: i.Name(),
+ Path: i.VirtualPath,
+ IsDir: false,
+ Data: i,
+ User: u,
+ Config: c,
+ },
+ }
+
+ if i.CanBeEdited() && u.AllowEdit {
+ p.Data, err = GetEditor(i)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ return p.PrintAsHTML(w, "frontmatter", "editor")
+ }
+
+ return p.PrintAsHTML(w, "single")
+}
diff --git a/errors/errors.go b/page/error.go
similarity index 84%
rename from errors/errors.go
rename to page/error.go
index d404e54e..fea2debf 100644
--- a/errors/errors.go
+++ b/page/error.go
@@ -1,4 +1,4 @@
-package errors
+package page
import (
"net/http"
@@ -6,7 +6,7 @@ import (
"strings"
)
-const template = `
+const errTemplate = `
TITLE
@@ -32,6 +32,9 @@ const template = `
color: #eee;
font-weight: bold;
}
+ p {
+ line-height: 1.3;
+ }
@@ -45,9 +48,9 @@ const template = `
`
-// PrintHTML prints the error page
-func PrintHTML(w http.ResponseWriter, code int, err error) (int, error) {
- tpl := template
+// PrintErrorHTML prints the error page
+func PrintErrorHTML(w http.ResponseWriter, code int, err error) (int, error) {
+ tpl := errTemplate
tpl = strings.Replace(tpl, "TITLE", strconv.Itoa(code)+" "+http.StatusText(code), -1)
tpl = strings.Replace(tpl, "CODE", err.Error(), -1)
diff --git a/page/page.go b/page/page.go
index a8266dc6..dd7c60a3 100644
--- a/page/page.go
+++ b/page/page.go
@@ -1,3 +1,4 @@
+// Package page is used to render the HTML to the end user
package page
import (
@@ -13,13 +14,13 @@ import (
"github.com/hacdias/caddy-filemanager/utils/variables"
)
-// Page contains the informations and functions needed to show the page
+// Page contains the informations and functions needed to show the Page
type Page struct {
*Info
Minimal bool
}
-// Info contains the information of a page
+// Info contains the information of a Page
type Info struct {
Name string
Path string
@@ -101,7 +102,7 @@ func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro
// For each template, add it to the the tpl variable
for i, t := range templates {
// Get the template from the assets
- page, err := assets.Asset("templates/" + t + ".tmpl")
+ Page, err := assets.Asset("templates/" + t + ".tmpl")
// Check if there is some error. If so, the template doesn't exist
if err != nil {
@@ -112,9 +113,9 @@ func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro
// If it's the first iteration, creates a new template and add the
// functions map
if i == 0 {
- tpl, err = template.New(t).Funcs(functions).Parse(string(page))
+ tpl, err = template.New(t).Funcs(functions).Parse(string(Page))
} else {
- tpl, err = tpl.Parse(string(page))
+ tpl, err = tpl.Parse(string(Page))
}
if err != nil {
@@ -135,7 +136,7 @@ func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro
return http.StatusOK, nil
}
-// PrintAsJSON prints the current page infromation in JSON
+// PrintAsJSON prints the current Page infromation in JSON
func (p Page) PrintAsJSON(w http.ResponseWriter) (int, error) {
marsh, err := json.Marshal(p.Info.Data)
if err != nil {
diff --git a/utils/errors/http.go b/utils/errors/errors.go
similarity index 66%
rename from utils/errors/http.go
rename to utils/errors/errors.go
index 644f9c55..ad4a2743 100644
--- a/utils/errors/http.go
+++ b/utils/errors/errors.go
@@ -5,13 +5,16 @@ import (
"os"
)
-// ToHTTPCode gets the respective HTTP code for an error
-func ToHTTPCode(err error) int {
+func ErrorToHTTPCode(err error, gone bool) int {
switch {
case os.IsPermission(err):
return http.StatusForbidden
case os.IsNotExist(err):
- return http.StatusNotFound
+ if !gone {
+ return http.StatusNotFound
+ }
+
+ return http.StatusGone
case os.IsExist(err):
return http.StatusGone
default: