From 69081360928587fd0c246ac1a9153cc2d46e695b Mon Sep 17 00:00:00 2001 From: W-Mark Kubacki Date: Mon, 18 Apr 2016 21:01:47 +0200 Subject: [PATCH 1/7] browse: Split ServeHTTP into small specialized functions --- middleware/browse/browse.go | 202 ++++++++++++++++++------------- middleware/browse/browse_test.go | 2 +- 2 files changed, 118 insertions(+), 86 deletions(-) diff --git a/middleware/browse/browse.go b/middleware/browse/browse.go index 32e42dda..3a727d96 100644 --- a/middleware/browse/browse.go +++ b/middleware/browse/browse.go @@ -5,7 +5,6 @@ package browse import ( "bytes" "encoding/json" - "errors" "net/http" "net/url" "os" @@ -173,22 +172,20 @@ func (l Listing) applySort() { } } -func directoryListing(files []os.FileInfo, r *http.Request, canGoUp bool, root string, ignoreIndexes bool, vars interface{}) (Listing, error) { +func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string) (Listing, bool) { var ( fileinfos []FileInfo dirCount, fileCount int - urlPath = r.URL.Path + hasIndexFile bool ) for _, f := range files { name := f.Name() - // Directory is not browsable if it contains index file - if !ignoreIndexes { - for _, indexName := range middleware.IndexPages { - if name == indexName { - return Listing{}, errors.New("Directory contains index file, not browsable!") - } + for _, indexName := range middleware.IndexPages { + if name == indexName { + hasIndexFile = true + break } } @@ -218,16 +215,11 @@ func directoryListing(files []os.FileInfo, r *http.Request, canGoUp bool, root s Items: fileinfos, NumDirs: dirCount, NumFiles: fileCount, - Context: middleware.Context{ - Root: http.Dir(root), - Req: r, - URL: r.URL, - }, - User: vars, - }, nil + }, hasIndexFile } -// ServeHTTP implements the middleware.Handler interface. +// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. +// If so, control is handed over to ServeListing. func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { var bc *Config // See if there's a browse configuration to match the path @@ -274,21 +266,69 @@ inScope: return 0, nil } + return b.ServeListing(w, r, requestedFilepath, bc) +} + +func (b Browse) loadDirectoryContents(requestedFilepath, urlPath string) (*Listing, bool, error) { // Load directory contents file, err := os.Open(requestedFilepath) if err != nil { - switch { - case os.IsPermission(err): - return http.StatusForbidden, err - case os.IsExist(err): - return http.StatusGone, err - default: - return http.StatusInternalServerError, err - } + return nil, false, err } defer file.Close() files, err := file.Readdir(-1) + if err != nil { + return nil, false, err + } + + // Determine if user can browse up another folder + var canGoUp bool + curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/")) + for _, other := range b.Configs { + if strings.HasPrefix(curPathDir, other.PathScope) { + canGoUp = true + break + } + } + + // Assemble listing of directory contents + listing, hasIndex := directoryListing(files, canGoUp, urlPath) + + return &listing, hasIndex, nil +} + +// handleSortOrder gets and stores for a Listing the 'sort' and 'order'. +// +// This sets Cookies. +func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (string, string) { + sort, order := r.URL.Query().Get("sort"), r.URL.Query().Get("order") + + // If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies + if sort == "" { + sort = "name" + if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { + sort = sortCookie.Value + } + } else { + http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil}) + } + + if order == "" { + order = "asc" + if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { + order = orderCookie.Value + } + } else { + http.SetCookie(w, &http.Cookie{Name: "order", Value: order, Path: scope, Secure: r.TLS != nil}) + } + + return sort, order +} + +// ServeListing returns a formatted view of 'requestedFilepath' contents'. +func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFilepath string, bc *Config) (int, error) { + listing, containsIndex, err := b.loadDirectoryContents(requestedFilepath, r.URL.Path) if err != nil { switch { case os.IsPermission(err): @@ -299,83 +339,40 @@ inScope: return http.StatusInternalServerError, err } } - - // Determine if user can browse up another folder - var canGoUp bool - curPath := strings.TrimSuffix(r.URL.Path, "/") - for _, other := range b.Configs { - if strings.HasPrefix(path.Dir(curPath), other.PathScope) { - canGoUp = true - break - } - } - // Assemble listing of directory contents - listing, err := directoryListing(files, r, canGoUp, b.Root, b.IgnoreIndexes, bc.Variables) - if err != nil { // directory isn't browsable + if containsIndex && !b.IgnoreIndexes { // directory isn't browsable return b.Next.ServeHTTP(w, r) } + listing.Context = middleware.Context{ + Root: http.Dir(b.Root), + Req: r, + URL: r.URL, + } + listing.User = bc.Variables // Copy the query values into the Listing struct - listing.Sort, listing.Order = r.URL.Query().Get("sort"), r.URL.Query().Get("order") - - // If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies - if listing.Sort == "" { - listing.Sort = "name" - if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { - listing.Sort = sortCookie.Value - } - } else { // Save the query value of 'sort' and 'order' as cookies. - http.SetCookie(w, &http.Cookie{Name: "sort", Value: listing.Sort, Path: bc.PathScope, Secure: r.TLS != nil}) - http.SetCookie(w, &http.Cookie{Name: "order", Value: listing.Order, Path: bc.PathScope, Secure: r.TLS != nil}) - } - - if listing.Order == "" { - listing.Order = "asc" - if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { - listing.Order = orderCookie.Value - } - } else { - http.SetCookie(w, &http.Cookie{Name: "order", Value: listing.Order, Path: bc.PathScope, Secure: r.TLS != nil}) - } + listing.Sort, listing.Order = b.handleSortOrder(w, r, bc.PathScope) listing.applySort() - var buf bytes.Buffer - // Check if we should provide json - acceptHeader := strings.Join(r.Header["Accept"], ",") - if strings.Contains(strings.ToLower(acceptHeader), "application/json") { - var marsh []byte - // Check if we are limited + var buf *bytes.Buffer + acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ",")) + switch { + case strings.Contains(acceptHeader, "application/json"): + var limit int if limitQuery := r.URL.Query().Get("limit"); limitQuery != "" { - limit, err := strconv.Atoi(limitQuery) + limit, err = strconv.Atoi(limitQuery) if err != nil { // if the 'limit' query can't be interpreted as a number, return err return http.StatusBadRequest, err } - // if `limit` is equal or less than len(listing.Items) and bigger than 0, list them - if limit <= len(listing.Items) && limit > 0 { - marsh, err = json.Marshal(listing.Items[:limit]) - } else { // if the 'limit' query is empty, or has the wrong value, list everything - marsh, err = json.Marshal(listing.Items) - } - if err != nil { - return http.StatusInternalServerError, err - } - } else { // There's no 'limit' query; list them all - marsh, err = json.Marshal(listing.Items) - if err != nil { - return http.StatusInternalServerError, err - } } - // Write the marshaled json to buf - if _, err = buf.Write(marsh); err != nil { + if buf, err = b.formatAsJSON(listing, bc, limit); err != nil { return http.StatusInternalServerError, err } w.Header().Set("Content-Type", "application/json; charset=utf-8") - } else { // There's no 'application/json' in the 'Accept' header; browse normally - err = bc.Template.Execute(&buf, listing) - if err != nil { + default: // There's no 'application/json' in the 'Accept' header; browse normally + if buf, err = b.formatAsHTML(listing, bc); err != nil { return http.StatusInternalServerError, err } w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -386,3 +383,38 @@ inScope: return http.StatusOK, nil } + +func (b Browse) formatAsJSON(listing *Listing, bc *Config, limit int) (*bytes.Buffer, error) { + buf := new(bytes.Buffer) + var marsh []byte + var err error + + // Check if we are limited + if limit > 0 { + // if `limit` is equal or less than len(listing.Items) and bigger than 0, list them + if limit <= len(listing.Items) { + marsh, err = json.Marshal(listing.Items[:limit]) + } else { // if the 'limit' query is empty, or has the wrong value, list everything + marsh, err = json.Marshal(listing.Items) + } + if err != nil { + return nil, err + } + } else { // There's no 'limit' query; list them all + marsh, err = json.Marshal(listing.Items) + if err != nil { + return nil, err + } + } + + // Write the marshaled json to buf + _, err = buf.Write(marsh) + + return buf, err +} + +func (b Browse) formatAsHTML(listing *Listing, bc *Config) (*bytes.Buffer, error) { + buf := new(bytes.Buffer) + err := bc.Template.Execute(buf, listing) + return buf, err +} diff --git a/middleware/browse/browse_test.go b/middleware/browse/browse_test.go index 56af8454..c8792524 100644 --- a/middleware/browse/browse_test.go +++ b/middleware/browse/browse_test.go @@ -315,7 +315,7 @@ func TestBrowseJson(t *testing.T) { code, err := b.ServeHTTP(rec, req) if code != http.StatusOK { - t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code) + t.Fatalf("In test %d: Wrong status, expected %d, got %d", i, http.StatusOK, code) } if rec.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { t.Fatalf("Expected Content type to be application/json; charset=utf-8, but got %s ", rec.HeaderMap.Get("Content-Type")) From 1d38d113f8e760872c22d10b67402f20d159661c Mon Sep 17 00:00:00 2001 From: W-Mark Kubacki Date: Mon, 18 Apr 2016 21:51:37 +0200 Subject: [PATCH 2/7] browse: Move predicate 'limit' to ServeListing This keeps the interface of all available formatters honest, and allows for truncated listings all formats. --- caddy/setup/browse.go | 21 +++++----- middleware/browse/browse.go | 78 ++++++++++++++++++------------------- 2 files changed, 49 insertions(+), 50 deletions(-) diff --git a/caddy/setup/browse.go b/caddy/setup/browse.go index 28cb2582..ff921b9b 100644 --- a/caddy/setup/browse.go +++ b/caddy/setup/browse.go @@ -309,6 +309,9 @@ footer {
{{.NumDirs}} director{{if eq 1 .NumDirs}}y{{else}}ies{{end}} {{.NumFiles}} file{{if ne 1 .NumFiles}}s{{end}} + {{- if ne 0 .ItemsLimitedTo}} + (of which only {{.ItemsLimitedTo}} are displayed) + {{- end}}
@@ -316,29 +319,29 @@ footer { {{if and (eq .Sort "name") (ne .Order "desc")}} - Name + Name {{else if and (eq .Sort "name") (ne .Order "asc")}} - Name + Name {{else}} - Name + Name {{end}} {{if and (eq .Sort "size") (ne .Order "desc")}} - Size + Size {{else if and (eq .Sort "size") (ne .Order "asc")}} - Size + Size {{else}} - Size + Size {{end}} {{if and (eq .Sort "time") (ne .Order "desc")}} - Modified + Modified {{else if and (eq .Sort "time") (ne .Order "asc")}} - Modified + Modified {{else}} - Modified + Modified {{end}} diff --git a/middleware/browse/browse.go b/middleware/browse/browse.go index 3a727d96..e350c07a 100644 --- a/middleware/browse/browse.go +++ b/middleware/browse/browse.go @@ -62,6 +62,9 @@ type Listing struct { // And which order Order string + // If ≠0 then Items have been limited to that many elements + ItemsLimitedTo int + // Optional custom variables for use in browse templates User interface{} @@ -298,32 +301,42 @@ func (b Browse) loadDirectoryContents(requestedFilepath, urlPath string) (*Listi return &listing, hasIndex, nil } -// handleSortOrder gets and stores for a Listing the 'sort' and 'order'. +// handleSortOrder gets and stores for a Listing the 'sort' and 'order', +// and reads 'limit' if given. The latter is 0 if not given. // // This sets Cookies. -func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (string, string) { - sort, order := r.URL.Query().Get("sort"), r.URL.Query().Get("order") +func (b Browse) 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 - if sort == "" { + switch sort { + case "": sort = "name" if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { sort = sortCookie.Value } - } else { + case "name", "size", "type": http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil}) } - if order == "" { + switch order { + case "": order = "asc" if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { order = orderCookie.Value } - } else { + case "asc", "desc": http.SetCookie(w, &http.Cookie{Name: "order", Value: order, Path: scope, Secure: r.TLS != nil}) } - return sort, order + 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 } // ServeListing returns a formatted view of 'requestedFilepath' contents'. @@ -350,23 +363,24 @@ func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFi listing.User = bc.Variables // Copy the query values into the Listing struct - listing.Sort, listing.Order = b.handleSortOrder(w, r, bc.PathScope) + var limit int + listing.Sort, listing.Order, limit, err = b.handleSortOrder(w, r, bc.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 + } + var buf *bytes.Buffer acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ",")) switch { case strings.Contains(acceptHeader, "application/json"): - var limit int - if limitQuery := r.URL.Query().Get("limit"); limitQuery != "" { - limit, err = strconv.Atoi(limitQuery) - if err != nil { // if the 'limit' query can't be interpreted as a number, return err - return http.StatusBadRequest, err - } - } - - if buf, err = b.formatAsJSON(listing, bc, limit); err != nil { + if buf, err = b.formatAsJSON(listing, bc); err != nil { return http.StatusInternalServerError, err } w.Header().Set("Content-Type", "application/json; charset=utf-8") @@ -384,32 +398,14 @@ func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFi return http.StatusOK, nil } -func (b Browse) formatAsJSON(listing *Listing, bc *Config, limit int) (*bytes.Buffer, error) { - buf := new(bytes.Buffer) - var marsh []byte - var err error - - // Check if we are limited - if limit > 0 { - // if `limit` is equal or less than len(listing.Items) and bigger than 0, list them - if limit <= len(listing.Items) { - marsh, err = json.Marshal(listing.Items[:limit]) - } else { // if the 'limit' query is empty, or has the wrong value, list everything - marsh, err = json.Marshal(listing.Items) - } - if err != nil { - return nil, err - } - } else { // There's no 'limit' query; list them all - marsh, err = json.Marshal(listing.Items) - if err != nil { - return nil, err - } +func (b Browse) formatAsJSON(listing *Listing, bc *Config) (*bytes.Buffer, error) { + marsh, err := json.Marshal(listing.Items) + if err != nil { + return nil, err } - // Write the marshaled json to buf + buf := new(bytes.Buffer) _, err = buf.Write(marsh) - return buf, err } From 239f6825f76d3046198ab556b1851e40422c9558 Mon Sep 17 00:00:00 2001 From: W-Mark Kubacki Date: Mon, 18 Apr 2016 23:05:08 +0200 Subject: [PATCH 3/7] browse: When sorting by size, offset directories Assigns negative sizes to directories in order to have them listed reliably before any zero-sized files. That order is what most users expect when sorting by size. As side effect directories will appear before files on all filesystem implementations. To give an example: before this change directories had a size of 4 KiB when using Linux with ext4 or tmpfs, and with ZFS a size resembling an estimation of the number of leaves within said directory. --- middleware/browse/browse.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/middleware/browse/browse.go b/middleware/browse/browse.go index e350c07a..a21b2304 100644 --- a/middleware/browse/browse.go +++ b/middleware/browse/browse.go @@ -135,9 +135,20 @@ func (l byName) Less(i, j int) bool { } // 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] } -func (l bySize) Less(i, j int) bool { return l.Items[i].Size < l.Items[j].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) } From cc6aa6b54bd560527c18524b803a280c56cb88e7 Mon Sep 17 00:00:00 2001 From: W-Mark Kubacki Date: Mon, 18 Apr 2016 22:47:53 +0200 Subject: [PATCH 4/7] browse: Remove whitespace from template's output, annotate output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a surplus — next to "go up". Identifies the preamble as the table's summary. Emits filesizes in bytes, which can be consumed by any browser-side scripts or utilized in sorting when the table is copy-and-pasted into a spreadsheet software. Uses