From 10f7ae1d0d0c7f694be098bfeb91a1d42348baf3 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 18 Oct 2016 15:55:30 +0100 Subject: [PATCH 01/28] new directory using webdav --- assets/embed/public/js/application.js | 5 ++-- filemanager.go | 37 +-------------------------- 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/assets/embed/public/js/application.js b/assets/embed/public/js/application.js index 58145e82..30246b62 100644 --- a/assets/embed/public/js/application.js +++ b/assets/embed/public/js/application.js @@ -430,13 +430,12 @@ var newDirEvent = function(event) { let button = document.getElementById('new'); let html = button.changeToLoading(); let request = new XMLHttpRequest(); - request.open("POST", window.location); + request.open("MKCOL", toWebDavURL(window.location.pathname + document.getElementById('newdir').value + "/")); 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(); }); diff --git a/filemanager.go b/filemanager.go index 97811360..6ed614c5 100644 --- a/filemanager.go +++ b/filemanager.go @@ -10,7 +10,6 @@ package filemanager import ( e "errors" "io" - "io/ioutil" "log" "mime/multipart" "net/http" @@ -179,8 +178,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err return command(w, r, c, user) } - // Creates a new folder. - return newDirectory(w, r, c) + fallthrough default: return http.StatusNotImplemented, nil } @@ -234,39 +232,6 @@ func upload(w http.ResponseWriter, r *http.Request, c *config.Config) (int, erro 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"), " ") From 59f5109617d318b77750a11525023c8439e5299d Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 18 Oct 2016 16:17:01 +0100 Subject: [PATCH 02/28] save file using webdav --- assets/embed/public/js/application.js | 5 ++- directory/file.go | 2 + directory/update.go | 63 +++++++++++++++------------ filemanager.go | 20 ++++----- 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/assets/embed/public/js/application.js b/assets/embed/public/js/application.js index 30246b62..e326f9b4 100644 --- a/assets/embed/public/js/application.js +++ b/assets/embed/public/js/application.js @@ -417,6 +417,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(() => { @@ -831,13 +832,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); } } } diff --git a/directory/file.go b/directory/file.go index a68936f1..240c809a 100644 --- a/directory/file.go +++ b/directory/file.go @@ -144,6 +144,8 @@ func (i *Info) serveSingleFile(w http.ResponseWriter, r *http.Request, c *config } page.Info.Data = editor + + // TODO: if serve Single File finds an error while parsing, show the raw content to edit instead of giving 500 return page.PrintAsHTML(w, "frontmatter", "editor") } diff --git a/directory/update.go b/directory/update.go index 7a0f5c3a..34d1ba15 100644 --- a/directory/update.go +++ b/directory/update.go @@ -16,48 +16,53 @@ import ( // 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) { + // TODO: review this + 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 { + rawBuffer := new(bytes.Buffer) + rawBuffer.ReadFrom(r.Body) + + if kind == "" { + file = rawBuffer.Bytes() + } else { + err := json.Unmarshal(rawBuffer.Bytes(), &data) + + if 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 + + 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 } - default: - return http.StatusBadRequest, nil } + // Overwrite the Body + r.Body = ioutil.NopCloser(bytes.NewReader(file)) + // Write the file - err = ioutil.WriteFile(i.Path, file, 0666) + // err = ioutil.WriteFile(i.Path, file, 0666) - if err != nil { - return http.StatusInternalServerError, err - } + //if err != nil { + //return http.StatusInternalServerError, err + // } return code, nil } diff --git a/filemanager.go b/filemanager.go index 6ed614c5..3bf02019 100644 --- a/filemanager.go +++ b/filemanager.go @@ -57,6 +57,13 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err } if c.WebDav && strings.HasPrefix(r.URL.Path, c.WebDavURL) { + if r.Method == http.MethodPut { + _, err = fi.Update(w, r, c, user) + if err != nil { + return http.StatusInternalServerError, err + } + } + //url := strings.TrimPrefix(r.URL.Path, c.WebDavURL) /* @@ -143,17 +150,6 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err return errors.PrintHTML(w, code, err) } return code, err - case http.MethodPut: - if fi.IsDir { - return http.StatusNotAcceptable, nil - } - - 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" { @@ -166,7 +162,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err // Search and git commands. if r.Header.Get("Search") == "true" { - // TODO: search commands. + // TODO: search commands. USE PROPFIND? } // VCS commands. From 3683c4c06a1a0b733f02644d419a095fa2c5c136 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 18 Oct 2016 16:42:48 +0100 Subject: [PATCH 03/28] Always using webdav --- assets/embed/public/css/styles.css | 4 +- assets/embed/public/js/application.js | 4 +- assets/embed/templates/listing.tmpl | 2 +- config/config.go | 22 +++++------ filemanager.go | 56 ++++++++++++++------------- 5 files changed, 46 insertions(+), 42 deletions(-) diff --git a/assets/embed/public/css/styles.css b/assets/embed/public/css/styles.css index 7fe9d964..bb21074c 100644 --- a/assets/embed/public/css/styles.css +++ b/assets/embed/public/css/styles.css @@ -774,7 +774,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 +1166,4 @@ i.spin { column-count: 1; column-gap: 0; } -} \ No newline at end of file +} diff --git a/assets/embed/public/js/application.js b/assets/embed/public/js/application.js index e326f9b4..ee333a21 100644 --- a/assets/embed/public/js/application.js +++ b/assets/embed/public/js/application.js @@ -431,7 +431,9 @@ var newDirEvent = function(event) { let button = document.getElementById('new'); let html = button.changeToLoading(); let request = new XMLHttpRequest(); - request.open("MKCOL", toWebDavURL(window.location.pathname + document.getElementById('newdir').value + "/")); + let name = document.getElementById('newdir').value; + + request.open((name.endsWith("/") ? "MKCOL" : "PUT"), toWebDavURL(window.location.pathname + name)); request.setRequestHeader('Token', token); request.send(); request.onreadystatechange = function() { diff --git a/assets/embed/templates/listing.tmpl b/assets/embed/templates/listing.tmpl index 70f443d7..c03f5f34 100644 --- a/assets/embed/templates/listing.tmpl +++ b/assets/embed/templates/listing.tmpl @@ -38,7 +38,7 @@ {{ end }} {{ if .User.AllowNew }} - +
add diff --git a/config/config.go b/config/config.go index c5f0145e..952cb134 100644 --- a/config/config.go +++ b/config/config.go @@ -69,6 +69,7 @@ func Parse(c *caddy.Controller) ([]Config, error) { cfg.AllowEdit = true cfg.AllowNew = true cfg.Commands = []string{"git", "svn", "hg"} + cfg.WebDav = true cfg.Rules = []*Rule{&Rule{ Regex: true, Allow: false, @@ -85,6 +86,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,23 +107,15 @@ 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() @@ -240,6 +234,12 @@ func Parse(c *caddy.Controller) ([]Config, error) { } } + cfg.WebDavHandler = &webdav.Handler{ + Prefix: cfg.WebDavURL, + FileSystem: webdav.Dir(cfg.PathScope), + 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/filemanager.go b/filemanager.go index 3bf02019..2c0c5009 100644 --- a/filemanager.go +++ b/filemanager.go @@ -56,7 +56,24 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err user = c.User } - if c.WebDav && strings.HasPrefix(r.URL.Path, c.WebDavURL) { + if strings.HasPrefix(r.URL.Path, c.WebDavURL) { + url := strings.TrimPrefix(r.URL.Path, c.WebDavURL) + + if !user.Allowed(url) { + return http.StatusForbidden, nil + } + + 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 + } + } + if r.Method == http.MethodPut { _, err = fi.Update(w, r, c, user) if err != nil { @@ -64,24 +81,6 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err } } - //url := strings.TrimPrefix(r.URL.Path, c.WebDavURL) - - /* - if !user.Allowed(url) { - return http.StatusForbidden, nil - } - - 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 } @@ -122,9 +121,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err } } - // Route the request depending on the HTTP Method. - switch r.Method { - case http.MethodGet: + if r.Method == http.MethodGet { // Read and show directory or file. if serveAssets { return assets.Serve(w, r, c) @@ -136,13 +133,18 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err if !fi.IsDir { query := r.URL.Query() if val, ok := query["raw"]; ok && val[0] == "true" { + // TODO: change URL to webdav and continue as webdav return fi.ServeRawFile(w, r, c) } if val, ok := query["download"]; ok && val[0] == "true" { w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name) + // TODO: change URL to webdav and continue as webdav return fi.ServeRawFile(w, r, c) + } + + // c.WebDavHandler.ServeHTTP(w, r) } code, err := fi.ServeAsHTML(w, r, c, user) @@ -150,7 +152,9 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err return errors.PrintHTML(w, code, err) } return code, err - case http.MethodPost: + } + + if r.Method == http.MethodPost { // Upload a new file. if r.Header.Get("Upload") == "true" { if !user.AllowNew { @@ -173,11 +177,9 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err return command(w, r, c, user) } - - fallthrough - default: - return http.StatusNotImplemented, nil } + + return http.StatusNotImplemented, nil } } From ccc539c592d77e086a3859f0829521b87f5b916f Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 18 Oct 2016 17:00:12 +0100 Subject: [PATCH 04/28] simplify router --- directory/file.go | 19 ++------------- filemanager.go | 61 +++++++++++++++++++---------------------------- 2 files changed, 27 insertions(+), 53 deletions(-) diff --git a/directory/file.go b/directory/file.go index 240c809a..e18b83d6 100644 --- a/directory/file.go +++ b/directory/file.go @@ -122,7 +122,8 @@ func (i *Info) serveSingleFile(w http.ResponseWriter, r *http.Request, c *config } if i.Type == "blob" { - return i.ServeRawFile(w, r, c) + http.Redirect(w, r, c.AddrPath+r.URL.Path+"?download=true", http.StatusTemporaryRedirect) + return 0, nil } page := &p.Page{ @@ -274,22 +275,6 @@ func directoryListing(files []os.FileInfo, urlPath string, basePath string, u *c } } -// 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") { diff --git a/filemanager.go b/filemanager.go index 2c0c5009..c4f8364a 100644 --- a/filemanager.go +++ b/filemanager.go @@ -56,13 +56,24 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err user = c.User } - if strings.HasPrefix(r.URL.Path, c.WebDavURL) { - url := strings.TrimPrefix(r.URL.Path, c.WebDavURL) - - if !user.Allowed(url) { - return http.StatusForbidden, nil + // TODO: make allow and block rules relative to baseurl and webdav + // 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.")) } + return http.StatusForbidden, nil + } + + // Security measures against CSRF attacks. + if r.Method != http.MethodGet { + if !c.CheckToken(r) { + return http.StatusForbidden, nil + } + } + + if strings.HasPrefix(r.URL.Path, c.WebDavURL) { switch r.Method { case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE": if !user.AllowEdit { @@ -85,18 +96,11 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err 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.")) - } - - return http.StatusForbidden, nil + if r.Method == http.MethodGet && serveAssets { + return assets.Serve(w, r, c) } - // If this request is neither to server assets, nor to upload/create - // a new file or directory. - if r.Method != http.MethodPost && !serveAssets { + if r.Method == http.MethodGet { // Gets the information of the directory/file fi, code, err = directory.GetInfo(r.URL, c, user) if err != nil { @@ -112,20 +116,6 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err 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 - } - } - - if r.Method == http.MethodGet { - // Read and show directory or file. - if serveAssets { - return assets.Serve(w, r, c) - } // Generate anti security token. c.GenerateToken() @@ -133,18 +123,17 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err if !fi.IsDir { query := r.URL.Query() if val, ok := query["raw"]; ok && val[0] == "true" { - // TODO: change URL to webdav and continue as webdav - return fi.ServeRawFile(w, r, c) + r.URL.Path = strings.Replace(r.URL.Path, c.BaseURL, c.WebDavURL, 1) + c.WebDavHandler.ServeHTTP(w, r) + return 0, nil } if val, ok := query["download"]; ok && val[0] == "true" { w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name) - // TODO: change URL to webdav and continue as webdav - return fi.ServeRawFile(w, r, c) - + r.URL.Path = strings.Replace(r.URL.Path, c.BaseURL, c.WebDavURL, 1) + c.WebDavHandler.ServeHTTP(w, r) + return 0, nil } - - // c.WebDavHandler.ServeHTTP(w, r) } code, err := fi.ServeAsHTML(w, r, c, user) From 0a755ec954626b8581b9dd21597b7acdc0be2381 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 18 Oct 2016 17:24:54 +0100 Subject: [PATCH 05/28] upload is now webdav --- assets/embed/public/js/application.js | 26 +++++----- filemanager.go | 71 ++++----------------------- 2 files changed, 21 insertions(+), 76 deletions(-) diff --git a/assets/embed/public/js/application.js b/assets/embed/public/js/application.js index ee333a21..8dbe38b3 100644 --- a/assets/embed/public/js/application.js +++ b/assets/embed/public/js/application.js @@ -299,27 +299,25 @@ 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); } } + + return false; } diff --git a/filemanager.go b/filemanager.go index c4f8364a..f1dfe856 100644 --- a/filemanager.go +++ b/filemanager.go @@ -8,12 +8,8 @@ package filemanager import ( - e "errors" - "io" - "log" - "mime/multipart" + "fmt" "net/http" - "os" "os/exec" "path/filepath" "strings" @@ -58,7 +54,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err // TODO: make allow and block rules relative to baseurl and webdav // Checks if the user has permission to access the current directory. - if !user.Allowed(r.URL.Path) { + /*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.")) } @@ -66,14 +62,17 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err return http.StatusForbidden, nil } + // TODO: How to exclude web dav clients? :/ // Security measures against CSRF attacks. if r.Method != http.MethodGet { if !c.CheckToken(r) { return http.StatusForbidden, nil } - } + } */ if strings.HasPrefix(r.URL.Path, c.WebDavURL) { + fmt.Println("e") + switch r.Method { case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE": if !user.AllowEdit { @@ -144,19 +143,11 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err } if r.Method == http.MethodPost { - // Upload a new file. - if r.Header.Get("Upload") == "true" { - if !user.AllowNew { - return http.StatusUnauthorized, nil - } - - return upload(w, r, c) - } - + /* TODO: search commands. USE PROPFIND? // Search and git commands. if r.Header.Get("Search") == "true" { - // TODO: search commands. USE PROPFIND? - } + + } */ // VCS commands. if r.Header.Get("Command") != "" { @@ -175,50 +166,6 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err 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 -} - // 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"), " ") From 22e0ad0831ab92c513968947f4b02af19ba18b8a Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 18 Oct 2016 17:56:35 +0100 Subject: [PATCH 06/28] update --- assets/embed/public/js/application.js | 4 +- directory/editor.go | 37 ++++++++------- directory/file.go | 2 - directory/update.go | 67 ++++++++++++--------------- filemanager.go | 57 ++++++++++++----------- 5 files changed, 81 insertions(+), 86 deletions(-) diff --git a/assets/embed/public/js/application.js b/assets/embed/public/js/application.js index 8dbe38b3..a4e2b86f 100644 --- a/assets/embed/public/js/application.js +++ b/assets/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 = ""; @@ -316,8 +318,6 @@ var handleFiles = function(files) { } } - - return false; } diff --git a/directory/editor.go b/directory/editor.go index 7ccb4ddd..b6b43371 100644 --- a/directory/editor.go +++ b/directory/editor.go @@ -42,23 +42,26 @@ func (i *Info) GetEditor() (*Editor, 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 + if !HasFrontMatterRune(i.Raw) { editor.Class = "content-only" editor.Content = i.Content + break } + + // Starts a new buffer and parses the file using Hugo's functions + buffer := bytes.NewBuffer(i.Raw) + page, err = parser.ReadFrom(buffer) + editor.Class = "complete" + + if err != nil { + editor.Class = "content-only" + editor.Content = i.Content + break + } + + // Parses the page content and the frontmatter + editor.Content = strings.TrimSpace(string(page.Content())) + editor.FrontMatter, _, err = frontmatter.Pretty(page.FrontMatter()) case "json", "toml", "yaml": // Defines the class and declares an error editor.Class = "frontmatter-only" @@ -72,13 +75,15 @@ func (i *Info) GetEditor() (*Editor, error) { // Check if there were any errors if err != nil { - return editor, err + editor.Class = "content-only" + editor.Content = i.Content + break } default: - // The editor will handle only content editor.Class = "content-only" editor.Content = i.Content } + return editor, nil } diff --git a/directory/file.go b/directory/file.go index e18b83d6..300691da 100644 --- a/directory/file.go +++ b/directory/file.go @@ -145,8 +145,6 @@ func (i *Info) serveSingleFile(w http.ResponseWriter, r *http.Request, c *config } page.Info.Data = editor - - // TODO: if serve Single File finds an error while parsing, show the raw content to edit instead of giving 500 return page.PrintAsHTML(w, "frontmatter", "editor") } diff --git a/directory/update.go b/directory/update.go index 34d1ba15..10483769 100644 --- a/directory/update.go +++ b/directory/update.go @@ -16,54 +16,45 @@ import ( // 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) { - // TODO: review this + var ( + data map[string]interface{} + file []byte + code int + err error + kind string + rawBuffer = new(bytes.Buffer) + ) - var data map[string]interface{} - kind := r.Header.Get("kind") - - var file []byte - var code int - - rawBuffer := new(bytes.Buffer) + kind = r.Header.Get("kind") rawBuffer.ReadFrom(r.Body) - if kind == "" { - file = rawBuffer.Bytes() - } else { - err := json.Unmarshal(rawBuffer.Bytes(), &data) + if kind != "" { + err = json.Unmarshal(rawBuffer.Bytes(), &data) if err != nil { return http.StatusInternalServerError, err } - - 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 - } } - // Overwrite the Body + 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: + file = rawBuffer.Bytes() + } + + // Overwrite the request Body r.Body = ioutil.NopCloser(bytes.NewReader(file)) - - // Write the file - // err = ioutil.WriteFile(i.Path, file, 0666) - - //if err != nil { - //return http.StatusInternalServerError, err - // } - return code, nil } diff --git a/filemanager.go b/filemanager.go index f1dfe856..3efc3cd6 100644 --- a/filemanager.go +++ b/filemanager.go @@ -8,7 +8,7 @@ package filemanager import ( - "fmt" + e "errors" "net/http" "os/exec" "path/filepath" @@ -32,18 +32,21 @@ 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 *directory.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) + + if r.Method == http.MethodGet && httpserver.Path(r.URL.Path).Matches(c.BaseURL+assets.BaseURL) { + return assets.Serve(w, r, c) + } + username, _, _ := r.BasicAuth() if _, ok := c.Users[username]; ok { @@ -52,26 +55,10 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err user = c.User } - // TODO: make allow and block rules relative to baseurl and webdav - // 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.")) - } - - return http.StatusForbidden, nil - } - - // TODO: How to exclude web dav clients? :/ - // Security measures against CSRF attacks. - if r.Method != http.MethodGet { - if !c.CheckToken(r) { + if strings.HasPrefix(r.URL.Path, c.WebDavURL) { + if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.WebDavURL)) { return http.StatusForbidden, nil } - } */ - - if strings.HasPrefix(r.URL.Path, c.WebDavURL) { - fmt.Println("e") switch r.Method { case "PROPPATCH", "MOVE", "PATCH", "PUT", "DELETE": @@ -95,8 +82,16 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err return 0, nil } - if r.Method == http.MethodGet && serveAssets { - return assets.Serve(w, r, c) + if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.BaseURL)) { + if r.Method == http.MethodGet { + return errors.PrintHTML( + w, + http.StatusForbidden, + e.New("You don't have permission to access this page."), + ) + } + + return http.StatusForbidden, nil } if r.Method == http.MethodGet { @@ -143,6 +138,12 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err } if r.Method == http.MethodPost { + // TODO: How to exclude web dav clients? :/ + // Security measures against CSRF attacks. + if !c.CheckToken(r) { + return http.StatusForbidden, nil + } + /* TODO: search commands. USE PROPFIND? // Search and git commands. if r.Header.Get("Search") == "true" { From 06c1a412a6418a8cc352559ed550325ed24b0550 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 18 Oct 2016 21:06:31 +0100 Subject: [PATCH 07/28] update --- assets/embed/templates/single.tmpl | 2 +- config/config.go | 42 ++-- config/user.go | 22 ++- directory/file.go | 299 ----------------------------- directory/editor.go => editor.go | 36 ++-- filemanager.go | 26 ++- info.go | 165 ++++++++++++++++ directory/listing.go => listing.go | 143 +++++++++++++- page/page.go => page.go | 24 +-- directory/update.go => preput.go | 24 +-- 10 files changed, 385 insertions(+), 398 deletions(-) delete mode 100644 directory/file.go rename directory/editor.go => editor.go (72%) create mode 100644 info.go rename directory/listing.go => listing.go (52%) rename page/page.go => page.go (85%) rename directory/update.go => preput.go (77%) diff --git a/assets/embed/templates/single.tmpl b/assets/embed/templates/single.tmpl index bd8f077d..d70d1646 100644 --- a/assets/embed/templates/single.tmpl +++ b/assets/embed/templates/single.tmpl @@ -8,7 +8,7 @@ {{ else if eq .Type "video" }} {{ else}} -
{{ .Content }}
+
{{ .StringifyContent }}
{{ end }} {{ end }} diff --git a/config/config.go b/config/config.go index 952cb134..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 @@ -69,7 +66,6 @@ func Parse(c *caddy.Controller) ([]Config, error) { cfg.AllowEdit = true cfg.AllowNew = true cfg.Commands = []string{"git", "svn", "hg"} - cfg.WebDav = true cfg.Rules = []*Rule{&Rule{ Regex: true, Allow: false, @@ -121,9 +117,9 @@ func Parse(c *caddy.Controller) ([]Config, error) { 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() @@ -227,16 +223,16 @@ 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.WebDavHandler = &webdav.Handler{ + cfg.Handler = &webdav.Handler{ Prefix: cfg.WebDavURL, - FileSystem: webdav.Dir(cfg.PathScope), + FileSystem: cfg.FileSystem, LockSystem: webdav.NewMemLS(), } 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/file.go b/directory/file.go deleted file mode 100644 index 300691da..00000000 --- a/directory/file.go +++ /dev/null @@ -1,299 +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" { - http.Redirect(w, r, c.AddrPath+r.URL.Path+"?download=true", http.StatusTemporaryRedirect) - return 0, nil - } - - 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, - } -} - -// 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/editor.go b/editor.go similarity index 72% rename from directory/editor.go rename to editor.go index b6b43371..d26efe0d 100644 --- a/directory/editor.go +++ b/editor.go @@ -1,4 +1,4 @@ -package directory +package filemanager import ( "bytes" @@ -18,10 +18,10 @@ type Editor struct { } // GetEditor gets the editor based on a FileInfo struct -func (i *Info) GetEditor() (*Editor, error) { +func (i *FileInfo) GetEditor() (*Editor, error) { // Create a new editor variable and set the mode editor := new(Editor) - editor.Mode = strings.TrimPrefix(filepath.Ext(i.Name), ".") + editor.Mode = strings.TrimPrefix(filepath.Ext(i.Name()), ".") switch editor.Mode { case "md", "markdown", "mdown", "mmark": @@ -42,20 +42,20 @@ func (i *Info) GetEditor() (*Editor, error) { // Handle the content depending on the file extension switch editor.Mode { case "markdown", "asciidoc", "rst": - if !HasFrontMatterRune(i.Raw) { + if !hasFrontMatterRune(i.Content) { editor.Class = "content-only" - editor.Content = i.Content + editor.Content = i.StringifyContent() break } // Starts a new buffer and parses the file using Hugo's functions - buffer := bytes.NewBuffer(i.Raw) + buffer := bytes.NewBuffer(i.Content) page, err = parser.ReadFrom(buffer) editor.Class = "complete" if err != nil { editor.Class = "content-only" - editor.Content = i.Content + editor.Content = i.StringifyContent() break } @@ -67,35 +67,35 @@ func (i *Info) GetEditor() (*Editor, 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) + if hasFrontMatterRune(i.Content) { + editor.FrontMatter, _, err = frontmatter.Pretty(i.Content) } else { - editor.FrontMatter, _, err = frontmatter.Pretty(AppendFrontMatterRune(i.Raw, editor.Mode)) + editor.FrontMatter, _, err = frontmatter.Pretty(appendFrontMatterRune(i.Content, editor.Mode)) } // Check if there were any errors if err != nil { editor.Class = "content-only" - editor.Content = i.Content + editor.Content = i.StringifyContent() break } default: editor.Class = "content-only" - editor.Content = i.Content + editor.Content = i.StringifyContent() } return editor, nil } -// HasFrontMatterRune checks if the file has the frontmatter rune -func HasFrontMatterRune(file []byte) bool { +// 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 { +// 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---") @@ -108,8 +108,8 @@ func AppendFrontMatterRune(frontmatter []byte, language string) []byte { return frontmatter } -// CanBeEdited checks if the extension of a file is supported by the editor -func CanBeEdited(filename string) bool { +// 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", diff --git a/filemanager.go b/filemanager.go index 3efc3cd6..bc8f1e18 100644 --- a/filemanager.go +++ b/filemanager.go @@ -16,9 +16,7 @@ import ( "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/page" "github.com/mholt/caddy/caddyhttp/httpserver" ) @@ -33,7 +31,7 @@ type FileManager struct { func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { var ( c *config.Config - fi *directory.Info + fi *FileInfo code int err error user *config.User @@ -78,7 +76,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err } } - c.WebDavHandler.ServeHTTP(w, r) + c.Handler.ServeHTTP(w, r) return 0, nil } @@ -96,7 +94,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err if r.Method == http.MethodGet { // Gets the information of the directory/file - fi, code, err = directory.GetInfo(r.URL, c, user) + fi, code, err = GetInfo(r.URL, c, user) if err != nil { if r.Method == http.MethodGet { return errors.PrintHTML(w, code, err) @@ -106,7 +104,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, 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, "/") { + if fi.IsDir() && !strings.HasSuffix(r.URL.Path, "/") { http.Redirect(w, r, c.AddrPath+r.URL.Path+"/", http.StatusTemporaryRedirect) return 0, nil } @@ -114,23 +112,23 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err // Generate anti security token. c.GenerateToken() - if !fi.IsDir { + if !fi.IsDir() { query := r.URL.Query() if val, ok := query["raw"]; ok && val[0] == "true" { r.URL.Path = strings.Replace(r.URL.Path, c.BaseURL, c.WebDavURL, 1) - c.WebDavHandler.ServeHTTP(w, r) + c.Handler.ServeHTTP(w, r) return 0, nil } if val, ok := query["download"]; ok && val[0] == "true" { - w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name) + w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name()) r.URL.Path = strings.Replace(r.URL.Path, c.BaseURL, c.WebDavURL, 1) - c.WebDavHandler.ServeHTTP(w, r) + c.Handler.ServeHTTP(w, r) return 0, nil } } - code, err := fi.ServeAsHTML(w, r, c, user) + code, err := fi.ServeHTTP(w, r, c, user) if err != nil { return errors.PrintHTML(w, code, err) } @@ -189,7 +187,7 @@ func command(w http.ResponseWriter, r *http.Request, c *config.Config, u *config return http.StatusNotImplemented, nil } - path := strings.Replace(r.URL.Path, c.BaseURL, c.PathScope, 1) + path := strings.Replace(r.URL.Path, c.BaseURL, c.Scope, 1) path = filepath.Clean(path) cmd := exec.Command(command[0], command[1:len(command)]...) @@ -200,6 +198,6 @@ func command(w http.ResponseWriter, r *http.Request, c *config.Config, u *config return http.StatusInternalServerError, err } - page := &page.Page{Info: &page.Info{Data: string(output)}} - return page.PrintAsJSON(w) + p := &page{pageInfo: &pageInfo{Data: string(output)}} + return p.PrintAsJSON(w) } diff --git a/info.go b/info.go new file mode 100644 index 00000000..ed6da747 --- /dev/null +++ b/info.go @@ -0,0 +1,165 @@ +package filemanager + +import ( + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + + humanize "github.com/dustin/go-humanize" + "github.com/hacdias/caddy-filemanager/config" +) + +// FileInfo contains the information about a particular file or directory +type FileInfo 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) (*FileInfo, int, error) { + var err error + + i := &FileInfo{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 { + code := http.StatusInternalServerError + + switch { + case os.IsPermission(err): + code = http.StatusForbidden + case os.IsNotExist(err): + code = http.StatusGone + case os.IsExist(err): + code = http.StatusGone + } + + return i, code, err + } + + return i, 0, nil +} + +func (i *FileInfo) Read() error { + var err error + i.Content, err = ioutil.ReadFile(i.Path) + if err != nil { + return err + } + i.Mimetype = http.DetectContentType(i.Content) + i.Type = SimplifyMimeType(i.Mimetype) + return nil +} + +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) +} + +func (i *FileInfo) ServeHTTP(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 *FileInfo) serveSingleFile(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) { + err := i.Read() + if err != nil { + code := http.StatusInternalServerError + + switch { + case os.IsPermission(err): + code = http.StatusForbidden + case os.IsNotExist(err): + code = http.StatusGone + case os.IsExist(err): + code = http.StatusGone + } + + return code, err + } + + if i.Type == "blob" { + http.Redirect( + w, r, + c.AddrPath+r.URL.Path+"?download=true", + http.StatusTemporaryRedirect, + ) + return 0, nil + } + + p := &page{ + pageInfo: &pageInfo{ + Name: i.Name(), + Path: i.VirtualPath, + IsDir: false, + Data: i, + User: u, + Config: c, + }, + } + + if (canBeEdited(i.Name()) || i.Type == "text") && u.AllowEdit { + p.Data, err = i.GetEditor() + if err != nil { + return http.StatusInternalServerError, err + } + + return p.PrintAsHTML(w, "frontmatter", "editor") + } + + return p.PrintAsHTML(w, "single") +} + +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/listing.go similarity index 52% rename from directory/listing.go rename to listing.go index 0aa87fa8..78dc6b47 100644 --- a/directory/listing.go +++ b/listing.go @@ -1,11 +1,18 @@ -package directory +package filemanager import ( + "encoding/json" + "fmt" "net/http" + "net/url" + "os" + "path" "sort" "strconv" "strings" + "github.com/hacdias/caddy-filemanager/config" + "github.com/hacdias/caddy-filemanager/utils/errors" "github.com/mholt/caddy/caddyhttp/httpserver" ) @@ -16,7 +23,7 @@ type Listing struct { // The full path of the request Path string // The items (files and folders) in the path - Items []Info + Items []FileInfo // The number of directories in the listing NumDirs int // The number of files (items that aren't directories) in the listing @@ -77,15 +84,15 @@ 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 { + if l.Items[i].IsDir() && !l.Items[j].IsDir() { return true } - if !l.Items[i].IsDir && l.Items[j].IsDir { + if !l.Items[i].IsDir() && l.Items[j].IsDir() { return false } - return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name) + return strings.ToLower(l.Items[i].Name()) < strings.ToLower(l.Items[j].Name()) } // By Size @@ -94,11 +101,11 @@ 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, jSize := l.Items[i].Size(), l.Items[j].Size() + if l.Items[i].IsDir() { iSize = directoryOffset + iSize } - if l.Items[j].IsDir { + if l.Items[j].IsDir() { jSize = directoryOffset + jSize } return iSize < jSize @@ -107,7 +114,7 @@ func (l bySize) Less(i, j int) bool { // 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) } +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" @@ -139,3 +146,121 @@ func (l Listing) applySort() { } } } + +func (i *FileInfo) serveListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) { + var err error + + file, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0) + 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: 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{ + pageInfo: &pageInfo{ + Name: listing.Name, + Path: i.VirtualPath, + IsDir: true, + User: u, + Config: c, + Data: listing, + }, + } + + if r.Header.Get("Minimal") == "true" { + page.Minimal = true + } + + return page.PrintAsHTML(w, "listing") +} + +func (i FileInfo) 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.VirtualPath, path, u) + return &listing, nil +} + +func directoryListing(files []os.FileInfo, urlPath string, basePath string, u *config.User) Listing { + var ( + fileinfos []FileInfo + 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, FileInfo{ + FileInfo: f, + URL: url.String(), + UserAllowed: u.Allowed(url.String()), + }) + } + + return Listing{ + Name: path.Base(urlPath), + Path: urlPath, + Items: fileinfos, + NumDirs: dirCount, + NumFiles: fileCount, + } +} diff --git a/page/page.go b/page.go similarity index 85% rename from page/page.go rename to page.go index a8266dc6..f511b7a8 100644 --- a/page/page.go +++ b/page.go @@ -1,4 +1,4 @@ -package page +package filemanager import ( "bytes" @@ -13,14 +13,14 @@ import ( "github.com/hacdias/caddy-filemanager/utils/variables" ) -// Page contains the informations and functions needed to show the page -type Page struct { - *Info +// page contains the informations and functions needed to show the page +type page struct { + *pageInfo Minimal bool } -// Info contains the information of a page -type Info struct { +// pageInfo contains the information of a page +type pageInfo struct { Name string Path string IsDir bool @@ -31,7 +31,7 @@ type Info struct { // BreadcrumbMap returns p.Path where every element is a map // of URLs and path segment names. -func (i Info) BreadcrumbMap() map[string]string { +func (i pageInfo) BreadcrumbMap() map[string]string { result := map[string]string{} if len(i.Path) == 0 { @@ -62,7 +62,7 @@ func (i Info) BreadcrumbMap() map[string]string { } // PreviousLink returns the path of the previous folder -func (i Info) PreviousLink() string { +func (i pageInfo) PreviousLink() string { path := strings.TrimSuffix(i.Path, "/") path = strings.TrimPrefix(path, "/") path = i.Config.AbsoluteURL + "/" + path @@ -76,7 +76,7 @@ func (i Info) PreviousLink() string { } // PrintAsHTML formats the page in HTML and executes the template -func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, error) { +func (p page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, error) { // Create the functions map, then the template, check for erros and // execute the template if there aren't errors functions := template.FuncMap{ @@ -124,7 +124,7 @@ func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro } buf := &bytes.Buffer{} - err := tpl.Execute(buf, p.Info) + err := tpl.Execute(buf, p.pageInfo) if err != nil { return http.StatusInternalServerError, err @@ -136,8 +136,8 @@ func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro } // PrintAsJSON prints the current page infromation in JSON -func (p Page) PrintAsJSON(w http.ResponseWriter) (int, error) { - marsh, err := json.Marshal(p.Info.Data) +func (p page) PrintAsJSON(w http.ResponseWriter) (int, error) { + marsh, err := json.Marshal(p.pageInfo.Data) if err != nil { return http.StatusInternalServerError, err } diff --git a/directory/update.go b/preput.go similarity index 77% rename from directory/update.go rename to preput.go index 10483769..f2285910 100644 --- a/directory/update.go +++ b/preput.go @@ -1,4 +1,4 @@ -package directory +package filemanager import ( "bytes" @@ -15,7 +15,7 @@ import ( ) // 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) { +func (i *FileInfo) Update(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) { var ( data map[string]interface{} file []byte @@ -38,7 +38,7 @@ func (i *Info) Update(w http.ResponseWriter, r *http.Request, c *config.Config, switch kind { case "frontmatter-only": - if file, code, err = ParseFrontMatterOnlyFile(data, i.Name); err != nil { + if file, code, err = parseFrontMatterOnlyFile(data, i.Name()); err != nil { return http.StatusInternalServerError, err } case "content-only": @@ -46,7 +46,7 @@ func (i *Info) Update(w http.ResponseWriter, r *http.Request, c *config.Config, mainContent = strings.TrimSpace(mainContent) file = []byte(mainContent) case "complete": - if file, code, err = ParseCompleteFile(data, i.Name, u.FrontMatter); err != nil { + if file, code, err = parseCompleteFile(data, i.Name(), u.FrontMatter); err != nil { return http.StatusInternalServerError, err } default: @@ -58,10 +58,10 @@ func (i *Info) Update(w http.ResponseWriter, r *http.Request, c *config.Config, return code, nil } -// ParseFrontMatterOnlyFile parses a frontmatter only file -func ParseFrontMatterOnlyFile(data interface{}, filename string) ([]byte, int, error) { +// 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) + f, code, err := parseFrontMatter(data, frontmatter) fString := string(f) // If it's toml or yaml, strip frontmatter identifier @@ -79,8 +79,8 @@ func ParseFrontMatterOnlyFile(data interface{}, filename string) ([]byte, int, e return f, code, err } -// ParseFrontMatter is the frontmatter parser -func ParseFrontMatter(data interface{}, frontmatter string) ([]byte, int, error) { +// parseFrontMatter is the frontmatter parser +func parseFrontMatter(data interface{}, frontmatter string) ([]byte, int, error) { var mark rune switch frontmatter { @@ -103,8 +103,8 @@ func ParseFrontMatter(data interface{}, frontmatter string) ([]byte, int, error) return f, http.StatusOK, nil } -// ParseCompleteFile parses a complete file -func ParseCompleteFile(data map[string]interface{}, filename string, frontmatter string) ([]byte, int, error) { +// parseCompleteFile parses a complete file +func parseCompleteFile(data map[string]interface{}, filename string, frontmatter string) ([]byte, int, error) { mainContent := "" if _, ok := data["content"]; ok { @@ -120,7 +120,7 @@ func ParseCompleteFile(data map[string]interface{}, filename string, frontmatter data["date"] = data["date"].(string) + ":00" } - front, code, err := ParseFrontMatter(data, frontmatter) + front, code, err := parseFrontMatter(data, frontmatter) if err != nil { fmt.Println(frontmatter) From f2fbe92591df25ac96fcf4a8ab80f4978f0d4a56 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 18 Oct 2016 21:30:10 +0100 Subject: [PATCH 08/28] organize better in sub packages --- errors/errors.go | 3 ++ filemanager.go | 3 +- frontmatter/frontmatter.go | 7 +++-- info.go | 5 ++-- listing.go | 10 ++++--- page.go => page/page.go | 37 +++++++++++++------------ utils/{errors/http.go => errors.go} | 11 +++++--- utils/{variables => }/types.go | 2 +- utils/{variables => }/variables.go | 2 +- utils/{variables => }/variables_test.go | 2 +- 10 files changed, 47 insertions(+), 35 deletions(-) rename page.go => page/page.go (76%) rename utils/{errors/http.go => errors.go} (62%) rename utils/{variables => }/types.go (94%) rename utils/{variables => }/variables.go (98%) rename utils/{variables => }/variables_test.go (97%) diff --git a/errors/errors.go b/errors/errors.go index d404e54e..b859a345 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -32,6 +32,9 @@ const template = ` color: #eee; font-weight: bold; } + p { + line-height: 1.3; + } diff --git a/filemanager.go b/filemanager.go index bc8f1e18..8532159f 100644 --- a/filemanager.go +++ b/filemanager.go @@ -17,6 +17,7 @@ import ( "github.com/hacdias/caddy-filemanager/assets" "github.com/hacdias/caddy-filemanager/config" "github.com/hacdias/caddy-filemanager/errors" + "github.com/hacdias/caddy-filemanager/page" "github.com/mholt/caddy/caddyhttp/httpserver" ) @@ -198,6 +199,6 @@ func command(w http.ResponseWriter, r *http.Request, c *config.Config, u *config return http.StatusInternalServerError, err } - p := &page{pageInfo: &pageInfo{Data: string(output)}} + p := &page.Page{Info: &page.Info{Data: string(output)}} return p.PrintAsJSON(w) } diff --git a/frontmatter/frontmatter.go b/frontmatter/frontmatter.go index 5703addc..18edbc5b 100644 --- a/frontmatter/frontmatter.go +++ b/frontmatter/frontmatter.go @@ -13,7 +13,8 @@ import ( "gopkg.in/yaml.v2" "github.com/BurntSushi/toml" - "github.com/hacdias/caddy-filemanager/utils/variables" + "github.com/hacdias/caddy-filemanager/utils" + "github.com/spf13/cast" ) @@ -125,9 +126,9 @@ func rawToPretty(config interface{}, parent *Block) *Content { } for name, element := range cnf { - if variables.IsMap(element) { + if utils.IsMap(element) { objects = append(objects, handleObjects(element, parent, name)) - } else if variables.IsSlice(element) { + } else if utils.IsSlice(element) { arrays = append(arrays, handleArrays(element, parent, name)) } else { if name == "title" && parent.Name == mainName { diff --git a/info.go b/info.go index ed6da747..542c13b3 100644 --- a/info.go +++ b/info.go @@ -10,6 +10,7 @@ import ( humanize "github.com/dustin/go-humanize" "github.com/hacdias/caddy-filemanager/config" + "github.com/hacdias/caddy-filemanager/page" ) // FileInfo contains the information about a particular file or directory @@ -117,8 +118,8 @@ func (i *FileInfo) serveSingleFile(w http.ResponseWriter, r *http.Request, c *co return 0, nil } - p := &page{ - pageInfo: &pageInfo{ + p := &page.Page{ + Info: &page.Info{ Name: i.Name(), Path: i.VirtualPath, IsDir: false, diff --git a/listing.go b/listing.go index 78dc6b47..e57be46f 100644 --- a/listing.go +++ b/listing.go @@ -12,7 +12,9 @@ import ( "strings" "github.com/hacdias/caddy-filemanager/config" - "github.com/hacdias/caddy-filemanager/utils/errors" + "github.com/hacdias/caddy-filemanager/page" + "github.com/hacdias/caddy-filemanager/utils" + "github.com/mholt/caddy/caddyhttp/httpserver" ) @@ -152,7 +154,7 @@ func (i *FileInfo) serveListing(w http.ResponseWriter, r *http.Request, c *confi file, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0) if err != nil { - return errors.ToHTTPCode(err), err + return utils.ErrorToHTTPCode(err, true), err } defer file.Close() @@ -203,8 +205,8 @@ func (i *FileInfo) serveListing(w http.ResponseWriter, r *http.Request, c *confi return http.StatusOK, nil } - page := &page{ - pageInfo: &pageInfo{ + page := &page.Page{ + Info: &page.Info{ Name: listing.Name, Path: i.VirtualPath, IsDir: true, diff --git a/page.go b/page/page.go similarity index 76% rename from page.go rename to page/page.go index f511b7a8..2b2ca46e 100644 --- a/page.go +++ b/page/page.go @@ -1,4 +1,5 @@ -package filemanager +// Package page is used to render the HTML to the end user +package page import ( "bytes" @@ -10,17 +11,17 @@ import ( "github.com/hacdias/caddy-filemanager/assets" "github.com/hacdias/caddy-filemanager/config" - "github.com/hacdias/caddy-filemanager/utils/variables" + "github.com/hacdias/caddy-filemanager/utils" ) -// page contains the informations and functions needed to show the page -type page struct { - *pageInfo +// Page contains the informations and functions needed to show the Page +type Page struct { + *Info Minimal bool } -// pageInfo contains the information of a page -type pageInfo struct { +// Info contains the information of a Page +type Info struct { Name string Path string IsDir bool @@ -31,7 +32,7 @@ type pageInfo struct { // BreadcrumbMap returns p.Path where every element is a map // of URLs and path segment names. -func (i pageInfo) BreadcrumbMap() map[string]string { +func (i Info) BreadcrumbMap() map[string]string { result := map[string]string{} if len(i.Path) == 0 { @@ -62,7 +63,7 @@ func (i pageInfo) BreadcrumbMap() map[string]string { } // PreviousLink returns the path of the previous folder -func (i pageInfo) PreviousLink() string { +func (i Info) PreviousLink() string { path := strings.TrimSuffix(i.Path, "/") path = strings.TrimPrefix(path, "/") path = i.Config.AbsoluteURL + "/" + path @@ -76,11 +77,11 @@ func (i pageInfo) PreviousLink() string { } // PrintAsHTML formats the page in HTML and executes the template -func (p page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, error) { +func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, error) { // Create the functions map, then the template, check for erros and // execute the template if there aren't errors functions := template.FuncMap{ - "Defined": variables.Defined, + "Defined": utils.Defined, "CSS": func(s string) template.CSS { return template.CSS(s) }, @@ -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 { @@ -124,7 +125,7 @@ func (p page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro } buf := &bytes.Buffer{} - err := tpl.Execute(buf, p.pageInfo) + err := tpl.Execute(buf, p.Info) if err != nil { return http.StatusInternalServerError, err @@ -135,9 +136,9 @@ func (p page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro return http.StatusOK, nil } -// PrintAsJSON prints the current page infromation in JSON -func (p page) PrintAsJSON(w http.ResponseWriter) (int, error) { - marsh, err := json.Marshal(p.pageInfo.Data) +// 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 { return http.StatusInternalServerError, err } diff --git a/utils/errors/http.go b/utils/errors.go similarity index 62% rename from utils/errors/http.go rename to utils/errors.go index 644f9c55..35841d85 100644 --- a/utils/errors/http.go +++ b/utils/errors.go @@ -1,17 +1,20 @@ -package errors +package utils import ( "net/http" "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: diff --git a/utils/variables/types.go b/utils/types.go similarity index 94% rename from utils/variables/types.go rename to utils/types.go index ee43dad3..7e6b408b 100644 --- a/utils/variables/types.go +++ b/utils/types.go @@ -1,4 +1,4 @@ -package variables +package utils import "reflect" diff --git a/utils/variables/variables.go b/utils/variables.go similarity index 98% rename from utils/variables/variables.go rename to utils/variables.go index 7a0168b4..28f8383a 100644 --- a/utils/variables/variables.go +++ b/utils/variables.go @@ -1,4 +1,4 @@ -package variables +package utils import ( "errors" diff --git a/utils/variables/variables_test.go b/utils/variables_test.go similarity index 97% rename from utils/variables/variables_test.go rename to utils/variables_test.go index ec76d459..7122478d 100644 --- a/utils/variables/variables_test.go +++ b/utils/variables_test.go @@ -1,4 +1,4 @@ -package variables +package utils import "testing" From fe7579966dda1a2e6cec9baa9436f3de0b3cd917 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 18 Oct 2016 21:49:46 +0100 Subject: [PATCH 09/28] Improvements --- editor.go => file/editor.go | 78 +++++++++++-------------------- info.go => file/info.go | 43 +++++++---------- listing.go => file/listing.go | 12 ++--- filemanager.go | 14 +++--- frontmatter/runes.go | 24 ++++++++++ errors/errors.go => page/error.go | 10 ++-- preput.go => preprocess.go | 5 +- 7 files changed, 90 insertions(+), 96 deletions(-) rename editor.go => file/editor.go (50%) rename info.go => file/info.go (71%) rename listing.go => file/listing.go (94%) create mode 100644 frontmatter/runes.go rename errors/errors.go => page/error.go (86%) rename preput.go => preprocess.go (93%) diff --git a/editor.go b/file/editor.go similarity index 50% rename from editor.go rename to file/editor.go index d26efe0d..e0cd3ba8 100644 --- a/editor.go +++ b/file/editor.go @@ -1,4 +1,4 @@ -package filemanager +package file import ( "bytes" @@ -18,7 +18,7 @@ type Editor struct { } // GetEditor gets the editor based on a FileInfo struct -func (i *FileInfo) GetEditor() (*Editor, error) { +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()), ".") @@ -41,44 +41,39 @@ func (i *FileInfo) GetEditor() (*Editor, error) { // Handle the content depending on the file extension switch editor.Mode { - case "markdown", "asciidoc", "rst": - if !hasFrontMatterRune(i.Content) { - editor.Class = "content-only" - editor.Content = i.StringifyContent() - break - } - - // 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 { - editor.Class = "content-only" - editor.Content = i.StringifyContent() - break - } - - // Parses the page content and the frontmatter - editor.Content = strings.TrimSpace(string(page.Content())) - editor.FrontMatter, _, err = frontmatter.Pretty(page.FrontMatter()) 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.Content) { + if frontmatter.HasRune(i.Content) { editor.FrontMatter, _, err = frontmatter.Pretty(i.Content) } else { - editor.FrontMatter, _, err = frontmatter.Pretty(appendFrontMatterRune(i.Content, editor.Mode)) + editor.FrontMatter, _, err = frontmatter.Pretty(frontmatter.AppendRune(i.Content, editor.Mode)) } // Check if there were any errors - if err != nil { - editor.Class = "content-only" - editor.Content = i.StringifyContent() + 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() @@ -87,29 +82,12 @@ func (i *FileInfo) GetEditor() (*Editor, error) { 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 +// CanBeEdited checks if the extension of a file is supported by the editor +func (i Info) CanBeEdited() bool { + if i.Type == "text" { + return true } - 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", @@ -122,7 +100,7 @@ func canBeEdited(filename string) bool { } for _, extension := range extensions { - if strings.HasSuffix(filename, extension) { + if strings.HasSuffix(i.Name(), extension) { return true } } diff --git a/info.go b/file/info.go similarity index 71% rename from info.go rename to file/info.go index 542c13b3..533bae86 100644 --- a/info.go +++ b/file/info.go @@ -1,4 +1,4 @@ -package filemanager +package file import ( "io/ioutil" @@ -11,10 +11,11 @@ import ( humanize "github.com/dustin/go-humanize" "github.com/hacdias/caddy-filemanager/config" "github.com/hacdias/caddy-filemanager/page" + "github.com/hacdias/caddy-filemanager/utils" ) -// FileInfo contains the information about a particular file or directory -type FileInfo struct { +// Info contains the information about a particular file or directory +type Info struct { os.FileInfo URL string Path string // Relative path to Caddyfile @@ -27,10 +28,10 @@ type FileInfo struct { // 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) (*FileInfo, int, error) { +func GetInfo(url *url.URL, c *config.Config, u *config.User) (*Info, int, error) { var err error - i := &FileInfo{URL: url.Path} + i := &Info{URL: url.Path} i.VirtualPath = strings.Replace(url.Path, c.BaseURL, "", 1) i.VirtualPath = strings.TrimPrefix(i.VirtualPath, "/") i.VirtualPath = "/" + i.VirtualPath @@ -41,50 +42,40 @@ func GetInfo(url *url.URL, c *config.Config, u *config.User) (*FileInfo, int, er i.FileInfo, err = os.Stat(i.Path) if err != nil { - code := http.StatusInternalServerError - - switch { - case os.IsPermission(err): - code = http.StatusForbidden - case os.IsNotExist(err): - code = http.StatusGone - case os.IsExist(err): - code = http.StatusGone - } - - return i, code, err + return i, utils.ErrorToHTTPCode(err, false), err } return i, 0, nil } -func (i *FileInfo) Read() error { +func (i *Info) Read() error { var err error i.Content, err = ioutil.ReadFile(i.Path) if err != nil { return err } i.Mimetype = http.DetectContentType(i.Content) - i.Type = SimplifyMimeType(i.Mimetype) + i.Type = simplifyMediaType(i.Mimetype) return nil } -func (i FileInfo) StringifyContent() string { +// 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 FileInfo) HumanSize() string { +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 FileInfo) HumanModTime(format string) string { +func (i Info) HumanModTime(format string) string { return i.ModTime().Format(format) } -func (i *FileInfo) ServeHTTP(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) { +func (i *Info) ServeHTTP(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) { if i.IsDir() { return i.serveListing(w, r, c, u) } @@ -92,7 +83,7 @@ func (i *FileInfo) ServeHTTP(w http.ResponseWriter, r *http.Request, c *config.C return i.serveSingleFile(w, r, c, u) } -func (i *FileInfo) serveSingleFile(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) { +func (i *Info) serveSingleFile(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) { err := i.Read() if err != nil { code := http.StatusInternalServerError @@ -129,7 +120,7 @@ func (i *FileInfo) serveSingleFile(w http.ResponseWriter, r *http.Request, c *co }, } - if (canBeEdited(i.Name()) || i.Type == "text") && u.AllowEdit { + if i.CanBeEdited() && u.AllowEdit { p.Data, err = i.GetEditor() if err != nil { return http.StatusInternalServerError, err @@ -141,7 +132,7 @@ func (i *FileInfo) serveSingleFile(w http.ResponseWriter, r *http.Request, c *co return p.PrintAsHTML(w, "single") } -func SimplifyMimeType(name string) string { +func simplifyMediaType(name string) string { if strings.HasPrefix(name, "video") { return "video" } diff --git a/listing.go b/file/listing.go similarity index 94% rename from listing.go rename to file/listing.go index e57be46f..c7e2c7eb 100644 --- a/listing.go +++ b/file/listing.go @@ -1,4 +1,4 @@ -package filemanager +package file import ( "encoding/json" @@ -25,7 +25,7 @@ type Listing struct { // The full path of the request Path string // The items (files and folders) in the path - Items []FileInfo + Items []Info // The number of directories in the listing NumDirs int // The number of files (items that aren't directories) in the listing @@ -149,7 +149,7 @@ func (l Listing) applySort() { } } -func (i *FileInfo) serveListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) { +func (i *Info) serveListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) { var err error file, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0) @@ -223,7 +223,7 @@ func (i *FileInfo) serveListing(w http.ResponseWriter, r *http.Request, c *confi return page.PrintAsHTML(w, "listing") } -func (i FileInfo) loadDirectoryContents(file http.File, path string, u *config.User) (*Listing, error) { +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 @@ -235,7 +235,7 @@ func (i FileInfo) loadDirectoryContents(file http.File, path string, u *config.U func directoryListing(files []os.FileInfo, urlPath string, basePath string, u *config.User) Listing { var ( - fileinfos []FileInfo + fileinfos []Info dirCount, fileCount int ) @@ -251,7 +251,7 @@ func directoryListing(files []os.FileInfo, urlPath string, basePath string, u *c // Absolute URL url := url.URL{Path: basePath + name} - fileinfos = append(fileinfos, FileInfo{ + fileinfos = append(fileinfos, Info{ FileInfo: f, URL: url.String(), UserAllowed: u.Allowed(url.String()), diff --git a/filemanager.go b/filemanager.go index 8532159f..b5826498 100644 --- a/filemanager.go +++ b/filemanager.go @@ -16,7 +16,7 @@ import ( "github.com/hacdias/caddy-filemanager/assets" "github.com/hacdias/caddy-filemanager/config" - "github.com/hacdias/caddy-filemanager/errors" + "github.com/hacdias/caddy-filemanager/file" "github.com/hacdias/caddy-filemanager/page" "github.com/mholt/caddy/caddyhttp/httpserver" ) @@ -32,7 +32,7 @@ type FileManager struct { func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { var ( c *config.Config - fi *FileInfo + fi *file.Info code int err error user *config.User @@ -71,7 +71,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err } if r.Method == http.MethodPut { - _, err = fi.Update(w, r, c, user) + _, err = processPUT(w, r, c, user, fi) if err != nil { return http.StatusInternalServerError, err } @@ -83,7 +83,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.BaseURL)) { if r.Method == http.MethodGet { - return errors.PrintHTML( + return page.PrintErrorHTML( w, http.StatusForbidden, e.New("You don't have permission to access this page."), @@ -95,10 +95,10 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err if r.Method == http.MethodGet { // Gets the information of the directory/file - fi, code, err = GetInfo(r.URL, c, user) + fi, code, err = file.GetInfo(r.URL, c, user) if err != nil { if r.Method == http.MethodGet { - return errors.PrintHTML(w, code, err) + return page.PrintErrorHTML(w, code, err) } return code, err } @@ -131,7 +131,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err code, err := fi.ServeHTTP(w, r, c, user) if err != nil { - return errors.PrintHTML(w, code, err) + return page.PrintErrorHTML(w, code, err) } return code, err } 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/errors/errors.go b/page/error.go similarity index 86% rename from errors/errors.go rename to page/error.go index b859a345..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 @@ -48,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/preput.go b/preprocess.go similarity index 93% rename from preput.go rename to preprocess.go index f2285910..0d039252 100644 --- a/preput.go +++ b/preprocess.go @@ -11,11 +11,12 @@ import ( "strings" "github.com/hacdias/caddy-filemanager/config" + "github.com/hacdias/caddy-filemanager/file" "github.com/spf13/hugo/parser" ) -// Update is used to update a file that was edited -func (i *FileInfo) Update(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) { +// processPUT is used to update a file that was edited +func processPUT(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User, i *file.Info) (int, error) { var ( data map[string]interface{} file []byte From 9f9a6254e549854cf6f2b07dce4daa2e54a5ec50 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 18 Oct 2016 22:00:26 +0100 Subject: [PATCH 10/28] Make router prettier --- filemanager.go | 207 ++++++++++++++++++++++++++----------------------- 1 file changed, 109 insertions(+), 98 deletions(-) diff --git a/filemanager.go b/filemanager.go index b5826498..e3ac8d32 100644 --- a/filemanager.go +++ b/filemanager.go @@ -39,128 +39,139 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err ) for i := range f.Configs { - if httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { - c = &f.Configs[i] + // 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 r.Method == http.MethodGet && httpserver.Path(r.URL.Path).Matches(c.BaseURL+assets.BaseURL) { + 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) } - username, _, _ := r.BasicAuth() + return http.StatusForbidden, nil + } - if _, ok := c.Users[username]; ok { - user = c.Users[username] - } else { - user = c.User - } - - if strings.HasPrefix(r.URL.Path, c.WebDavURL) { - if !user.Allowed(strings.TrimPrefix(r.URL.Path, c.WebDavURL)) { - return http.StatusForbidden, nil - } - - 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 - } - } - - if r.Method == http.MethodPut { - _, err = processPUT(w, r, c, user, fi) - if err != nil { - return http.StatusInternalServerError, err - } - } - - c.Handler.ServeHTTP(w, r) - return 0, nil - } - - 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."), - ) - } + // Obtains the user + 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 strings.HasPrefix(r.URL.Path, 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 "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 { + _, err = processPUT(w, r, c, user, fi) + if err != nil { + return http.StatusInternalServerError, err + } + } + + c.Handler.ServeHTTP(w, r) + return 0, nil + } + + // 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 { - // 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 - } + return page.PrintErrorHTML( + w, http.StatusForbidden, + e.New("You don't have permission to access this page."), + ) + } - // 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 - } + return http.StatusForbidden, nil + } - // Generate anti security token. - c.GenerateToken() - - if !fi.IsDir() { - query := r.URL.Query() - if val, ok := query["raw"]; ok && val[0] == "true" { - r.URL.Path = strings.Replace(r.URL.Path, c.BaseURL, c.WebDavURL, 1) - c.Handler.ServeHTTP(w, r) - return 0, nil - } - - if val, ok := query["download"]; ok && val[0] == "true" { - w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name()) - r.URL.Path = strings.Replace(r.URL.Path, c.BaseURL, c.WebDavURL, 1) - c.Handler.ServeHTTP(w, r) - return 0, nil - } - } - - code, err := fi.ServeHTTP(w, r, c, user) - if err != nil { + if r.Method == http.MethodGet { + // 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 } - if r.Method == http.MethodPost { - // TODO: How to exclude web dav clients? :/ - // Security measures against CSRF attacks. - if !c.CheckToken(r) { - return http.StatusForbidden, nil + // 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 + } + + // Generate anti security token. + c.GenerateToken() + + if !fi.IsDir() { + query := r.URL.Query() + webdav := false + + if val, ok := query["raw"]; ok && val[0] == "true" { + webdav = true } - /* TODO: search commands. USE PROPFIND? - // Search and git commands. - if r.Header.Get("Search") == "true" { + if val, ok := query["download"]; ok && val[0] == "true" { + w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name()) + webdav = true + } - } */ - - // VCS commands. - if r.Header.Get("Command") != "" { - if !user.AllowCommands { - return http.StatusUnauthorized, nil - } - - return command(w, r, c, user) + if webdav { + r.URL.Path = strings.Replace(r.URL.Path, c.BaseURL, c.WebDavURL, 1) + c.Handler.ServeHTTP(w, r) + return 0, nil } } - return http.StatusNotImplemented, nil + code, err := fi.ServeHTTP(w, r, c, user) + if err != nil { + return 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 command(w, r, c, user) + } + } + + return http.StatusNotImplemented, nil + } return f.Next.ServeHTTP(w, r) From 3baf7537d9031da485b77030afbd819d8ffdb560 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Wed, 19 Oct 2016 20:46:29 +0100 Subject: [PATCH 11/28] Make sort functions more readable --- file/listing.go | 112 -------------------------------- file/listing_sort.go | 148 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 112 deletions(-) create mode 100644 file/listing_sort.go diff --git a/file/listing.go b/file/listing.go index c7e2c7eb..95e30c8d 100644 --- a/file/listing.go +++ b/file/listing.go @@ -7,8 +7,6 @@ import ( "net/url" "os" "path" - "sort" - "strconv" "strings" "github.com/hacdias/caddy-filemanager/config" @@ -39,116 +37,6 @@ type Listing struct { 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 - } - } -} - func (i *Info) serveListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) { var err error diff --git a/file/listing_sort.go b/file/listing_sort.go new file mode 100644 index 00000000..54076fda --- /dev/null +++ b/file/listing_sort.go @@ -0,0 +1,148 @@ +package file + +import ( + "net/http" + "sort" + "strconv" + "strings" +) + +// 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 +} + +// 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 + } + } +} + +// 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()) +} From 63a1a2cd5457e23763d6ed567a11d643c50c20b1 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Wed, 19 Oct 2016 20:58:08 +0100 Subject: [PATCH 12/28] make everything more readble --- file/listing.go | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/file/listing.go b/file/listing.go index 95e30c8d..71f0cb71 100644 --- a/file/listing.go +++ b/file/listing.go @@ -2,7 +2,6 @@ package file import ( "encoding/json" - "fmt" "net/http" "net/url" "os" @@ -40,23 +39,18 @@ type Listing struct { func (i *Info) serveListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) { var err error + // Gets the directory information using the Virtual File System of + // the user configuration file, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0) if err != nil { return utils.ErrorToHTTPCode(err, true), err } defer file.Close() + // Loads the content of the directory 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 - } + return utils.ErrorToHTTPCode(err, true), err } listing.Context = httpserver.Context{ @@ -111,17 +105,12 @@ func (i *Info) serveListing(w http.ResponseWriter, r *http.Request, c *config.Co return page.PrintAsHTML(w, "listing") } -func (i Info) loadDirectoryContents(file http.File, path string, u *config.User) (*Listing, error) { +func (i Info) loadDirectoryContents(file http.File, basePath string, u *config.User) (*Listing, error) { files, err := file.Readdir(-1) if err != nil { return nil, err } - listing := directoryListing(files, i.VirtualPath, path, u) - return &listing, nil -} - -func directoryListing(files []os.FileInfo, urlPath string, basePath string, u *config.User) Listing { var ( fileinfos []Info dirCount, fileCount int @@ -146,11 +135,11 @@ func directoryListing(files []os.FileInfo, urlPath string, basePath string, u *c }) } - return Listing{ - Name: path.Base(urlPath), - Path: urlPath, + return &Listing{ + Name: path.Base(i.VirtualPath), + Path: i.VirtualPath, Items: fileinfos, NumDirs: dirCount, NumFiles: fileCount, - } + }, nil } From 3379e6e67c062d87d3634e7ce6814abad19e733e Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Thu, 20 Oct 2016 21:55:55 +0100 Subject: [PATCH 13/28] add support to download directories as zip, tar, targz and tarbz2 #29 --- file/download.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ filemanager.go | 6 ++++++ 2 files changed, 55 insertions(+) create mode 100644 file/download.go diff --git a/file/download.go b/file/download.go new file mode 100644 index 00000000..e46bbbca --- /dev/null +++ b/file/download.go @@ -0,0 +1,49 @@ +package file + +import ( + "io" + "io/ioutil" + "net/http" + "os" + + "github.com/mholt/archiver" +) + +func (i *Info) DownloadAs(w http.ResponseWriter, query string) (int, error) { + var ( + extension string + temp string + err error + ) + + temp, err = ioutil.TempDir("", "") + if err != nil { + return http.StatusInternalServerError, err + } + + switch query { + case "zip": + extension, err = ".zip", archiver.Zip.Make(temp+"/temp", []string{i.Path}) + case "tar": + extension, err = ".tar", archiver.Tar.Make(temp+"/temp", []string{i.Path}) + case "targz": + extension, err = ".tar.gz", archiver.TarGz.Make(temp+"/temp", []string{i.Path}) + case "tarbz2": + extension, err = ".tar.bz2", archiver.TarBz2.Make(temp+"/temp", []string{i.Path}) + 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/filemanager.go b/filemanager.go index e3ac8d32..a915ba5b 100644 --- a/filemanager.go +++ b/filemanager.go @@ -126,6 +126,12 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err // Generate anti security token. c.GenerateToken() + if fi.IsDir() { + if val, ok := r.URL.Query()["download"]; ok && val[0] != "" { + return fi.DownloadAs(w, val[0]) + } + } + if !fi.IsDir() { query := r.URL.Query() webdav := false From 9453dc246a1ee2772cddeb2029e5a1894812d47f Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Thu, 20 Oct 2016 21:58:53 +0100 Subject: [PATCH 14/28] updates on #29 --- file/download.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/file/download.go b/file/download.go index e46bbbca..ce751e3f 100644 --- a/file/download.go +++ b/file/download.go @@ -5,15 +5,19 @@ import ( "io/ioutil" "net/http" "os" + "path/filepath" "github.com/mholt/archiver" ) +// DownloadAs creates an archieve in one of the supported formats (zip, tar, +// tar.gz or tar.bz2) and sends it to be downloaded. func (i *Info) DownloadAs(w http.ResponseWriter, query string) (int, error) { var ( extension string temp string err error + tempfile string ) temp, err = ioutil.TempDir("", "") @@ -21,15 +25,18 @@ func (i *Info) DownloadAs(w http.ResponseWriter, query string) (int, error) { return http.StatusInternalServerError, err } + defer os.RemoveAll(temp) + tempfile = filepath.Join(temp, "temp") + switch query { case "zip": - extension, err = ".zip", archiver.Zip.Make(temp+"/temp", []string{i.Path}) + extension, err = ".zip", archiver.Zip.Make(tempfile, []string{i.Path}) case "tar": - extension, err = ".tar", archiver.Tar.Make(temp+"/temp", []string{i.Path}) + extension, err = ".tar", archiver.Tar.Make(tempfile, []string{i.Path}) case "targz": - extension, err = ".tar.gz", archiver.TarGz.Make(temp+"/temp", []string{i.Path}) + extension, err = ".tar.gz", archiver.TarGz.Make(tempfile, []string{i.Path}) case "tarbz2": - extension, err = ".tar.bz2", archiver.TarBz2.Make(temp+"/temp", []string{i.Path}) + extension, err = ".tar.bz2", archiver.TarBz2.Make(tempfile, []string{i.Path}) default: return http.StatusNotImplemented, nil } From 591d5b3084d75ad8663bf2399cde2fa3b6830bec Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Thu, 20 Oct 2016 22:18:45 +0100 Subject: [PATCH 15/28] css fix word-wrap on command result --- assets/embed/public/css/styles.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/embed/public/css/styles.css b/assets/embed/public/css/styles.css index bb21074c..ce534396 100644 --- a/assets/embed/public/css/styles.css +++ b/assets/embed/public/css/styles.css @@ -566,6 +566,7 @@ header p i { transition: .1s ease all; visibility: hidden; opacity: 0; + word-wrap: break-word; } #search.active div i, @@ -1166,4 +1167,4 @@ i.spin { column-count: 1; column-gap: 0; } -} +} \ No newline at end of file From 1b387d215f809760df7f30ddc41385cd8aba5094 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Thu, 20 Oct 2016 22:23:36 +0100 Subject: [PATCH 16/28] Progresses on #23. Support differnt webdav urls --- assets/embed/public/js/application.js | 4 ++-- assets/embed/templates/base.tmpl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/embed/public/js/application.js b/assets/embed/public/js/application.js index a4e2b86f..12c73c70 100644 --- a/assets/embed/public/js/application.js +++ b/assets/embed/public/js/application.js @@ -89,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 } @@ -908,4 +908,4 @@ document.addEventListener("DOMContentLoaded", function(event) { } return false; -}); +}); \ No newline at end of file diff --git a/assets/embed/templates/base.tmpl b/assets/embed/templates/base.tmpl index 7acb1bab..86f4e844 100644 --- a/assets/embed/templates/base.tmpl +++ b/assets/embed/templates/base.tmpl @@ -129,8 +129,8 @@ - - + + From 212bba2c376fef38dd05b60601c5bf4dd6afd033 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Thu, 20 Oct 2016 22:45:43 +0100 Subject: [PATCH 17/28] improve styles; on hover not really working --- assets/embed/public/css/styles.css | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/assets/embed/public/css/styles.css b/assets/embed/public/css/styles.css index ce534396..644d9cec 100644 --- a/assets/embed/public/css/styles.css +++ b/assets/embed/public/css/styles.css @@ -687,14 +687,14 @@ header .prev-links:hover { header .prev-links { position: absolute; - top: 0; + top: 4em; 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; @@ -702,6 +702,16 @@ header .prev-links { min-width: 12em; } +header .prev-links:before { + top: -16px; + left: 1em; + right: auto; + border: 8px solid transparent; + border-bottom-color: #68efad; + content: ''; + position: absolute; +} + header .prev-links a { padding: .5em; border-bottom: 1px solid #f5f5f5; @@ -1167,4 +1177,4 @@ i.spin { column-count: 1; column-gap: 0; } -} \ No newline at end of file +} From 4c8e023e9f8490940d130aa68958ae844eb7368a Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 22 Oct 2016 09:19:08 +0100 Subject: [PATCH 18/28] update --- file/listing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/file/listing.go b/file/listing.go index 71f0cb71..757a9080 100644 --- a/file/listing.go +++ b/file/listing.go @@ -131,7 +131,7 @@ func (i Info) loadDirectoryContents(file http.File, basePath string, u *config.U fileinfos = append(fileinfos, Info{ FileInfo: f, URL: url.String(), - UserAllowed: u.Allowed(url.String()), + UserAllowed: u.Allowed(i.VirtualPath), }) } From ae338251823f1a0b101ce5ce4dd327405b12c13b Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 22 Oct 2016 11:47:49 +0100 Subject: [PATCH 19/28] organise stuff better --- assets/embed/templates/single.tmpl | 4 +- file/download.go | 55 --------- file/info.go | 58 ---------- file/listing.go | 175 +++++++++++++++++------------ file/listing_sort.go | 148 ------------------------ filemanager.go | 80 +++---------- handlers/command.go | 48 ++++++++ handlers/download.go | 70 ++++++++++++ handlers/listing.go | 123 ++++++++++++++++++++ handlers/single.go | 41 +++++++ preprocess.go | 8 +- 11 files changed, 407 insertions(+), 403 deletions(-) delete mode 100644 file/listing_sort.go create mode 100644 handlers/command.go create mode 100644 handlers/download.go create mode 100644 handlers/listing.go create mode 100644 handlers/single.go diff --git a/assets/embed/templates/single.tmpl b/assets/embed/templates/single.tmpl index d70d1646..f360663e 100644 --- a/assets/embed/templates/single.tmpl +++ b/assets/embed/templates/single.tmpl @@ -6,7 +6,9 @@ {{ else if eq .Type "audio" }} {{ else if eq .Type "video" }} - + + {{ else if eq .Type "blob" }} + Download {{ else}}
{{ .StringifyContent }}
{{ end }} diff --git a/file/download.go b/file/download.go index ce751e3f..b691ba57 100644 --- a/file/download.go +++ b/file/download.go @@ -1,56 +1 @@ package file - -import ( - "io" - "io/ioutil" - "net/http" - "os" - "path/filepath" - - "github.com/mholt/archiver" -) - -// DownloadAs creates an archieve in one of the supported formats (zip, tar, -// tar.gz or tar.bz2) and sends it to be downloaded. -func (i *Info) DownloadAs(w http.ResponseWriter, query string) (int, error) { - 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, []string{i.Path}) - case "tar": - extension, err = ".tar", archiver.Tar.Make(tempfile, []string{i.Path}) - case "targz": - extension, err = ".tar.gz", archiver.TarGz.Make(tempfile, []string{i.Path}) - case "tarbz2": - extension, err = ".tar.bz2", archiver.TarBz2.Make(tempfile, []string{i.Path}) - 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/file/info.go b/file/info.go index 533bae86..9ffaa66d 100644 --- a/file/info.go +++ b/file/info.go @@ -10,7 +10,6 @@ import ( humanize "github.com/dustin/go-humanize" "github.com/hacdias/caddy-filemanager/config" - "github.com/hacdias/caddy-filemanager/page" "github.com/hacdias/caddy-filemanager/utils" ) @@ -75,63 +74,6 @@ func (i Info) HumanModTime(format string) string { return i.ModTime().Format(format) } -func (i *Info) ServeHTTP(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.Read() - if err != nil { - code := http.StatusInternalServerError - - switch { - case os.IsPermission(err): - code = http.StatusForbidden - case os.IsNotExist(err): - code = http.StatusGone - case os.IsExist(err): - code = http.StatusGone - } - - return code, err - } - - if i.Type == "blob" { - http.Redirect( - w, r, - c.AddrPath+r.URL.Path+"?download=true", - http.StatusTemporaryRedirect, - ) - return 0, nil - } - - 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 = i.GetEditor() - if err != nil { - return http.StatusInternalServerError, err - } - - return p.PrintAsHTML(w, "frontmatter", "editor") - } - - return p.PrintAsHTML(w, "single") -} - func simplifyMediaType(name string) string { if strings.HasPrefix(name, "video") { return "video" diff --git a/file/listing.go b/file/listing.go index 757a9080..91ff2b1a 100644 --- a/file/listing.go +++ b/file/listing.go @@ -1,16 +1,13 @@ package file import ( - "encoding/json" - "net/http" "net/url" "os" "path" + "sort" "strings" "github.com/hacdias/caddy-filemanager/config" - "github.com/hacdias/caddy-filemanager/page" - "github.com/hacdias/caddy-filemanager/utils" "github.com/mholt/caddy/caddyhttp/httpserver" ) @@ -19,7 +16,7 @@ import ( type Listing struct { // The name of the directory (the last element of the path) Name string - // The full path of the request + // The full path of the request relatively to a File System Path string // The items (files and folders) in the path Items []Info @@ -36,76 +33,17 @@ type Listing struct { httpserver.Context `json:"-"` } -func (i *Info) serveListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) { - var err error - +// 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(i.VirtualPath, os.O_RDONLY, 0) + // the user configuration. + file, err := u.FileSystem.OpenFile(filePath, os.O_RDONLY, 0) if err != nil { - return utils.ErrorToHTTPCode(err, true), err + return nil, err } defer file.Close() - // Loads the content of the directory - listing, err := i.loadDirectoryContents(file, r.URL.Path, u) - if err != nil { - return utils.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{ - Info: &page.Info{ - Name: listing.Name, - Path: i.VirtualPath, - 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, basePath string, u *config.User) (*Listing, error) { + // Reads the directory and gets the information about the files. files, err := file.Readdir(-1) if err != nil { return nil, err @@ -127,19 +65,108 @@ func (i Info) loadDirectoryContents(file http.File, basePath string, u *config.U } // Absolute URL - url := url.URL{Path: basePath + name} + url := url.URL{Path: baseURL + name} fileinfos = append(fileinfos, Info{ FileInfo: f, URL: url.String(), - UserAllowed: u.Allowed(i.VirtualPath), + UserAllowed: u.Allowed(filePath), }) } return &Listing{ - Name: path.Base(i.VirtualPath), - Path: i.VirtualPath, + 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/file/listing_sort.go b/file/listing_sort.go deleted file mode 100644 index 54076fda..00000000 --- a/file/listing_sort.go +++ /dev/null @@ -1,148 +0,0 @@ -package file - -import ( - "net/http" - "sort" - "strconv" - "strings" -) - -// 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 -} - -// 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 - } - } -} - -// 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 a915ba5b..23d31bbd 100644 --- a/filemanager.go +++ b/filemanager.go @@ -10,13 +10,12 @@ package filemanager import ( e "errors" "net/http" - "os/exec" - "path/filepath" "strings" "github.com/hacdias/caddy-filemanager/assets" "github.com/hacdias/caddy-filemanager/config" "github.com/hacdias/caddy-filemanager/file" + "github.com/hacdias/caddy-filemanager/handlers" "github.com/hacdias/caddy-filemanager/page" "github.com/mholt/caddy/caddyhttp/httpserver" ) @@ -126,36 +125,22 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err // Generate anti security token. c.GenerateToken() - if fi.IsDir() { - if val, ok := r.URL.Query()["download"]; ok && val[0] != "" { - return fi.DownloadAs(w, val[0]) - } + 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: + code, err = handlers.ServeSingle(w, r, c, user, fi) } - if !fi.IsDir() { - query := r.URL.Query() - webdav := false - - if val, ok := query["raw"]; ok && val[0] == "true" { - webdav = true - } - - if val, ok := query["download"]; ok && val[0] == "true" { - w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name()) - webdav = true - } - - if webdav { - r.URL.Path = strings.Replace(r.URL.Path, c.BaseURL, c.WebDavURL, 1) - c.Handler.ServeHTTP(w, r) - return 0, nil - } - } - - code, err := fi.ServeHTTP(w, r, c, user) if err != nil { - return page.PrintErrorHTML(w, code, err) + code, err = page.PrintErrorHTML(w, code, err) } + return code, err } @@ -172,7 +157,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err return http.StatusUnauthorized, nil } - return command(w, r, c, user) + return handlers.Command(w, r, c, user) } } @@ -182,40 +167,3 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err return f.Next.ServeHTTP(w, r) } - -// 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/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..f4fc95a2 --- /dev/null +++ b/handlers/download.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + + "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 + } + + 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, []string{i.Path}) + case "tar": + extension, err = ".tar", archiver.Tar.Make(tempfile, []string{i.Path}) + case "targz": + extension, err = ".tar.gz", archiver.TarGz.Make(tempfile, []string{i.Path}) + case "tarbz2": + extension, err = ".tar.bz2", archiver.TarBz2.Make(tempfile, []string{i.Path}) + 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/listing.go b/handlers/listing.go new file mode 100644 index 00000000..13b36d33 --- /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" + "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 utils.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/single.go b/handlers/single.go new file mode 100644 index 00000000..93dd3b85 --- /dev/null +++ b/handlers/single.go @@ -0,0 +1,41 @@ +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" +) + +// 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) { + err := i.Read() + if err != nil { + return utils.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 = i.GetEditor() + if err != nil { + return http.StatusInternalServerError, err + } + + return p.PrintAsHTML(w, "frontmatter", "editor") + } + + return p.PrintAsHTML(w, "single") +} diff --git a/preprocess.go b/preprocess.go index 0d039252..bec8b2f7 100644 --- a/preprocess.go +++ b/preprocess.go @@ -16,7 +16,13 @@ import ( ) // processPUT is used to update a file that was edited -func processPUT(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User, i *file.Info) (int, error) { +func processPUT( + w http.ResponseWriter, + r *http.Request, + c *config.Config, + u *config.User, + i *file.Info, +) (int, error) { var ( data map[string]interface{} file []byte From df888b604ac6e7222e5cf72d771f930e3fde1f38 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 22 Oct 2016 12:00:45 +0100 Subject: [PATCH 20/28] move put to put --- file/download.go | 1 - filemanager.go | 3 +-- preprocess.go => handlers/put.go | 44 +++++++++++++++----------------- 3 files changed, 22 insertions(+), 26 deletions(-) delete mode 100644 file/download.go rename preprocess.go => handlers/put.go (72%) diff --git a/file/download.go b/file/download.go deleted file mode 100644 index b691ba57..00000000 --- a/file/download.go +++ /dev/null @@ -1 +0,0 @@ -package file diff --git a/filemanager.go b/filemanager.go index 23d31bbd..d53ae279 100644 --- a/filemanager.go +++ b/filemanager.go @@ -83,8 +83,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err // Preprocess the PUT request if it's the case if r.Method == http.MethodPut { - _, err = processPUT(w, r, c, user, fi) - if err != nil { + if handlers.PreProccessPUT(w, r, c, user, fi) != nil { return http.StatusInternalServerError, err } } diff --git a/preprocess.go b/handlers/put.go similarity index 72% rename from preprocess.go rename to handlers/put.go index bec8b2f7..e13a463d 100644 --- a/preprocess.go +++ b/handlers/put.go @@ -1,4 +1,4 @@ -package filemanager +package handlers import ( "bytes" @@ -15,19 +15,17 @@ import ( "github.com/spf13/hugo/parser" ) -// processPUT is used to update a file that was edited -func processPUT( +// 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, -) (int, error) { +) (err error) { var ( data map[string]interface{} file []byte - code int - err error kind string rawBuffer = new(bytes.Buffer) ) @@ -39,22 +37,22 @@ func processPUT( err = json.Unmarshal(rawBuffer.Bytes(), &data) if err != nil { - return http.StatusInternalServerError, err + return } } switch kind { case "frontmatter-only": - if file, code, err = parseFrontMatterOnlyFile(data, i.Name()); err != nil { - return http.StatusInternalServerError, err + 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, code, err = parseCompleteFile(data, i.Name(), u.FrontMatter); err != nil { - return http.StatusInternalServerError, err + if file, err = parseCompleteFile(data, i.Name(), u.FrontMatter); err != nil { + return } default: file = rawBuffer.Bytes() @@ -62,13 +60,13 @@ func processPUT( // Overwrite the request Body r.Body = ioutil.NopCloser(bytes.NewReader(file)) - return code, nil + return } // parseFrontMatterOnlyFile parses a frontmatter only file -func parseFrontMatterOnlyFile(data interface{}, filename string) ([]byte, int, error) { +func parseFrontMatterOnlyFile(data interface{}, filename string) ([]byte, error) { frontmatter := strings.TrimPrefix(filepath.Ext(filename), ".") - f, code, err := parseFrontMatter(data, frontmatter) + f, err := parseFrontMatter(data, frontmatter) fString := string(f) // If it's toml or yaml, strip frontmatter identifier @@ -83,11 +81,11 @@ func parseFrontMatterOnlyFile(data interface{}, filename string) ([]byte, int, e } f = []byte(fString) - return f, code, err + return f, err } // parseFrontMatter is the frontmatter parser -func parseFrontMatter(data interface{}, frontmatter string) ([]byte, int, error) { +func parseFrontMatter(data interface{}, frontmatter string) ([]byte, error) { var mark rune switch frontmatter { @@ -98,20 +96,20 @@ func parseFrontMatter(data interface{}, frontmatter string) ([]byte, int, error) case "yaml": mark = rune('-') default: - return []byte{}, http.StatusBadRequest, errors.New("Can't define the frontmatter.") + return []byte{}, errors.New("Can't define the frontmatter.") } f, err := parser.InterfaceToFrontMatter(data, mark) if err != nil { - return []byte{}, http.StatusInternalServerError, err + return []byte{}, err } - return f, http.StatusOK, nil + return f, nil } // parseCompleteFile parses a complete file -func parseCompleteFile(data map[string]interface{}, filename string, frontmatter string) ([]byte, int, error) { +func parseCompleteFile(data map[string]interface{}, filename string, frontmatter string) ([]byte, error) { mainContent := "" if _, ok := data["content"]; ok { @@ -127,16 +125,16 @@ func parseCompleteFile(data map[string]interface{}, filename string, frontmatter data["date"] = data["date"].(string) + ":00" } - front, code, err := parseFrontMatter(data, frontmatter) + front, err := parseFrontMatter(data, frontmatter) if err != nil { fmt.Println(frontmatter) - return []byte{}, code, err + return []byte{}, err } // Generates the final file f := new(bytes.Buffer) f.Write(front) f.Write([]byte(mainContent)) - return f.Bytes(), http.StatusOK, nil + return f.Bytes(), nil } From 5fce287cd23a5318da4bae441180c54861cfea27 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 22 Oct 2016 12:07:19 +0100 Subject: [PATCH 21/28] move some stuff --- file/info.go | 30 ++++++++++++++++++++++-- frontmatter/frontmatter.go | 6 ++--- {file => handlers}/editor.go | 31 +++---------------------- handlers/listing.go | 4 ++-- handlers/single.go | 6 ++--- page/page.go | 4 ++-- utils/{ => errors}/errors.go | 2 +- utils/{ => variables}/types.go | 2 +- utils/{ => variables}/variables.go | 2 +- utils/{ => variables}/variables_test.go | 2 +- 10 files changed, 45 insertions(+), 44 deletions(-) rename {file => handlers}/editor.go (78%) rename utils/{ => errors}/errors.go (95%) rename utils/{ => variables}/types.go (94%) rename utils/{ => variables}/variables.go (98%) rename utils/{ => variables}/variables_test.go (97%) diff --git a/file/info.go b/file/info.go index 9ffaa66d..18060e38 100644 --- a/file/info.go +++ b/file/info.go @@ -10,7 +10,7 @@ import ( humanize "github.com/dustin/go-humanize" "github.com/hacdias/caddy-filemanager/config" - "github.com/hacdias/caddy-filemanager/utils" + "github.com/hacdias/caddy-filemanager/utils/errors" ) // Info contains the information about a particular file or directory @@ -41,7 +41,7 @@ func GetInfo(url *url.URL, c *config.Config, u *config.User) (*Info, int, error) i.FileInfo, err = os.Stat(i.Path) if err != nil { - return i, utils.ErrorToHTTPCode(err, false), err + return i, errors.ErrorToHTTPCode(err, false), err } return i, 0, nil @@ -74,6 +74,32 @@ 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" diff --git a/frontmatter/frontmatter.go b/frontmatter/frontmatter.go index 18edbc5b..d50df10e 100644 --- a/frontmatter/frontmatter.go +++ b/frontmatter/frontmatter.go @@ -13,7 +13,7 @@ import ( "gopkg.in/yaml.v2" "github.com/BurntSushi/toml" - "github.com/hacdias/caddy-filemanager/utils" + "github.com/hacdias/caddy-filemanager/utils/variables" "github.com/spf13/cast" ) @@ -126,9 +126,9 @@ func rawToPretty(config interface{}, parent *Block) *Content { } for name, element := range cnf { - if utils.IsMap(element) { + if variables.IsMap(element) { objects = append(objects, handleObjects(element, parent, name)) - } else if utils.IsSlice(element) { + } else if variables.IsSlice(element) { arrays = append(arrays, handleArrays(element, parent, name)) } else { if name == "title" && parent.Name == mainName { diff --git a/file/editor.go b/handlers/editor.go similarity index 78% rename from file/editor.go rename to handlers/editor.go index e0cd3ba8..f9343538 100644 --- a/file/editor.go +++ b/handlers/editor.go @@ -1,10 +1,11 @@ -package file +package handlers import ( "bytes" "path/filepath" "strings" + "github.com/hacdias/caddy-filemanager/file" "github.com/hacdias/caddy-filemanager/frontmatter" "github.com/spf13/hugo/parser" ) @@ -18,7 +19,7 @@ type Editor struct { } // GetEditor gets the editor based on a FileInfo struct -func (i *Info) GetEditor() (*Editor, error) { +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()), ".") @@ -81,29 +82,3 @@ func (i *Info) GetEditor() (*Editor, error) { return editor, nil } - -// 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 -} diff --git a/handlers/listing.go b/handlers/listing.go index 13b36d33..28d022db 100644 --- a/handlers/listing.go +++ b/handlers/listing.go @@ -9,7 +9,7 @@ import ( "github.com/hacdias/caddy-filemanager/config" "github.com/hacdias/caddy-filemanager/file" "github.com/hacdias/caddy-filemanager/page" - "github.com/hacdias/caddy-filemanager/utils" + "github.com/hacdias/caddy-filemanager/utils/errors" "github.com/mholt/caddy/caddyhttp/httpserver" ) @@ -20,7 +20,7 @@ func ServeListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *c // Loads the content of the directory listing, err := file.GetListing(u, i.VirtualPath, r.URL.Path) if err != nil { - return utils.ErrorToHTTPCode(err, true), err + return errors.ErrorToHTTPCode(err, true), err } listing.Context = httpserver.Context{ diff --git a/handlers/single.go b/handlers/single.go index 93dd3b85..2c22f885 100644 --- a/handlers/single.go +++ b/handlers/single.go @@ -6,7 +6,7 @@ import ( "github.com/hacdias/caddy-filemanager/config" "github.com/hacdias/caddy-filemanager/file" "github.com/hacdias/caddy-filemanager/page" - "github.com/hacdias/caddy-filemanager/utils" + "github.com/hacdias/caddy-filemanager/utils/errors" ) // ServeSingle serves a single file in an editor (if it is editable), shows the @@ -14,7 +14,7 @@ import ( func ServeSingle(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User, i *file.Info) (int, error) { err := i.Read() if err != nil { - return utils.ErrorToHTTPCode(err, true), err + return errors.ErrorToHTTPCode(err, true), err } p := &page.Page{ @@ -29,7 +29,7 @@ func ServeSingle(w http.ResponseWriter, r *http.Request, c *config.Config, u *co } if i.CanBeEdited() && u.AllowEdit { - p.Data, err = i.GetEditor() + p.Data, err = GetEditor(i) if err != nil { return http.StatusInternalServerError, err } diff --git a/page/page.go b/page/page.go index 2b2ca46e..dd7c60a3 100644 --- a/page/page.go +++ b/page/page.go @@ -11,7 +11,7 @@ import ( "github.com/hacdias/caddy-filemanager/assets" "github.com/hacdias/caddy-filemanager/config" - "github.com/hacdias/caddy-filemanager/utils" + "github.com/hacdias/caddy-filemanager/utils/variables" ) // Page contains the informations and functions needed to show the Page @@ -81,7 +81,7 @@ func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro // Create the functions map, then the template, check for erros and // execute the template if there aren't errors functions := template.FuncMap{ - "Defined": utils.Defined, + "Defined": variables.Defined, "CSS": func(s string) template.CSS { return template.CSS(s) }, diff --git a/utils/errors.go b/utils/errors/errors.go similarity index 95% rename from utils/errors.go rename to utils/errors/errors.go index 35841d85..ad4a2743 100644 --- a/utils/errors.go +++ b/utils/errors/errors.go @@ -1,4 +1,4 @@ -package utils +package errors import ( "net/http" diff --git a/utils/types.go b/utils/variables/types.go similarity index 94% rename from utils/types.go rename to utils/variables/types.go index 7e6b408b..ee43dad3 100644 --- a/utils/types.go +++ b/utils/variables/types.go @@ -1,4 +1,4 @@ -package utils +package variables import "reflect" diff --git a/utils/variables.go b/utils/variables/variables.go similarity index 98% rename from utils/variables.go rename to utils/variables/variables.go index 28f8383a..7a0168b4 100644 --- a/utils/variables.go +++ b/utils/variables/variables.go @@ -1,4 +1,4 @@ -package utils +package variables import ( "errors" diff --git a/utils/variables_test.go b/utils/variables/variables_test.go similarity index 97% rename from utils/variables_test.go rename to utils/variables/variables_test.go index 7122478d..ec76d459 100644 --- a/utils/variables_test.go +++ b/utils/variables/variables_test.go @@ -1,4 +1,4 @@ -package utils +package variables import "testing" From 17eff65b0dd49b4c74004bfb659f5d078d8af0af Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 22 Oct 2016 12:11:08 +0100 Subject: [PATCH 22/28] move assets --- {assets/embed => _embed}/public/css/.jsbeautifyrc | 0 {assets/embed => _embed}/public/css/styles.css | 0 {assets/embed => _embed}/public/js/.jsbeautifyrc | 0 {assets/embed => _embed}/public/js/application.js | 0 {assets/embed => _embed}/public/js/form2js.js | 0 {assets/embed => _embed}/templates/.jsbeautifyrc | 0 {assets/embed => _embed}/templates/actions.tmpl | 0 {assets/embed => _embed}/templates/base.tmpl | 0 {assets/embed => _embed}/templates/editor.tmpl | 0 {assets/embed => _embed}/templates/frontmatter.tmpl | 0 {assets/embed => _embed}/templates/listing.tmpl | 0 {assets/embed => _embed}/templates/minimal.tmpl | 0 {assets/embed => _embed}/templates/single.tmpl | 0 filemanager.go | 2 +- 14 files changed, 1 insertion(+), 1 deletion(-) rename {assets/embed => _embed}/public/css/.jsbeautifyrc (100%) rename {assets/embed => _embed}/public/css/styles.css (100%) rename {assets/embed => _embed}/public/js/.jsbeautifyrc (100%) rename {assets/embed => _embed}/public/js/application.js (100%) rename {assets/embed => _embed}/public/js/form2js.js (100%) rename {assets/embed => _embed}/templates/.jsbeautifyrc (100%) rename {assets/embed => _embed}/templates/actions.tmpl (100%) rename {assets/embed => _embed}/templates/base.tmpl (100%) rename {assets/embed => _embed}/templates/editor.tmpl (100%) rename {assets/embed => _embed}/templates/frontmatter.tmpl (100%) rename {assets/embed => _embed}/templates/listing.tmpl (100%) rename {assets/embed => _embed}/templates/minimal.tmpl (100%) rename {assets/embed => _embed}/templates/single.tmpl (100%) 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 100% rename from assets/embed/public/css/styles.css rename to _embed/public/css/styles.css 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 100% rename from assets/embed/public/js/application.js rename to _embed/public/js/application.js 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/assets/embed/templates/actions.tmpl b/_embed/templates/actions.tmpl similarity index 100% rename from assets/embed/templates/actions.tmpl rename to _embed/templates/actions.tmpl diff --git a/assets/embed/templates/base.tmpl b/_embed/templates/base.tmpl similarity index 100% rename from assets/embed/templates/base.tmpl rename to _embed/templates/base.tmpl 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 100% rename from assets/embed/templates/listing.tmpl rename to _embed/templates/listing.tmpl 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/assets/embed/templates/single.tmpl b/_embed/templates/single.tmpl similarity index 100% rename from assets/embed/templates/single.tmpl rename to _embed/templates/single.tmpl diff --git a/filemanager.go b/filemanager.go index d53ae279..0e0d2ebb 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 From 3430488b583a20f2fbf5147295680c81d813d70c Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 22 Oct 2016 13:09:38 +0100 Subject: [PATCH 23/28] Add more info to file --- file/info.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/file/info.go b/file/info.go index 18060e38..7fc717c3 100644 --- a/file/info.go +++ b/file/info.go @@ -16,6 +16,7 @@ import ( // Info contains the information about a particular file or directory type Info struct { os.FileInfo + File *os.File URL string Path string // Relative path to Caddyfile VirtualPath string // Relative path to u.FileSystem @@ -39,11 +40,24 @@ func GetInfo(url *url.URL, c *config.Config, u *config.User) (*Info, int, error) i.Path = strings.Replace(i.Path, "\\", "/", -1) i.Path = filepath.Clean(i.Path) - i.FileInfo, err = os.Stat(i.Path) + i.File, err = os.Open(i.Path) if err != nil { return i, errors.ErrorToHTTPCode(err, false), err } + i.FileInfo, err = i.File.Stat() + if err != nil { + return i, errors.ErrorToHTTPCode(err, true), err + } + + p := make([]byte, 512) + _, err = i.File.Read(p) + if err != nil { + return i, errors.ErrorToHTTPCode(err, false), err + } + + i.Mimetype = http.DetectContentType(p) + i.Type = simplifyMediaType(i.Mimetype) return i, 0, nil } @@ -53,6 +67,7 @@ func (i *Info) Read() error { if err != nil { return err } + i.Mimetype = http.DetectContentType(i.Content) i.Type = simplifyMediaType(i.Mimetype) return nil From d96bbff55082ce4f6c94b11ca0a990d4e43efb89 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 22 Oct 2016 13:46:10 +0100 Subject: [PATCH 24/28] support for video and improvements --- _embed/public/css/styles.css | 4 ++++ _embed/templates/single.tmpl | 6 +++++- file/info.go | 32 ++++++++++++++++++-------------- handlers/single.go | 11 +++++++++-- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/_embed/public/css/styles.css b/_embed/public/css/styles.css index 644d9cec..0db84589 100644 --- a/_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 diff --git a/_embed/templates/single.tmpl b/_embed/templates/single.tmpl index f360663e..e070dcd8 100644 --- a/_embed/templates/single.tmpl +++ b/_embed/templates/single.tmpl @@ -6,7 +6,11 @@ {{ else if eq .Type "audio" }} {{ else if eq .Type "video" }} - + {{ else if eq .Type "blob" }} Download {{ else}} diff --git a/file/info.go b/file/info.go index 7fc717c3..df0b40ea 100644 --- a/file/info.go +++ b/file/info.go @@ -16,7 +16,6 @@ import ( // Info contains the information about a particular file or directory type Info struct { os.FileInfo - File *os.File URL string Path string // Relative path to Caddyfile VirtualPath string // Relative path to u.FileSystem @@ -40,36 +39,41 @@ func GetInfo(url *url.URL, c *config.Config, u *config.User) (*Info, int, error) i.Path = strings.Replace(i.Path, "\\", "/", -1) i.Path = filepath.Clean(i.Path) - i.File, err = os.Open(i.Path) - if err != nil { - return i, errors.ErrorToHTTPCode(err, false), err - } - - i.FileInfo, err = i.File.Stat() + i.FileInfo, err = os.Stat(i.Path) if err != nil { return i, errors.ErrorToHTTPCode(err, true), err } - p := make([]byte, 512) - _, err = i.File.Read(p) + 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 { + file, err := os.Open(i.Path) if err != nil { - return i, errors.ErrorToHTTPCode(err, false), err + return err + } + defer file.Close() + + p := make([]byte, 512) + _, err = file.Read(p) + if err != nil { + return err } i.Mimetype = http.DetectContentType(p) i.Type = simplifyMediaType(i.Mimetype) - return i, 0, nil + return nil } +// Reads the file. func (i *Info) Read() error { var err error i.Content, err = ioutil.ReadFile(i.Path) if err != nil { return err } - - i.Mimetype = http.DetectContentType(i.Content) - i.Type = simplifyMediaType(i.Mimetype) return nil } diff --git a/handlers/single.go b/handlers/single.go index 2c22f885..85d0ff5a 100644 --- a/handlers/single.go +++ b/handlers/single.go @@ -12,11 +12,18 @@ import ( // 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) { - err := i.Read() - if err != nil { + 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(), From 4e7c730faf40f3ee45c0f2d9baaa506df7a6d863 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 22 Oct 2016 15:45:45 +0100 Subject: [PATCH 25/28] download improvements; CSRF token commented --- filemanager.go | 47 ++++++++++++++++++++++++++++++++++++++------ handlers/download.go | 21 ++++++++++++++++---- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/filemanager.go b/filemanager.go index 0e0d2ebb..8827e772 100644 --- a/filemanager.go +++ b/filemanager.go @@ -43,6 +43,10 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err return f.Next.ServeHTTP(w, r) } + 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 @@ -65,6 +69,10 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err // 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 @@ -105,6 +113,36 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err } 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 { @@ -121,9 +159,6 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err return 0, nil } - // Generate anti security token. - c.GenerateToken() - switch { case r.URL.Query().Get("download") != "": code, err = handlers.Download(w, r, c, fi) @@ -146,9 +181,9 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, 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 - } + // if !c.CheckToken(r) { + // return http.StatusForbidden, nil + // } // VCS commands. if r.Header.Get("Command") != "" { diff --git a/handlers/download.go b/handlers/download.go index f4fc95a2..4b064499 100644 --- a/handlers/download.go +++ b/handlers/download.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "github.com/hacdias/caddy-filemanager/config" "github.com/hacdias/caddy-filemanager/file" @@ -23,6 +24,18 @@ func Download(w http.ResponseWriter, r *http.Request, c *config.Config, i *file. 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" } @@ -44,13 +57,13 @@ func Download(w http.ResponseWriter, r *http.Request, c *config.Config, i *file. switch query { case "zip": - extension, err = ".zip", archiver.Zip.Make(tempfile, []string{i.Path}) + extension, err = ".zip", archiver.Zip.Make(tempfile, files) case "tar": - extension, err = ".tar", archiver.Tar.Make(tempfile, []string{i.Path}) + extension, err = ".tar", archiver.Tar.Make(tempfile, files) case "targz": - extension, err = ".tar.gz", archiver.TarGz.Make(tempfile, []string{i.Path}) + extension, err = ".tar.gz", archiver.TarGz.Make(tempfile, files) case "tarbz2": - extension, err = ".tar.bz2", archiver.TarBz2.Make(tempfile, []string{i.Path}) + extension, err = ".tar.bz2", archiver.TarBz2.Make(tempfile, files) default: return http.StatusNotImplemented, nil } From 82cc6e77bdd7ad75e8f5195946b159ae35107e07 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 22 Oct 2016 16:37:20 +0100 Subject: [PATCH 26/28] Improve Mimetype detecting --- file/info.go | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/file/info.go b/file/info.go index df0b40ea..bec3901e 100644 --- a/file/info.go +++ b/file/info.go @@ -2,6 +2,7 @@ package file import ( "io/ioutil" + "mime" "net/http" "net/url" "os" @@ -50,25 +51,27 @@ func GetInfo(url *url.URL, c *config.Config, u *config.User) (*Info, int, error) // RetrieveFileType obtains the mimetype and a simplified internal Type // using the first 512 bytes from the file. func (i *Info) RetrieveFileType() error { - file, err := os.Open(i.Path) - if err != nil { - return err - } - defer file.Close() + i.Mimetype = mime.TypeByExtension(filepath.Ext(i.Name())) - p := make([]byte, 512) - _, err = file.Read(p) - if err != nil { - return err + if i.Mimetype == "" { + err := i.Read() + if err != nil { + return err + } + + i.Mimetype = http.DetectContentType(i.Content) } - i.Mimetype = http.DetectContentType(p) 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 { From cb72d8c6c2999c981c95d4fe3c19d3338e4cf580 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 25 Oct 2016 21:08:26 +0100 Subject: [PATCH 27/28] close #29 --- _embed/public/css/styles.css | 35 ++--- _embed/public/js/application.js | 37 +++--- _embed/templates/actions.tmpl | 42 +++--- _embed/templates/base.tmpl | 229 +++++++++++++++----------------- 4 files changed, 173 insertions(+), 170 deletions(-) diff --git a/_embed/public/css/styles.css b/_embed/public/css/styles.css index 0db84589..3368741b 100644 --- a/_embed/public/css/styles.css +++ b/_embed/public/css/styles.css @@ -264,7 +264,7 @@ textarea { body { font-family: 'Roboto', sans-serif; padding-top: 5em; - background-color: #fcfcfc; + background-color: #ffffff; text-rendering: optimizespeed; } @@ -473,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 { @@ -481,7 +483,9 @@ header h1 { } header a, -header a:hover { +header a:hover, +#toolbar a, +#toolbar a:hover { color: inherit; } @@ -680,8 +684,7 @@ header .only-side { display: none; } -header #prev:hover+.prev-links, -header .prev-links:hover { +.action:hover ul { display: flex; } @@ -689,9 +692,9 @@ header .prev-links:hover { border-radius: 0; } -header .prev-links { +.action ul { position: absolute; - top: 4em; + top: 3.1em; left: 0; color: #7d7d7d; list-style: none; @@ -703,37 +706,39 @@ header .prev-links { flex-direction: column-reverse; display: none; transition: .2s ease all; - min-width: 12em; + min-width: 3em; + z-index: 99999; } -header .prev-links:before { +.action ul:before { top: -16px; left: 1em; right: auto; border: 8px solid transparent; - border-bottom-color: #68efad; + border-bottom-color: #ffffff; content: ''; position: absolute; } -header .prev-links a { - padding: .5em; +.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; } @@ -1181,4 +1186,4 @@ i.spin { column-count: 1; column-gap: 0; } -} +} \ No newline at end of file diff --git a/_embed/public/js/application.js b/_embed/public/js/application.js index 12c73c70..4ba2ed56 100644 --- a/_embed/public/js/application.js +++ b/_embed/public/js/application.js @@ -151,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('/'); @@ -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'); @@ -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"); }); @@ -908,4 +909,4 @@ document.addEventListener("DOMContentLoaded", function(event) { } return false; -}); \ No newline at end of file +}); diff --git a/_embed/templates/actions.tmpl b/_embed/templates/actions.tmpl index 069598f2..a42d11d1 100644 --- a/_embed/templates/actions.tmpl +++ b/_embed/templates/actions.tmpl @@ -1,18 +1,28 @@ {{ define "actions" }} -
- open_in_new See raw -
- {{ if and .IsDir .User.AllowEdit }} -
- mode_edit -
- {{ end }} -
- file_download Download -
- {{ if .User.AllowEdit }} -
- delete Delete -
- {{ end }} +
+ 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 index 86f4e844..679795e7 100644 --- a/_embed/templates/base.tmpl +++ b/_embed/templates/base.tmpl @@ -1,139 +1,126 @@ -{{ $absURL := .Config.AbsoluteURL }} - - - {{.Name}} - - - - - - {{ if ne .User.StyleSheet "" }} - - {{ end }} - - -
-
- {{ $lnk := .PreviousLink }} - - - - {{ if ne $lnk ""}} - - {{ end }} - -
- menu -
- -

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

+{{ $absURL := .Config.AbsoluteURL }} + + {{.Name}} + + + + + {{ if ne .User.StyleSheet "" }} + {{ end }} -
- -
-
- {{ $lnk := .PreviousLink }} - {{ if ne $lnk ""}} - - {{ end }} + + +
+
+ {{ $lnk := .PreviousLink }} - {{ if ne $lnk ""}} - - {{ end }} +
+ menu +
-

- - File Manager - -

-
- - {{ if .IsDir}} - - {{ if .User.AllowCommands }} - - {{ end }} - -
- view_headline Switch view + {{ if ne .Name "/"}}

{{ .Name }}

{{ end }}
- {{ if .User.AllowNew }} -
- file_upload Upload + +
+
+ {{ $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 +
- {{ end }} - {{ else }} - {{ template "actions" . }} - {{ end }} +
+
-
- exit_to_app Logout -
-
-
-
- - {{ if .IsDir }} -
+ {{ if .IsDir }} +
-
- arrow_back -
-

- 0 - selected.

+
+ arrow_back +
+

+ 0 + selected.

- {{ template "actions" . }} + {{ template "actions" . }}
-
- {{ end }} +
+ {{ end }} -
- {{ template "content" . }} - {{ .Config.Token }} -
+
+ {{ template "content" . }} + {{ .Config.Token }} +
- + - - - - - - - {{ if .Config.HugoEnabled }}{{ end }} - + + + + + + + {{ if .Config.HugoEnabled }}{{ end }} + From 1f3d2100185bcee3bb20086b0df0893fb662547b Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 25 Oct 2016 21:13:25 +0100 Subject: [PATCH 28/28] don't display toolbar "selected" text when the device is small --- _embed/public/css/styles.css | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/_embed/public/css/styles.css b/_embed/public/css/styles.css index 3368741b..511378e5 100644 --- a/_embed/public/css/styles.css +++ b/_embed/public/css/styles.css @@ -473,8 +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); + border-bottom: 1px solid rgba(0,0,0,0.075); + box-shadow: 0 0 5px rgba(0,0,0,0.1); } header h1 { @@ -1186,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; + } +}