diff --git a/assets/embed/public/css/.jsbeautifyrc b/_embed/public/css/.jsbeautifyrc similarity index 100% rename from assets/embed/public/css/.jsbeautifyrc rename to _embed/public/css/.jsbeautifyrc diff --git a/assets/embed/public/css/styles.css b/_embed/public/css/styles.css similarity index 96% rename from assets/embed/public/css/styles.css rename to _embed/public/css/styles.css index 7fe9d964..511378e5 100644 --- a/assets/embed/public/css/styles.css +++ b/_embed/public/css/styles.css @@ -33,6 +33,10 @@ video { display: inline-block } +video { + max-width: 100%; +} + audio:not([controls]) { display: none; height: 0 @@ -260,7 +264,7 @@ textarea { body { font-family: 'Roboto', sans-serif; padding-top: 5em; - background-color: #fcfcfc; + background-color: #ffffff; text-rendering: optimizespeed; } @@ -469,6 +473,8 @@ header { z-index: 999; padding: 1.7em 0; background-color: #2196f3; + border-bottom: 1px solid rgba(0,0,0,0.075); + box-shadow: 0 0 5px rgba(0,0,0,0.1); } header h1 { @@ -477,7 +483,9 @@ header h1 { } header a, -header a:hover { +header a:hover, +#toolbar a, +#toolbar a:hover { color: inherit; } @@ -566,6 +574,7 @@ header p i { transition: .1s ease all; visibility: hidden; opacity: 0; + word-wrap: break-word; } #search.active div i, @@ -675,8 +684,7 @@ header .only-side { display: none; } -header #prev:hover+.prev-links, -header .prev-links:hover { +.action:hover ul { display: flex; } @@ -684,41 +692,53 @@ header .prev-links:hover { border-radius: 0; } -header .prev-links { +.action ul { position: absolute; - top: 0; + top: 3.1em; left: 0; color: #7d7d7d; list-style: none; margin: 0; padding: 0; background: #fff; - box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12); + box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15); border-radius: .2em; flex-direction: column-reverse; display: none; transition: .2s ease all; - min-width: 12em; + min-width: 3em; + z-index: 99999; } -header .prev-links a { - padding: .5em; +.action ul:before { + top: -16px; + left: 1em; + right: auto; + border: 8px solid transparent; + border-bottom-color: #ffffff; + content: ''; + position: absolute; +} + +.action ul a { + padding: .3em .5em; border-bottom: 1px solid #f5f5f5; transition: .2s ease all; + text-align: left; } -header .prev-links a:first-child { +.action ul a:first-child { border: 0; border-bottom-right-radius: .2em; border-bottom-left-radius: .2em; } -header .prev-links a:last-child { +.action ul a:last-child { border-top-right-radius: .2em; border-top-left-radius: .2em; } -header .prev-links a:hover { +.action ul a:hover { background-color: #f5f5f5; } @@ -774,7 +794,7 @@ header .action span { border: 0; box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24); padding: .5em; - width: 10em; + width: 22em; border-radius: .2em; } @@ -1166,4 +1186,10 @@ i.spin { column-count: 1; column-gap: 0; } -} \ No newline at end of file +} + +@media screen and (max-width: 450px) { + #toolbar p { + display: none; + } +} diff --git a/assets/embed/public/js/.jsbeautifyrc b/_embed/public/js/.jsbeautifyrc similarity index 100% rename from assets/embed/public/js/.jsbeautifyrc rename to _embed/public/js/.jsbeautifyrc diff --git a/assets/embed/public/js/application.js b/_embed/public/js/application.js similarity index 95% rename from assets/embed/public/js/application.js rename to _embed/public/js/application.js index 58145e82..4ba2ed56 100644 --- a/assets/embed/public/js/application.js +++ b/_embed/public/js/application.js @@ -1,5 +1,7 @@ 'use strict'; +// TODO: way to get the webdav url + var tempID = "_fm_internal_temporary_id" var selectedItems = []; var token = ""; @@ -87,7 +89,7 @@ Element.prototype.changeToDone = function(error, html) { } var toWebDavURL = function(url) { - url = url.replace("/", "/webdav/") + url = url.replace(baseURL + "/", webdavURL + "/"); return window.location.origin + url } @@ -149,22 +151,6 @@ var preventDefault = function(event) { event.preventDefault(); } -// Download file event -var downloadEvent = function(event) { - if (this.classList.contains('disabled')) { - return false; - } - if (selectedItems.length) { - Array.from(selectedItems).forEach(item => { - window.open(item + "?download=true"); - }); - return false; - } - - window.open(window.location + "?download=true"); - return false; -} - // Remove the last directory of an url var RemoveLastDirectoryPartOf = function(url) { var arr = url.split('/'); @@ -299,24 +285,20 @@ var renameEvent = function(event) { var handleFiles = function(files) { let button = document.getElementById("upload"); let html = button.changeToLoading(); - let data = new FormData(); for (let i = 0; i < files.length; i++) { - data.append(files[i].name, files[i]); - } + let request = new XMLHttpRequest(); + request.open('PUT', toWebDavURL(window.location.pathname + files[i].name)); + request.setRequestHeader('Token', token); + request.send(files[i]); + request.onreadystatechange = function() { + if (request.readyState == 4) { + if (request.status == 201) { + reloadListing(); + } - let request = new XMLHttpRequest(); - request.open('POST', window.location.pathname); - request.setRequestHeader("Upload", "true"); - request.setRequestHeader('Token', token); - request.send(data); - request.onreadystatechange = function() { - if (request.readyState == 4) { - if (request.status == 200) { - reloadListing(); + button.changeToDone((request.status != 201), html); } - - button.changeToDone((request.status != 200), html); } } @@ -417,6 +399,7 @@ var addNewDirEvents = function() { // Handles the new directory event var newDirEvent = function(event) { + // TODO: create new dir button and new file button if (event.keyCode == 27) { document.getElementById('newdir').classList.toggle('enabled'); setTimeout(() => { @@ -430,13 +413,14 @@ var newDirEvent = function(event) { let button = document.getElementById('new'); let html = button.changeToLoading(); let request = new XMLHttpRequest(); - request.open("POST", window.location); + let name = document.getElementById('newdir').value; + + request.open((name.endsWith("/") ? "MKCOL" : "PUT"), toWebDavURL(window.location.pathname + name)); request.setRequestHeader('Token', token); - request.setRequestHeader('Filename', document.getElementById('newdir').value); request.send(); request.onreadystatechange = function() { if (request.readyState == 4) { - button.changeToDone((request.status != 200), html); + button.changeToDone((request.status != 201), html); reloadListing(() => { addNewDirEvents(); }); @@ -466,6 +450,8 @@ document.addEventListener("changed-selected", function(event) { document.getElementById("rename").classList.remove("disabled"); } + redefineDownloadURLs(); + return false; } @@ -473,6 +459,22 @@ document.addEventListener("changed-selected", function(event) { return false; }); +var redefineDownloadURLs = function() { + let files = ""; + + for (let i = 0; i < selectedItems.length; i++) { + files += selectedItems[i].replace(window.location.pathname, "") + ","; + } + + files = files.substring(0, files.length - 1); + files = encodeURIComponent(files); + + let links = document.querySelectorAll("#download ul a"); + Array.from(links).forEach(link => { + link.href = "?download=" + link.dataset.format + "&files=" + files; + }); +} + var searchEvent = function(event) { let value = this.value; let box = document.querySelector('#search div'); @@ -832,13 +834,13 @@ document.addEventListener("editor", (event) => { let data = form2js(document.querySelector('form')); let html = button.changeToLoading(); let request = new XMLHttpRequest(); - request.open("PUT", window.location); + request.open("PUT", toWebDavURL(window.location.pathname)); request.setRequestHeader('Kind', kind); request.setRequestHeader('Token', token); request.send(JSON.stringify(data)); request.onreadystatechange = function() { if (request.readyState == 4) { - button.changeToDone((request.status != 200), html); + button.changeToDone((request.status != 201), html); } } } @@ -891,7 +893,6 @@ document.addEventListener("DOMContentLoaded", function(event) { document.getElementById("delete").addEventListener("click", deleteEvent); } - document.getElementById("download").addEventListener("click", downloadEvent); document.getElementById("open-nav").addEventListener("click", event => { document.querySelector("header > div:nth-child(2)").classList.toggle("active"); }); diff --git a/assets/embed/public/js/form2js.js b/_embed/public/js/form2js.js similarity index 100% rename from assets/embed/public/js/form2js.js rename to _embed/public/js/form2js.js diff --git a/assets/embed/templates/.jsbeautifyrc b/_embed/templates/.jsbeautifyrc similarity index 100% rename from assets/embed/templates/.jsbeautifyrc rename to _embed/templates/.jsbeautifyrc diff --git a/_embed/templates/actions.tmpl b/_embed/templates/actions.tmpl new file mode 100644 index 00000000..a42d11d1 --- /dev/null +++ b/_embed/templates/actions.tmpl @@ -0,0 +1,28 @@ +{{ define "actions" }} +
+ open_in_new See raw +
+{{ if and .IsDir .User.AllowEdit }} +
+ mode_edit +
+{{ end }} +
+ + file_download Download + + {{ if .IsDir }} + + {{ end }} +
+{{ if .User.AllowEdit }} +
+ delete Delete +
+{{ end }} +{{ end }} diff --git a/_embed/templates/base.tmpl b/_embed/templates/base.tmpl new file mode 100644 index 00000000..679795e7 --- /dev/null +++ b/_embed/templates/base.tmpl @@ -0,0 +1,126 @@ + + +{{ $absURL := .Config.AbsoluteURL }} + + {{.Name}} + + + + + {{ if ne .User.StyleSheet "" }} + + {{ end }} + + +
+
+ {{ $lnk := .PreviousLink }} + + +
+ menu +
+ + {{ if ne .Name "/"}}

{{ .Name }}

{{ end }} +
+ +
+
+ {{ $lnk := .PreviousLink }} + {{ if ne $lnk ""}}{{ end }} + + {{ if ne $lnk ""}}{{ end }} + +

File Manager

+
+ + {{ if .IsDir}} + {{ if .User.AllowCommands }} + + {{ end }} + +
+ view_headline Switch view +
+ + {{ if .User.AllowNew }} +
+ file_upload Upload +
+ {{ end }} + + + {{ else }} + {{ template "actions" . }} + {{ end }} + +
+ exit_to_app Logout +
+
+
+
+ + {{ if .IsDir }} +
+
+
+ arrow_back +
+

+ 0 + selected.

+
+
+ {{ template "actions" . }} +
+
+ {{ end }} + +
+ {{ template "content" . }} + {{ .Config.Token }} +
+ + + + + + + + + + {{ if .Config.HugoEnabled }}{{ end }} + + diff --git a/assets/embed/templates/editor.tmpl b/_embed/templates/editor.tmpl similarity index 100% rename from assets/embed/templates/editor.tmpl rename to _embed/templates/editor.tmpl diff --git a/assets/embed/templates/frontmatter.tmpl b/_embed/templates/frontmatter.tmpl similarity index 100% rename from assets/embed/templates/frontmatter.tmpl rename to _embed/templates/frontmatter.tmpl diff --git a/assets/embed/templates/listing.tmpl b/_embed/templates/listing.tmpl similarity index 94% rename from assets/embed/templates/listing.tmpl rename to _embed/templates/listing.tmpl index 70f443d7..c03f5f34 100644 --- a/assets/embed/templates/listing.tmpl +++ b/_embed/templates/listing.tmpl @@ -38,7 +38,7 @@ {{ end }} {{ if .User.AllowNew }} - +
add diff --git a/assets/embed/templates/minimal.tmpl b/_embed/templates/minimal.tmpl similarity index 100% rename from assets/embed/templates/minimal.tmpl rename to _embed/templates/minimal.tmpl diff --git a/_embed/templates/single.tmpl b/_embed/templates/single.tmpl new file mode 100644 index 00000000..e070dcd8 --- /dev/null +++ b/_embed/templates/single.tmpl @@ -0,0 +1,21 @@ +{{ define "content" }} +{{ with .Data}} +
+ {{ if eq .Type "image" }} + + {{ else if eq .Type "audio" }} + + {{ else if eq .Type "video" }} + + {{ else if eq .Type "blob" }} + Download + {{ else}} +
{{ .StringifyContent }}
+ {{ end }} +
+{{ end }} +{{ end }} diff --git a/assets/embed/templates/actions.tmpl b/assets/embed/templates/actions.tmpl deleted file mode 100644 index 069598f2..00000000 --- a/assets/embed/templates/actions.tmpl +++ /dev/null @@ -1,18 +0,0 @@ -{{ define "actions" }} -
- open_in_new See raw -
- {{ if and .IsDir .User.AllowEdit }} -
- mode_edit -
- {{ end }} -
- file_download Download -
- {{ if .User.AllowEdit }} -
- delete Delete -
- {{ end }} -{{ end }} diff --git a/assets/embed/templates/base.tmpl b/assets/embed/templates/base.tmpl deleted file mode 100644 index 7acb1bab..00000000 --- a/assets/embed/templates/base.tmpl +++ /dev/null @@ -1,139 +0,0 @@ -{{ $absURL := .Config.AbsoluteURL }} - - - - - {{.Name}} - - - - - - {{ if ne .User.StyleSheet "" }} - - {{ end }} - - -
-
- {{ $lnk := .PreviousLink }} - - - - {{ if ne $lnk ""}} - - {{ end }} - -
- menu -
- -

- {{ if ne .Name "/"}} - {{ .Name }} -

- {{ end }} -
- -
-
- {{ $lnk := .PreviousLink }} - {{ if ne $lnk ""}} - - {{ end }} - - - {{ if ne $lnk ""}} - - {{ end }} - -

- - File Manager - -

-
- - {{ if .IsDir}} - - {{ if .User.AllowCommands }} - - {{ end }} - -
- view_headline Switch view -
- {{ if .User.AllowNew }} -
- file_upload Upload -
- {{ end }} - {{ else }} - {{ template "actions" . }} - {{ end }} - -
- exit_to_app Logout -
-
-
-
- - {{ if .IsDir }} -
-
-
- arrow_back -
-

- 0 - selected.

-
-
- {{ template "actions" . }} -
-
- {{ end }} - -
- {{ template "content" . }} - {{ .Config.Token }} -
- - - - - - - - - - {{ if .Config.HugoEnabled }}{{ end }} - - diff --git a/assets/embed/templates/single.tmpl b/assets/embed/templates/single.tmpl deleted file mode 100644 index bd8f077d..00000000 --- a/assets/embed/templates/single.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -{{ define "content" }} -{{ with .Data}} -
- {{ if eq .Type "image" }} - - {{ else if eq .Type "audio" }} - - {{ else if eq .Type "video" }} - - {{ else}} -
{{ .Content }}
- {{ 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: