diff --git a/editor.go b/editor.go
new file mode 100644
index 00000000..d2b591d2
--- /dev/null
+++ b/editor.go
@@ -0,0 +1,120 @@
+package filemanager
+
+import (
+ "bytes"
+ "errors"
+ "net/http"
+ "path/filepath"
+ "strings"
+
+ "github.com/hacdias/filemanager/frontmatter"
+ "github.com/spf13/hugo/parser"
+)
+
+// Editor contains the information for the editor page
+type Editor struct {
+ Class string
+ Mode string
+ Visual bool
+ Content string
+ FrontMatter struct {
+ Content *frontmatter.Content
+ Rune rune
+ }
+}
+
+// GetEditor gets the editor based on a Info struct
+func GetEditor(r *http.Request, i *FileInfo) (*Editor, error) {
+ var err error
+
+ // Create a new editor variable and set the mode
+ e := new(Editor)
+ e.Mode = editorMode(i.Name)
+ e.Class = editorClass(e.Mode)
+
+ if e.Class == "frontmatter-only" || e.Class == "complete" {
+ e.Visual = true
+ }
+
+ if r.URL.Query().Get("visual") == "false" {
+ e.Class = "content-only"
+ }
+
+ hasRune := frontmatter.HasRune(i.content)
+
+ if e.Class == "frontmatter-only" && !hasRune {
+ e.FrontMatter.Rune, err = frontmatter.StringFormatToRune(e.Mode)
+ if err != nil {
+ goto Error
+ }
+ i.content = frontmatter.AppendRune(i.content, e.FrontMatter.Rune)
+ hasRune = true
+ }
+
+ if e.Class == "frontmatter-only" && hasRune {
+ e.FrontMatter.Content, _, err = frontmatter.Pretty(i.content)
+ if err != nil {
+ goto Error
+ }
+ }
+
+ if e.Class == "complete" && hasRune {
+ var page parser.Page
+ // Starts a new buffer and parses the file using Hugo's functions
+ buffer := bytes.NewBuffer(i.content)
+ page, err = parser.ReadFrom(buffer)
+
+ if err != nil {
+ goto Error
+ }
+
+ // Parses the page content and the frontmatter
+ e.Content = strings.TrimSpace(string(page.Content()))
+ e.FrontMatter.Rune = rune(i.content[0])
+ e.FrontMatter.Content, _, err = frontmatter.Pretty(page.FrontMatter())
+ }
+
+ if e.Class == "complete" && !hasRune {
+ err = errors.New("Complete but without rune")
+ }
+
+Error:
+ if e.Class == "content-only" || err != nil {
+ e.Class = "content-only"
+ e.Content = i.StringifyContent()
+ }
+
+ return e, nil
+}
+
+func editorClass(mode string) string {
+ switch mode {
+ case "json", "toml", "yaml":
+ return "frontmatter-only"
+ case "markdown", "asciidoc", "rst":
+ return "complete"
+ }
+
+ return "content-only"
+}
+
+func editorMode(filename string) string {
+ mode := strings.TrimPrefix(filepath.Ext(filename), ".")
+
+ switch mode {
+ case "md", "markdown", "mdown", "mmark":
+ mode = "markdown"
+ case "asciidoc", "adoc", "ad":
+ mode = "asciidoc"
+ case "rst":
+ mode = "rst"
+ case "html", "htm":
+ mode = "html"
+ case "js":
+ mode = "javascript"
+ case "go":
+ mode = "golang"
+ }
+
+ return mode
+}
diff --git a/error.go b/error.go
new file mode 100644
index 00000000..1a77cc97
--- /dev/null
+++ b/error.go
@@ -0,0 +1,65 @@
+package filemanager
+
+import (
+ "net/http"
+ "strconv"
+ "strings"
+)
+
+const errTemplate = `
+
+
+ TITLE
+
+
+
+
+
+
+
+
TITLE
+
+
Try reloading the page or hitting the back button. If this error persists, it seems that you may have found a bug! Please create an issue at hacdias/caddy-filemanager repository on GitHub with the code below.
+
+
CODE
+
+`
+
+// 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)
+
+ _, err = w.Write([]byte(tpl))
+
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ return http.StatusOK, nil
+}
diff --git a/filemanager.go b/filemanager.go
index 2e5b8ff3..f65c3362 100644
--- a/filemanager.go
+++ b/filemanager.go
@@ -1 +1,99 @@
package filemanager
+
+import (
+ "net/http"
+ "regexp"
+ "strings"
+
+ rice "github.com/GeertJohan/go.rice"
+ "golang.org/x/net/webdav"
+)
+
+// FileManager is a file manager instance.
+type FileManager struct {
+ *User `json:"-"`
+ Assets *Assets `json:"-"`
+
+ // PrefixURL is a part of the URL that is trimmed from the http.Request.URL before
+ // it arrives to our handlers. It may be useful when using FileManager as a middleware
+ // such as in caddy-filemanager plugin.
+ PrefixURL string
+
+ // BaseURL is the path where the GUI will be accessible.
+ BaseURL string
+
+ // WebDavURL is the path where the WebDAV will be accessible. It can be set to "/"
+ // in order to override the GUI and only use the WebDAV.
+ WebDavURL string
+
+ // Users is a map with the different configurations for each user.
+ Users map[string]*User `json:"-"`
+
+ // TODO: event-based?
+ BeforeSave CommandFunc `json:"-"`
+ AfterSave CommandFunc `json:"-"`
+}
+
+// User contains the configuration for each user.
+type User struct {
+ 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
+ Rules []*Rule `json:"-"` // Access rules
+ StyleSheet string `json:"-"` // Costum stylesheet
+ AllowNew bool // Can create files and folders
+ AllowEdit bool // Can edit/rename files
+ AllowCommands bool // Can execute commands
+ Commands []string // Available Commands
+}
+
+// Assets are the static and front-end assets, such as JS, CSS and HTML templates.
+type Assets struct {
+ requiredJS rice.Box // JS that is always required to have in order to be usable.
+ Templates rice.Box
+ CSS rice.Box
+ JS rice.Box
+}
+
+// Rule is a dissalow/allow rule.
+type Rule struct {
+ Regex bool
+ Allow bool
+ Path string
+ Regexp *regexp.Regexp
+}
+
+// CommandFunc ...
+type CommandFunc func(r *http.Request, c *FileManager, u *User) error
+
+// AbsoluteURL ...
+func (m FileManager) AbsoluteURL() string {
+ return m.PrefixURL + m.BaseURL
+}
+
+// AbsoluteWebdavURL ...
+func (m FileManager) AbsoluteWebdavURL() string {
+ return m.PrefixURL + m.WebDavURL
+}
+
+// Allowed checks if the user has permission to access a directory/file.
+func (u User) Allowed(url string) bool {
+ var rule *Rule
+ i := len(u.Rules) - 1
+
+ for i >= 0 {
+ rule = u.Rules[i]
+
+ if rule.Regex {
+ if rule.Regexp.MatchString(url) {
+ return rule.Allow
+ }
+ } else if strings.HasPrefix(url, rule.Path) {
+ return rule.Allow
+ }
+
+ i--
+ }
+
+ return true
+}
diff --git a/frontmatter/frontmatter.go b/frontmatter/frontmatter.go
new file mode 100644
index 00000000..ba28af20
--- /dev/null
+++ b/frontmatter/frontmatter.go
@@ -0,0 +1,276 @@
+package frontmatter
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "log"
+ "reflect"
+ "sort"
+ "strconv"
+ "strings"
+
+ "gopkg.in/yaml.v2"
+
+ "github.com/BurntSushi/toml"
+ "github.com/hacdias/filemanager/variables"
+
+ "github.com/spf13/cast"
+)
+
+const (
+ mainName = "#MAIN#"
+ objectType = "object"
+ arrayType = "array"
+)
+
+var mainTitle = ""
+
+// Pretty creates a new FrontMatter object
+func Pretty(content []byte) (*Content, string, error) {
+ data, err := Unmarshal(content)
+
+ if err != nil {
+ return &Content{}, "", err
+ }
+
+ kind := reflect.ValueOf(data).Kind()
+
+ if kind == reflect.Invalid {
+ return &Content{}, "", nil
+ }
+
+ object := new(Block)
+ object.Type = objectType
+ object.Name = mainName
+
+ if kind == reflect.Map {
+ object.Type = objectType
+ } else if kind == reflect.Slice || kind == reflect.Array {
+ object.Type = arrayType
+ }
+
+ return rawToPretty(data, object), mainTitle, nil
+}
+
+// Unmarshal returns the data of the frontmatter
+func Unmarshal(content []byte) (interface{}, error) {
+ mark := rune(content[0])
+ var data interface{}
+
+ switch mark {
+ case '-':
+ // If it's YAML
+ if err := yaml.Unmarshal(content, &data); err != nil {
+ return nil, err
+ }
+ case '+':
+ // If it's TOML
+ content = bytes.Replace(content, []byte("+"), []byte(""), -1)
+ if _, err := toml.Decode(string(content), &data); err != nil {
+ return nil, err
+ }
+ case '{', '[':
+ // If it's JSON
+ if err := json.Unmarshal(content, &data); err != nil {
+ return nil, err
+ }
+ default:
+ return nil, errors.New("Invalid frontmatter type")
+ }
+
+ return data, nil
+}
+
+// Marshal encodes the interface in a specific format
+func Marshal(data interface{}, mark rune) ([]byte, error) {
+ b := new(bytes.Buffer)
+
+ switch mark {
+ case '+':
+ enc := toml.NewEncoder(b)
+ err := enc.Encode(data)
+ if err != nil {
+ return nil, err
+ }
+ return b.Bytes(), nil
+ case '{':
+ by, err := json.MarshalIndent(data, "", " ")
+ if err != nil {
+ return nil, err
+ }
+ b.Write(by)
+ _, err = b.Write([]byte("\n"))
+ if err != nil {
+ return nil, err
+ }
+ return b.Bytes(), nil
+ case '-':
+ by, err := yaml.Marshal(data)
+ if err != nil {
+ return nil, err
+ }
+ b.Write(by)
+ _, err = b.Write([]byte("..."))
+ if err != nil {
+ return nil, err
+ }
+ return b.Bytes(), nil
+ default:
+ return nil, errors.New("Unsupported Format provided")
+ }
+}
+
+// Content is the block content
+type Content struct {
+ Other interface{}
+ Fields []*Block
+ Arrays []*Block
+ Objects []*Block
+}
+
+// Block is a block
+type Block struct {
+ Name string
+ Title string
+ Type string
+ HTMLType string
+ Content *Content
+ Parent *Block
+}
+
+func rawToPretty(config interface{}, parent *Block) *Content {
+ objects := []*Block{}
+ arrays := []*Block{}
+ fields := []*Block{}
+
+ cnf := map[string]interface{}{}
+ kind := reflect.TypeOf(config)
+
+ switch kind {
+ case reflect.TypeOf(map[interface{}]interface{}{}):
+ for key, value := range config.(map[interface{}]interface{}) {
+ cnf[key.(string)] = value
+ }
+ case reflect.TypeOf([]map[string]interface{}{}):
+ for index, value := range config.([]map[string]interface{}) {
+ cnf[strconv.Itoa(index)] = value
+ }
+ case reflect.TypeOf([]map[interface{}]interface{}{}):
+ for index, value := range config.([]map[interface{}]interface{}) {
+ cnf[strconv.Itoa(index)] = value
+ }
+ case reflect.TypeOf([]interface{}{}):
+ for index, value := range config.([]interface{}) {
+ cnf[strconv.Itoa(index)] = value
+ }
+ default:
+ cnf = config.(map[string]interface{})
+ }
+
+ for name, element := range cnf {
+ if variables.IsMap(element) {
+ objects = append(objects, handleObjects(element, parent, name))
+ } else if variables.IsSlice(element) {
+ arrays = append(arrays, handleArrays(element, parent, name))
+ } else {
+ if name == "title" && parent.Name == mainName {
+ mainTitle = element.(string)
+ }
+ fields = append(fields, handleFlatValues(element, parent, name))
+ }
+ }
+
+ sort.Sort(sortByTitle(fields))
+ sort.Sort(sortByTitle(arrays))
+ sort.Sort(sortByTitle(objects))
+ return &Content{
+ Fields: fields,
+ Arrays: arrays,
+ Objects: objects,
+ }
+}
+
+type sortByTitle []*Block
+
+func (f sortByTitle) Len() int { return len(f) }
+func (f sortByTitle) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
+func (f sortByTitle) Less(i, j int) bool {
+ return strings.ToLower(f[i].Name) < strings.ToLower(f[j].Name)
+}
+
+func handleObjects(content interface{}, parent *Block, name string) *Block {
+ c := new(Block)
+ c.Parent = parent
+ c.Type = objectType
+ c.Title = name
+
+ if parent.Name == mainName {
+ c.Name = c.Title
+ } else if parent.Type == arrayType {
+ c.Name = parent.Name + "[" + name + "]"
+ } else {
+ c.Name = parent.Name + "." + c.Title
+ }
+
+ c.Content = rawToPretty(content, c)
+ return c
+}
+
+func handleArrays(content interface{}, parent *Block, name string) *Block {
+ c := new(Block)
+ c.Parent = parent
+ c.Type = arrayType
+ c.Title = name
+
+ if parent.Name == mainName {
+ c.Name = name
+ } else {
+ c.Name = parent.Name + "." + name
+ }
+
+ c.Content = rawToPretty(content, c)
+ return c
+}
+
+func handleFlatValues(content interface{}, parent *Block, name string) *Block {
+ c := new(Block)
+ c.Parent = parent
+
+ switch content.(type) {
+ case bool:
+ c.Type = "boolean"
+ case int, float32, float64:
+ c.Type = "number"
+ default:
+ c.Type = "string"
+ }
+
+ c.Content = &Content{Other: content}
+
+ switch strings.ToLower(name) {
+ case "description":
+ c.HTMLType = "textarea"
+ case "date", "publishdate":
+ c.HTMLType = "datetime"
+ c.Content = &Content{Other: cast.ToTime(content)}
+ default:
+ c.HTMLType = "text"
+ }
+
+ if parent.Type == arrayType {
+ c.Name = parent.Name + "[]"
+ c.Title = content.(string)
+ } else if parent.Type == objectType {
+ c.Title = name
+ c.Name = parent.Name + "." + name
+
+ if parent.Name == mainName {
+ c.Name = name
+ }
+ } else {
+ log.Panic("Parent type not allowed in handleFlatValues.")
+ }
+
+ return c
+}
diff --git a/frontmatter/runes.go b/frontmatter/runes.go
new file mode 100644
index 00000000..b4ad1dc2
--- /dev/null
+++ b/frontmatter/runes.go
@@ -0,0 +1,58 @@
+package frontmatter
+
+import (
+ "bytes"
+ "errors"
+ "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, mark rune) []byte {
+ frontmatter = bytes.TrimSpace(frontmatter)
+
+ switch mark {
+ case '-':
+ return []byte("---\n" + string(frontmatter) + "\n---")
+ case '+':
+ return []byte("+++\n" + string(frontmatter) + "\n+++")
+ case '{':
+ return []byte("{\n" + string(frontmatter) + "\n}")
+ }
+
+ return frontmatter
+}
+
+// RuneToStringFormat converts the rune to a string with the format
+func RuneToStringFormat(mark rune) (string, error) {
+ switch mark {
+ case '-':
+ return "yaml", nil
+ case '+':
+ return "toml", nil
+ case '{', '}':
+ return "json", nil
+ default:
+ return "", errors.New("Unsupported format type")
+ }
+}
+
+// StringFormatToRune converts the format name to its rune
+func StringFormatToRune(format string) (rune, error) {
+ switch format {
+ case "yaml":
+ return '-', nil
+ case "toml":
+ return '+', nil
+ case "json":
+ return '{', nil
+ default:
+ return '0', errors.New("Unsupported format type")
+ }
+}
diff --git a/frontmatter/runes_test.go b/frontmatter/runes_test.go
new file mode 100644
index 00000000..6d120948
--- /dev/null
+++ b/frontmatter/runes_test.go
@@ -0,0 +1,131 @@
+package frontmatter
+
+import "testing"
+
+type hasRuneTest struct {
+ File []byte
+ Return bool
+}
+
+var testHasRune = []hasRuneTest{
+ hasRuneTest{
+ File: []byte(`---
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+Sed auctor libero eget ante fermentum commodo.
+---`),
+ Return: true,
+ },
+ hasRuneTest{
+ File: []byte(`+++
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+Sed auctor libero eget ante fermentum commodo.
++++`),
+ Return: true,
+ },
+ hasRuneTest{
+ File: []byte(`{
+ "json": "Lorem ipsum dolor sit amet"
+}`),
+ Return: true,
+ },
+ hasRuneTest{
+ File: []byte(`+`),
+ Return: false,
+ },
+ hasRuneTest{
+ File: []byte(`++`),
+ Return: false,
+ },
+ hasRuneTest{
+ File: []byte(`-`),
+ Return: false,
+ },
+ hasRuneTest{
+ File: []byte(`--`),
+ Return: false,
+ },
+ hasRuneTest{
+ File: []byte(`Lorem ipsum`),
+ Return: false,
+ },
+}
+
+func TestHasRune(t *testing.T) {
+ for _, test := range testHasRune {
+ if HasRune(test.File) != test.Return {
+ t.Error("Incorrect value on HasRune")
+ }
+ }
+}
+
+type appendRuneTest struct {
+ Before []byte
+ After []byte
+ Mark rune
+}
+
+var testAppendRuneTest = []appendRuneTest{}
+
+func TestAppendRune(t *testing.T) {
+ for i, test := range testAppendRuneTest {
+ if !compareByte(AppendRune(test.Before, test.Mark), test.After) {
+ t.Errorf("Incorrect value on AppendRune of Test %d", i)
+ }
+ }
+}
+
+func compareByte(a, b []byte) bool {
+ if a == nil && b == nil {
+ return true
+ }
+
+ if a == nil || b == nil {
+ return false
+ }
+
+ if len(a) != len(b) {
+ return false
+ }
+
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+
+ return true
+}
+
+var testRuneToStringFormat = map[rune]string{
+ '-': "yaml",
+ '+': "toml",
+ '{': "json",
+ '}': "json",
+ '1': "",
+ 'a': "",
+}
+
+func TestRuneToStringFormat(t *testing.T) {
+ for mark, format := range testRuneToStringFormat {
+ val, _ := RuneToStringFormat(mark)
+ if val != format {
+ t.Errorf("Incorrect value on RuneToStringFormat of %v; want: %s; got: %s", mark, format, val)
+ }
+ }
+}
+
+var testStringFormatToRune = map[string]rune{
+ "yaml": '-',
+ "toml": '+',
+ "json": '{',
+ "lorem": '0',
+}
+
+func TestStringFormatToRune(t *testing.T) {
+ for format, mark := range testStringFormatToRune {
+ val, _ := StringFormatToRune(format)
+ if val != mark {
+ t.Errorf("Incorrect value on StringFormatToRune of %s; want: %v; got: %v", format, mark, val)
+ }
+ }
+}
diff --git a/http.go b/http.go
new file mode 100644
index 00000000..cfe4b852
--- /dev/null
+++ b/http.go
@@ -0,0 +1,167 @@
+package filemanager
+
+import (
+ "errors"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+)
+
+// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
+func (c *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
+ var (
+ fi *FileInfo
+ code int
+ err error
+ user *User
+ )
+
+ // 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 + AssetsURL) {
+ if r.Method == http.MethodGet {
+ return serveAssets(w, r, c)
+ }
+
+ return http.StatusForbidden, nil
+ }
+
+ username, _, _ := r.BasicAuth()
+ if _, ok := c.Users[username]; ok {
+ user = c.Users[username]
+ } else {
+ user = c.User
+ }
+
+ // Checks if the request URL is for the WebDav server
+ if httpserver.Path(r.URL.Path).Matches(c.WebDavURL) {
+ // Checks for user permissions relatively to this PATH
+ if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.WebDavURL)) {
+ return http.StatusForbidden, nil
+ }
+
+ switch r.Method {
+ case "GET", "HEAD":
+ // Excerpt from RFC4918, section 9.4:
+ //
+ // GET, when applied to a collection, may return the contents of an
+ // "index.html" resource, a human-readable view of the contents of
+ // the collection, or something else altogether.
+ //
+ // It was decided on https://github.com/hacdias/caddy-filemanager/issues/85
+ // that GET, for collections, will return the same as PROPFIND method.
+ path := strings.Replace(r.URL.Path, c.WebDavURL, "", 1)
+ path = user.Scope + "/" + path
+ path = filepath.Clean(path)
+
+ var i os.FileInfo
+ i, err = os.Stat(path)
+ if err != nil {
+ // Is there any error? WebDav will handle it... no worries.
+ break
+ }
+
+ if i.IsDir() {
+ r.Method = "PROPFIND"
+
+ if r.Method == "HEAD" {
+ w = newResponseWriterNoBody(w)
+ }
+ }
+ case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE":
+ if !user.AllowEdit {
+ return http.StatusForbidden, nil
+ }
+ case "MKCOL", "COPY":
+ if !user.AllowNew {
+ return http.StatusForbidden, nil
+ }
+ }
+
+ // Preprocess the PUT request if it's the case
+ if r.Method == http.MethodPut {
+ if err = c.BeforeSave(r, c, user); err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ if put(w, r, c, user) != nil {
+ return http.StatusInternalServerError, err
+ }
+ }
+
+ c.Handler.ServeHTTP(w, r)
+ if err = c.AfterSave(r, c, user); err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ return 0, nil
+ }
+
+ w.Header().Set("x-frame-options", "SAMEORIGIN")
+ w.Header().Set("x-content-type", "nosniff")
+ w.Header().Set("x-xss-protection", "1; mode=block")
+
+ // 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 PrintErrorHTML(
+ w, http.StatusForbidden,
+ errors.New("You don't have permission to access this page"),
+ )
+ }
+
+ return http.StatusForbidden, nil
+ }
+
+ if r.URL.Query().Get("search") != "" {
+ return search(w, r, c, user)
+ }
+
+ if r.URL.Query().Get("command") != "" {
+ return command(w, r, c, user)
+ }
+
+ if r.Method == http.MethodGet {
+ // Gets the information of the directory/file
+ fi, err = GetInfo(r.URL, c, user)
+ if err != nil {
+ if r.Method == http.MethodGet {
+ return PrintErrorHTML(w, code, err)
+ }
+ code = errorToHTTP(err, false)
+ 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.PrefixURL+r.URL.Path+"/", http.StatusTemporaryRedirect)
+ return 0, nil
+ }
+
+ switch {
+ case r.URL.Query().Get("download") != "":
+ code, err = download(w, r, fi)
+ case r.URL.Query().Get("raw") == "true" && !fi.IsDir:
+ http.ServeFile(w, r, fi.Path)
+ code, err = 0, nil
+ case !fi.IsDir && r.URL.Query().Get("checksum") != "":
+ code, err = checksum(w, r, fi)
+ case fi.IsDir:
+ code, err = serveListing(w, r, c, user, fi)
+ default:
+ code, err = serveSingle(w, r, c, user, fi)
+ }
+
+ if err != nil {
+ code, err = PrintErrorHTML(w, code, err)
+ }
+
+ return code, err
+ }
+
+ return http.StatusNotImplemented, nil
+}
diff --git a/http_assets.go b/http_assets.go
new file mode 100644
index 00000000..b15513d6
--- /dev/null
+++ b/http_assets.go
@@ -0,0 +1,49 @@
+package filemanager
+
+import (
+ "errors"
+ "mime"
+ "net/http"
+ "path/filepath"
+ "strings"
+)
+
+// AssetsURL is the url of the assets
+const AssetsURL = "/_filemanagerinternal"
+
+// Serve provides the needed assets for the front-end
+func serveAssets(w http.ResponseWriter, r *http.Request, m *FileManager) (int, error) {
+ // gets the filename to be used with Assets function
+ filename := strings.Replace(r.URL.Path, m.BaseURL+AssetsURL, "", 1)
+
+ var file []byte
+ var err error
+
+ switch {
+ case strings.HasPrefix(filename, "/css"):
+ filename = strings.Replace(filename, "/css/", "", 1)
+ file, err = m.Assets.CSS.Bytes(filename)
+ case strings.HasPrefix(filename, "/js"):
+ filename = strings.Replace(filename, "/js/", "", 1)
+ file, err = m.Assets.requiredJS.Bytes(filename)
+ case strings.HasPrefix(filename, "/vendor"):
+ filename = strings.Replace(filename, "/vendor/", "", 1)
+ file, err = m.Assets.JS.Bytes(filename)
+ default:
+ err = errors.New("not found")
+ }
+
+ if err != nil {
+ return http.StatusNotFound, nil
+ }
+
+ // Get the file extension and its mimetype
+ extension := filepath.Ext(filename)
+ mediatype := mime.TypeByExtension(extension)
+
+ // Write the header with the Content-Type and write the file
+ // content to the buffer
+ w.Header().Set("Content-Type", mediatype)
+ w.Write(file)
+ return 200, nil
+}
diff --git a/http_checksum.go b/http_checksum.go
new file mode 100644
index 00000000..eeeddcbc
--- /dev/null
+++ b/http_checksum.go
@@ -0,0 +1,50 @@
+package filemanager
+
+import (
+ "crypto/md5"
+ "crypto/sha1"
+ "crypto/sha256"
+ "crypto/sha512"
+ "encoding/hex"
+ e "errors"
+ "hash"
+ "io"
+ "net/http"
+ "os"
+)
+
+// checksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512.
+func checksum(w http.ResponseWriter, r *http.Request, i *FileInfo) (int, error) {
+ query := r.URL.Query().Get("checksum")
+
+ file, err := os.Open(i.Path)
+ if err != nil {
+ return errorToHTTP(err, true), err
+ }
+
+ defer file.Close()
+
+ var h hash.Hash
+
+ switch query {
+ case "md5":
+ h = md5.New()
+ case "sha1":
+ h = sha1.New()
+ case "sha256":
+ h = sha256.New()
+ case "sha512":
+ h = sha512.New()
+ default:
+ return http.StatusBadRequest, e.New("Unknown HASH type")
+ }
+
+ _, err = io.Copy(h, file)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ val := hex.EncodeToString(h.Sum(nil))
+ w.Write([]byte(val))
+ return http.StatusOK, nil
+}
diff --git a/http_command.go b/http_command.go
new file mode 100644
index 00000000..eea6fa48
--- /dev/null
+++ b/http_command.go
@@ -0,0 +1,135 @@
+package filemanager
+
+import (
+ "bytes"
+ "net/http"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+var upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+}
+
+var (
+ cmdNotImplemented = []byte("Command not implemented.")
+ cmdNotAllowed = []byte("Command not allowed.")
+)
+
+// command handles the requests for VCS related commands: git, svn and mercurial
+func command(w http.ResponseWriter, r *http.Request, c *FileManager, u *User) (int, error) {
+ // Upgrades the connection to a websocket and checks for errors.
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ return 0, err
+ }
+ defer conn.Close()
+
+ var (
+ message []byte
+ command []string
+ )
+
+ // Starts an infinite loop until a valid command is captured.
+ for {
+ _, message, err = conn.ReadMessage()
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ command = strings.Split(string(message), " ")
+ if len(command) != 0 {
+ break
+ }
+ }
+
+ // Check if the command is allowed
+ allowed := false
+
+ for _, cmd := range u.Commands {
+ if cmd == command[0] {
+ allowed = true
+ }
+ }
+
+ if !allowed {
+ err = conn.WriteMessage(websocket.BinaryMessage, cmdNotAllowed)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ return 0, nil
+ }
+
+ // Check if the program is talled is installed on the computer.
+ if _, err = exec.LookPath(command[0]); err != nil {
+ err = conn.WriteMessage(websocket.BinaryMessage, cmdNotImplemented)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ return http.StatusNotImplemented, nil
+ }
+
+ // Gets the path and initializes a buffer.
+ path := strings.Replace(r.URL.Path, c.BaseURL, c.Scope, 1)
+ path = filepath.Clean(path)
+ buff := new(bytes.Buffer)
+
+ // Sets up the command executation.
+ cmd := exec.Command(command[0], command[1:]...)
+ cmd.Dir = path
+ cmd.Stderr = buff
+ cmd.Stdout = buff
+
+ // Starts the command and checks for errors.
+ err = cmd.Start()
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ // Set a 'done' variable to check whetever the command has already finished
+ // running or not. This verification is done using a goroutine that uses the
+ // method .Wait() from the command.
+ done := false
+ go func() {
+ err = cmd.Wait()
+ done = true
+ }()
+
+ // Function to print the current information on the buffer to the connection.
+ print := func() error {
+ by := buff.Bytes()
+ if len(by) > 0 {
+ err = conn.WriteMessage(websocket.TextMessage, by)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+ }
+
+ // While the command hasn't finished running, continue sending the output
+ // to the client in intervals of 100 milliseconds.
+ for !done {
+ if err = print(); err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ time.Sleep(100 * time.Millisecond)
+ }
+
+ // After the command is done executing, send the output one more time to the
+ // browser to make sure it gets the latest information.
+ if err = print(); err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ return 0, nil
+}
diff --git a/http_download.go b/http_download.go
new file mode 100644
index 00000000..dc028dc2
--- /dev/null
+++ b/http_download.go
@@ -0,0 +1,95 @@
+package filemanager
+
+import (
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/mholt/archiver"
+)
+
+// download creates an archive 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, i *FileInfo) (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 {
+ name, err := url.QueryUnescape(name)
+
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ 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)
+ case "tarxz":
+ extension, err = ".tar.xz", archiver.TarXZ.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
+ }
+
+ name := i.Name
+ if name == "." || name == "" {
+ name = "download"
+ }
+
+ w.Header().Set("Content-Disposition", "attachment; filename="+name+extension)
+ io.Copy(w, file)
+ return http.StatusOK, nil
+}
diff --git a/http_listing.go b/http_listing.go
new file mode 100644
index 00000000..c4ea51b1
--- /dev/null
+++ b/http_listing.go
@@ -0,0 +1,144 @@
+package filemanager
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "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 *FileManager, u *User, i *FileInfo) (int, error) {
+ var err error
+
+ // Loads the content of the directory
+ listing, err := GetListing(u, i.VirtualPath, c.PrefixURL+r.URL.Path)
+ if err != nil {
+ return errorToHTTP(err, true), err
+ }
+
+ listing.Context = httpserver.Context{
+ Root: http.Dir(u.Scope),
+ Req: r,
+ URL: r.URL,
+ }
+
+ cookieScope := c.BaseURL
+ if cookieScope == "" {
+ cookieScope = "/"
+ }
+
+ // Copy the query values into the Listing struct
+ var limit int
+ listing.Sort, listing.Order, limit, err = handleSortOrder(w, r, cookieScope)
+ 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
+ }
+
+ displayMode := r.URL.Query().Get("display")
+
+ if displayMode == "" {
+ if displayCookie, err := r.Cookie("display"); err == nil {
+ displayMode = displayCookie.Value
+ }
+ }
+
+ if displayMode == "" || (displayMode != "mosaic" && displayMode != "list") {
+ displayMode = "mosaic"
+ }
+
+ http.SetCookie(w, &http.Cookie{
+ Name: "display",
+ Value: displayMode,
+ Path: cookieScope,
+ Secure: r.TLS != nil,
+ })
+
+ page := &Page{
+ Minimal: r.Header.Get("Minimal") == "true",
+ PageInfo: &PageInfo{
+ Name: listing.Name,
+ Path: i.VirtualPath,
+ IsDir: true,
+ User: u,
+ Config: c,
+ Display: displayMode,
+ 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/http_put.go b/http_put.go
new file mode 100644
index 00000000..67164119
--- /dev/null
+++ b/http_put.go
@@ -0,0 +1,138 @@
+package filemanager
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "io/ioutil"
+ "net/http"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/hacdias/filemanager/frontmatter"
+)
+
+// put is used to update a file that was edited
+func put(w http.ResponseWriter, r *http.Request, c *FileManager, u *User) (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, r.URL.Path); err != nil {
+ return
+ }
+ case "content-only":
+ mainContent := data["content"].(string)
+ mainContent = strings.TrimSpace(mainContent)
+ file = []byte(mainContent)
+ case "complete":
+ var mark rune
+
+ if v := r.Header.Get("Rune"); v != "" {
+ var n int
+ n, err = strconv.Atoi(v)
+ if err != nil {
+ return err
+ }
+
+ mark = rune(n)
+ }
+
+ if file, err = parseCompleteFile(data, r.URL.Path, mark); 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{}, front string) ([]byte, error) {
+ var mark rune
+
+ switch front {
+ case "toml":
+ mark = '+'
+ case "json":
+ mark = '{'
+ case "yaml":
+ mark = '-'
+ default:
+ return nil, errors.New("Unsupported Format provided")
+ }
+
+ return frontmatter.Marshal(data, mark)
+}
+
+// parseCompleteFile parses a complete file
+func parseCompleteFile(data map[string]interface{}, filename string, mark rune) ([]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 := frontmatter.Marshal(data, mark)
+ if err != nil {
+ return []byte{}, err
+ }
+
+ front = frontmatter.AppendRune(front, mark)
+
+ // Generates the final file
+ f := new(bytes.Buffer)
+ f.Write(front)
+ f.Write([]byte(mainContent))
+ return f.Bytes(), nil
+}
diff --git a/http_search.go b/http_search.go
new file mode 100644
index 00000000..e35918c8
--- /dev/null
+++ b/http_search.go
@@ -0,0 +1,117 @@
+package filemanager
+
+import (
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/gorilla/websocket"
+)
+
+type searchOptions struct {
+ CaseInsensitive bool
+ Terms []string
+}
+
+func parseSearch(value string) *searchOptions {
+ opts := &searchOptions{
+ CaseInsensitive: strings.Contains(value, "case:insensitive"),
+ }
+
+ // removes the options from the value
+ value = strings.Replace(value, "case:insensitive", "", -1)
+ value = strings.Replace(value, "case:sensitive", "", -1)
+ value = strings.TrimSpace(value)
+
+ if opts.CaseInsensitive {
+ value = strings.ToLower(value)
+ }
+
+ // if the value starts with " and finishes what that character, we will
+ // only search for that term
+ if value[0] == '"' && value[len(value)-1] == '"' {
+ unique := strings.TrimPrefix(value, "\"")
+ unique = strings.TrimSuffix(unique, "\"")
+
+ opts.Terms = []string{unique}
+ return opts
+ }
+
+ opts.Terms = strings.Split(value, " ")
+ return opts
+}
+
+// search searches for a file or directory.
+func search(w http.ResponseWriter, r *http.Request, c *FileManager, u *User) (int, error) {
+ // Upgrades the connection to a websocket and checks for errors.
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ return 0, err
+ }
+ defer conn.Close()
+
+ var (
+ value string
+ search *searchOptions
+ message []byte
+ )
+
+ // Starts an infinite loop until a valid command is captured.
+ for {
+ _, message, err = conn.ReadMessage()
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ if len(message) != 0 {
+ value = string(message)
+ break
+ }
+ }
+
+ search = parseSearch(value)
+ scope := strings.Replace(r.URL.Path, c.BaseURL, "", 1)
+ scope = strings.TrimPrefix(scope, "/")
+ scope = "/" + scope
+ scope = u.Scope + scope
+ scope = strings.Replace(scope, "\\", "/", -1)
+ scope = filepath.Clean(scope)
+
+ err = filepath.Walk(scope, func(path string, f os.FileInfo, err error) error {
+ if search.CaseInsensitive {
+ path = strings.ToLower(path)
+ }
+
+ path = strings.Replace(path, "\\", "/", -1)
+ is := false
+
+ for _, term := range search.Terms {
+ if is {
+ break
+ }
+
+ if strings.Contains(path, term) {
+ if !u.Allowed(path) {
+ return nil
+ }
+
+ is = true
+ }
+ }
+
+ if !is {
+ return nil
+ }
+
+ path = strings.TrimPrefix(path, scope)
+ path = strings.TrimPrefix(path, "/")
+ return conn.WriteMessage(websocket.TextMessage, []byte(path))
+ })
+
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ return http.StatusOK, nil
+}
diff --git a/http_single.go b/http_single.go
new file mode 100644
index 00000000..87f01315
--- /dev/null
+++ b/http_single.go
@@ -0,0 +1,50 @@
+package filemanager
+
+import (
+ "net/http"
+ "strings"
+)
+
+// 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 *FileManager, u *User, i *FileInfo) (int, error) {
+ var err error
+
+ if err = i.RetrieveFileType(); err != nil {
+ return errorToHTTP(err, true), err
+ }
+
+ p := &Page{
+ PageInfo: &PageInfo{
+ Name: i.Name,
+ Path: i.VirtualPath,
+ IsDir: false,
+ Data: i,
+ User: u,
+ Config: c,
+ },
+ }
+
+ // If the request accepts JSON, we send the file information.
+ if strings.Contains(r.Header.Get("Accept"), "application/json") {
+ return p.PrintAsJSON(w)
+ }
+
+ if i.Type == "text" {
+ if err = i.Read(); err != nil {
+ return errorToHTTP(err, true), err
+ }
+ }
+
+ if i.CanBeEdited() && u.AllowEdit {
+ p.Data, err = GetEditor(r, i)
+ p.Editor = true
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ return p.PrintAsHTML(w, "frontmatter", "editor")
+ }
+
+ return p.PrintAsHTML(w, "single")
+}
diff --git a/http_utils.go b/http_utils.go
new file mode 100644
index 00000000..cbb9fa3a
--- /dev/null
+++ b/http_utils.go
@@ -0,0 +1,50 @@
+package filemanager
+
+import (
+ "net/http"
+ "os"
+)
+
+// responseWriterNoBody is a wrapper used to suprress the body of the response
+// to a request. Mainly used for HEAD requests.
+type responseWriterNoBody struct {
+ http.ResponseWriter
+}
+
+// newResponseWriterNoBody creates a new responseWriterNoBody.
+func newResponseWriterNoBody(w http.ResponseWriter) *responseWriterNoBody {
+ return &responseWriterNoBody{w}
+}
+
+// Header executes the Header method from the http.ResponseWriter.
+func (w responseWriterNoBody) Header() http.Header {
+ return w.ResponseWriter.Header()
+}
+
+// Write suprresses the body.
+func (w responseWriterNoBody) Write(data []byte) (int, error) {
+ return 0, nil
+}
+
+// WriteHeader writes the header to the http.ResponseWriter.
+func (w responseWriterNoBody) WriteHeader(statusCode int) {
+ w.ResponseWriter.WriteHeader(statusCode)
+}
+
+// errorToHTTP converts errors to HTTP Status Code.
+func errorToHTTP(err error, gone bool) int {
+ switch {
+ case os.IsPermission(err):
+ return http.StatusForbidden
+ case os.IsNotExist(err):
+ if !gone {
+ return http.StatusNotFound
+ }
+
+ return http.StatusGone
+ case os.IsExist(err):
+ return http.StatusGone
+ default:
+ return http.StatusInternalServerError
+ }
+}
diff --git a/info.go b/info.go
new file mode 100644
index 00000000..741f03bd
--- /dev/null
+++ b/info.go
@@ -0,0 +1,163 @@
+package filemanager
+
+import (
+ "io/ioutil"
+ "mime"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ humanize "github.com/dustin/go-humanize"
+)
+
+// FileInfo contains the information about a particular file or directory
+type FileInfo struct {
+ Name string
+ Size int64
+ URL string
+ Extension string
+ ModTime time.Time
+ Mode os.FileMode
+ IsDir bool
+ Path string // Relative path to Current Working Directory
+ VirtualPath string // Relative path to user's virtual File System
+ Mimetype string
+ Type string
+ UserAllowed bool // Indicates if the user has enough permissions
+
+ content []byte
+}
+
+// GetInfo gets the file information and, in case of error, returns the
+// respective HTTP error code
+func GetInfo(url *url.URL, c *FileManager, u *User) (*FileInfo, error) {
+ var err error
+
+ i := &FileInfo{URL: c.PrefixURL + 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 = filepath.Clean(i.Path)
+
+ info, err := os.Stat(i.Path)
+ if err != nil {
+ return i, err
+ }
+
+ i.Name = info.Name()
+ i.ModTime = info.ModTime()
+ i.Mode = info.Mode()
+ i.IsDir = info.IsDir()
+ i.Size = info.Size()
+ i.Extension = filepath.Ext(i.Name)
+ return i, nil
+}
+
+var textExtensions = [...]string{
+ ".md", ".markdown", ".mdown", ".mmark",
+ ".asciidoc", ".adoc", ".ad",
+ ".rst",
+ ".json", ".toml", ".yaml", ".csv", ".xml", ".rss", ".conf", ".ini",
+ ".tex", ".sty",
+ ".css", ".sass", ".scss",
+ ".js",
+ ".html",
+ ".txt", ".rtf",
+ ".sh", ".bash", ".ps1", ".bat", ".cmd",
+ ".php", ".pl", ".py",
+ "Caddyfile",
+ ".c", ".cc", ".h", ".hh", ".cpp", ".hpp", ".f90",
+ ".f", ".bas", ".d", ".ada", ".nim", ".cr", ".java", ".cs", ".vala", ".vapi",
+}
+
+// RetrieveFileType obtains the mimetype and a simplified internal Type
+// using the first 512 bytes from the file.
+func (i FileInfo) RetrieveFileType() error {
+ i.Mimetype = mime.TypeByExtension(i.Extension)
+
+ if i.Mimetype == "" {
+ err := i.Read()
+ if err != nil {
+ return err
+ }
+
+ i.Mimetype = http.DetectContentType(i.content)
+ }
+
+ if strings.HasPrefix(i.Mimetype, "video") {
+ i.Type = "video"
+ return nil
+ }
+
+ if strings.HasPrefix(i.Mimetype, "audio") {
+ i.Type = "audio"
+ return nil
+ }
+
+ if strings.HasPrefix(i.Mimetype, "image") {
+ i.Type = "image"
+ return nil
+ }
+
+ if strings.HasPrefix(i.Mimetype, "text") {
+ i.Type = "text"
+ return nil
+ }
+
+ if strings.HasPrefix(i.Mimetype, "application/javascript") {
+ i.Type = "text"
+ return nil
+ }
+
+ // If the type isn't text (and is blob for example), it will check some
+ // common types that are mistaken not to be text.
+ for _, extension := range textExtensions {
+ if strings.HasSuffix(i.Name, extension) {
+ i.Type = "text"
+ return nil
+ }
+ }
+
+ i.Type = "blob"
+ return nil
+}
+
+// Reads the file.
+func (i FileInfo) 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 FileInfo) 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 FileInfo) HumanSize() string {
+ return humanize.IBytes(uint64(i.Size))
+}
+
+// HumanModTime returns the modified time of the file as a human-readable string.
+func (i FileInfo) HumanModTime(format string) string {
+ return i.ModTime.Format(format)
+}
+
+// CanBeEdited checks if the extension of a file is supported by the editor
+func (i FileInfo) CanBeEdited() bool {
+ return i.Type == "text"
+}
diff --git a/listing.go b/listing.go
new file mode 100644
index 00000000..b272c2af
--- /dev/null
+++ b/listing.go
@@ -0,0 +1,184 @@
+package filemanager
+
+import (
+ "context"
+ "net/url"
+ "os"
+ "path"
+ "sort"
+ "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 relatively to a File System
+ Path string
+ // 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
+ httpserver.Context `json:"-"`
+}
+
+// GetListing gets the information about a specific directory and its files.
+func GetListing(u *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(context.TODO(), 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 []FileInfo
+ dirCount, fileCount int
+ )
+
+ for _, f := range files {
+ name := f.Name()
+ allowed := u.Allowed("/" + name)
+
+ if !allowed {
+ continue
+ }
+
+ if f.IsDir() {
+ name += "/"
+ dirCount++
+ } else {
+ fileCount++
+ }
+
+ // Absolute URL
+ url := url.URL{Path: baseURL + name}
+
+ i := FileInfo{
+ Name: f.Name(),
+ Size: f.Size(),
+ ModTime: f.ModTime(),
+ Mode: f.Mode(),
+ IsDir: f.IsDir(),
+ URL: url.String(),
+ UserAllowed: allowed,
+ }
+ i.RetrieveFileType()
+
+ fileinfos = append(fileinfos, i)
+ }
+
+ 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/page.go b/page.go
new file mode 100644
index 00000000..b65a7365
--- /dev/null
+++ b/page.go
@@ -0,0 +1,168 @@
+package filemanager
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "html/template"
+ "log"
+ "net/http"
+ "strings"
+
+ "github.com/hacdias/filemanager/variables"
+)
+
+// Page contains the informations and functions needed to show the Page
+type Page struct {
+ *PageInfo
+ Minimal bool
+}
+
+// PageInfo contains the information of a Page
+type PageInfo struct {
+ Name string
+ Path string
+ IsDir bool
+ User *User
+ Config *FileManager
+ Data interface{}
+ Editor bool
+ Display string
+}
+
+// BreadcrumbMapItem ...
+type BreadcrumbMapItem struct {
+ Name string
+ URL string
+}
+
+// BreadcrumbMap returns p.Path where every element is a map
+// of URLs and path segment names.
+func (i PageInfo) BreadcrumbMap() []BreadcrumbMapItem {
+ result := []BreadcrumbMapItem{}
+
+ if len(i.Path) == 0 {
+ return result
+ }
+
+ // skip trailing slash
+ lpath := i.Path
+ if lpath[len(lpath)-1] == '/' {
+ lpath = lpath[:len(lpath)-1]
+ }
+
+ parts := strings.Split(lpath, "/")
+ for i, part := range parts {
+ if i == len(parts)-1 {
+ continue
+ }
+
+ if i == 0 && part == "" {
+ result = append([]BreadcrumbMapItem{{
+ Name: "/",
+ URL: "/",
+ }}, result...)
+ continue
+ }
+
+ result = append([]BreadcrumbMapItem{{
+ Name: part,
+ URL: strings.Join(parts[:i+1], "/") + "/",
+ }}, result...)
+ }
+
+ return result
+}
+
+// PreviousLink returns the path of the previous folder
+func (i PageInfo) PreviousLink() string {
+ path := strings.TrimSuffix(i.Path, "/")
+ path = strings.TrimPrefix(path, "/")
+ path = i.Config.AbsoluteURL() + "/" + path
+ path = path[0 : len(path)-len(i.Name)]
+
+ if len(path) < len(i.Config.AbsoluteURL()+"/") {
+ return ""
+ }
+
+ return path
+}
+
+// Create the functions map, then the template, check for erros and
+// execute the template if there aren't errors
+var functions = template.FuncMap{
+ "Defined": variables.FieldInStruct,
+ "CSS": func(s string) template.CSS {
+ return template.CSS(s)
+ },
+ "Marshal": func(v interface{}) template.JS {
+ a, _ := json.Marshal(v)
+ return template.JS(a)
+ },
+ "EncodeBase64": func(s string) string {
+ return base64.StdEncoding.EncodeToString([]byte(s))
+ },
+}
+
+// PrintAsHTML formats the page in HTML and executes the template
+func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, error) {
+
+ if p.Minimal {
+ templates = append(templates, "minimal")
+ } else {
+ templates = append(templates, "base")
+ }
+
+ var tpl *template.Template
+
+ // For each template, add it to the the tpl variable
+ for i, t := range templates {
+ // Get the template from the assets
+ Page, err := p.Config.Assets.Templates.String(t + ".tmpl")
+
+ // Check if there is some error. If so, the template doesn't exist
+ if err != nil {
+ log.Print(err)
+ return http.StatusInternalServerError, err
+ }
+
+ // 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(Page)
+ } else {
+ tpl, err = tpl.Parse(string(Page))
+ }
+
+ if err != nil {
+ log.Print(err)
+ return http.StatusInternalServerError, err
+ }
+ }
+
+ buf := &bytes.Buffer{}
+ err := tpl.Execute(buf, p.PageInfo)
+
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, err = buf.WriteTo(w)
+ return http.StatusOK, err
+}
+
+// PrintAsJSON prints the current Page information in JSON
+func (p Page) PrintAsJSON(w http.ResponseWriter) (int, error) {
+ marsh, err := json.MarshalIndent(p.PageInfo.Data, "", " ")
+ 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
+}
diff --git a/variables/types.go b/variables/types.go
new file mode 100644
index 00000000..ee43dad3
--- /dev/null
+++ b/variables/types.go
@@ -0,0 +1,13 @@
+package variables
+
+import "reflect"
+
+// IsMap checks if some variable is a map
+func IsMap(sth interface{}) bool {
+ return reflect.ValueOf(sth).Kind() == reflect.Map
+}
+
+// IsSlice checks if some variable is a slice
+func IsSlice(sth interface{}) bool {
+ return reflect.ValueOf(sth).Kind() == reflect.Slice
+}
diff --git a/variables/types_test.go b/variables/types_test.go
new file mode 100644
index 00000000..9955b9b2
--- /dev/null
+++ b/variables/types_test.go
@@ -0,0 +1,49 @@
+package variables
+
+import "testing"
+
+type interfaceToBool struct {
+ Value interface{}
+ Result bool
+}
+
+var testIsMap = []*interfaceToBool{
+ &interfaceToBool{"teste", false},
+ &interfaceToBool{453478, false},
+ &interfaceToBool{-984512, false},
+ &interfaceToBool{true, false},
+ &interfaceToBool{map[string]bool{}, true},
+ &interfaceToBool{map[int]bool{}, true},
+ &interfaceToBool{map[interface{}]bool{}, true},
+ &interfaceToBool{[]string{}, false},
+}
+
+func TestIsMap(t *testing.T) {
+ for _, test := range testIsMap {
+ if IsMap(test.Value) != test.Result {
+ t.Errorf("Incorrect value on IsMap for %v; want: %v; got: %v", test.Value, test.Result, !test.Result)
+ }
+ }
+}
+
+var testIsSlice = []*interfaceToBool{
+ &interfaceToBool{"teste", false},
+ &interfaceToBool{453478, false},
+ &interfaceToBool{-984512, false},
+ &interfaceToBool{true, false},
+ &interfaceToBool{map[string]bool{}, false},
+ &interfaceToBool{map[int]bool{}, false},
+ &interfaceToBool{map[interface{}]bool{}, false},
+ &interfaceToBool{[]string{}, true},
+ &interfaceToBool{[]int{}, true},
+ &interfaceToBool{[]bool{}, true},
+ &interfaceToBool{[]interface{}{}, true},
+}
+
+func TestIsSlice(t *testing.T) {
+ for _, test := range testIsSlice {
+ if IsSlice(test.Value) != test.Result {
+ t.Errorf("Incorrect value on IsSlice for %v; want: %v; got: %v", test.Value, test.Result, !test.Result)
+ }
+ }
+}
diff --git a/variables/variables.go b/variables/variables.go
new file mode 100644
index 00000000..37782c74
--- /dev/null
+++ b/variables/variables.go
@@ -0,0 +1,47 @@
+package variables
+
+import (
+ "errors"
+ "log"
+ "reflect"
+)
+
+// Dict allows to send more than one variable into a template.
+func Dict(values ...interface{}) (map[string]interface{}, error) {
+ if len(values)%2 != 0 {
+ return nil, errors.New("invalid dict call")
+ }
+ dict := make(map[string]interface{}, len(values)/2)
+ for i := 0; i < len(values); i += 2 {
+ key, ok := values[i].(string)
+ if !ok {
+ return nil, errors.New("dict keys must be strings")
+ }
+ dict[key] = values[i+1]
+ }
+
+ return dict, nil
+}
+
+// FieldInStruct checks if variable is defined in a struct.
+func FieldInStruct(data interface{}, field string) bool {
+ t := reflect.Indirect(reflect.ValueOf(data)).Type()
+
+ if t.Kind() != reflect.Struct {
+ log.Print("Non-struct type not allowed.")
+ return false
+ }
+
+ _, b := t.FieldByName(field)
+ return b
+}
+
+// StringInSlice checks if a slice contains a string.
+func StringInSlice(a string, list []string) (bool, int) {
+ for i, b := range list {
+ if b == a {
+ return true, i
+ }
+ }
+ return false, 0
+}
diff --git a/variables/variables_test.go b/variables/variables_test.go
new file mode 100644
index 00000000..95dcd5ab
--- /dev/null
+++ b/variables/variables_test.go
@@ -0,0 +1,41 @@
+package variables
+
+import "testing"
+
+type testFieldInStructData struct {
+ f1 string
+ f2 bool
+ f3 int
+ f4 func()
+}
+
+type testFieldInStruct struct {
+ data interface{}
+ field string
+ result bool
+}
+
+var testFieldInStructCases = []testFieldInStruct{
+ {testFieldInStructData{}, "f1", true},
+ {testFieldInStructData{}, "f2", true},
+ {testFieldInStructData{}, "f3", true},
+ {testFieldInStructData{}, "f4", true},
+ {testFieldInStructData{}, "f5", false},
+ {[]string{}, "", false},
+ {map[string]int{"oi": 4}, "", false},
+ {"asa", "", false},
+ {"int", "", false},
+}
+
+func TestFieldInStruct(t *testing.T) {
+ for _, pair := range testFieldInStructCases {
+ v := FieldInStruct(pair.data, pair.field)
+ if v != pair.result {
+ t.Error(
+ "For", pair.data,
+ "expected", pair.result,
+ "got", v,
+ )
+ }
+ }
+}