From 68add78230894fe32d14e176d9236390081fe371 Mon Sep 17 00:00:00 2001
From: pyed <iabdulelah@gmail.com>
Date: Mon, 15 Jun 2015 05:02:10 +0300
Subject: [PATCH] Implement sorting functionality for "Browse"

---
 config/setup/browse.go           |  30 +++++-
 middleware/browse/browse.go      | 157 ++++++++++++++++++++++---------
 middleware/browse/browse_test.go |  96 +++++++++++++++++++
 3 files changed, 238 insertions(+), 45 deletions(-)
 create mode 100644 middleware/browse/browse_test.go

diff --git a/config/setup/browse.go b/config/setup/browse.go
index ca1ad40eb..bf5a3fe96 100644
--- a/config/setup/browse.go
+++ b/config/setup/browse.go
@@ -202,9 +202,33 @@ th {
 		<main>
 			<table>
 				<tr>
-					<th>Name</th>
-					<th>Size</th>
-					<th class="hideable">Modified</th>
+					<th>
+						{{if and (eq .Sort "name") (ne .Order "desc")}}
+						<a href="?sort=name&order=desc">Name&#8595;</a>
+						{{else if and (eq .Sort "name") (ne .Order "asc")}}
+						<a href="?sort=name&order=asc">Name&#8593;</a>
+						{{else}}
+						<a href="?sort=name&order=asc">Name</a>
+						{{end}}
+					</th>
+					<th>
+						{{if and (eq .Sort "size") (ne .Order "desc")}}
+						<a href="?sort=size&order=desc">Size&#8595;</a>
+						{{else if and (eq .Sort "size") (ne .Order "asc")}}
+						<a href="?sort=size&order=asc">Size&#8593;</a>
+						{{else}}
+						<a href="?sort=size&order=asc">Size</a>
+						{{end}}
+					</th>
+					<th class="hideable">
+						{{if and (eq .Sort "time") (ne .Order "desc")}}
+						<a href="?sort=time&order=desc">Modified&#8595;</a>
+						{{else if and (eq .Sort "time") (ne .Order "asc")}}
+						<a href="?sort=time&order=asc">Modified&#8593;</a>
+						{{else}}
+						<a href="?sort=time&order=asc">Modified</a>
+						{{end}}
+					</th>
 				</tr>
 				{{range .Items}}
 				<tr>
diff --git a/middleware/browse/browse.go b/middleware/browse/browse.go
index ea2ff52c8..c0090acd6 100644
--- a/middleware/browse/browse.go
+++ b/middleware/browse/browse.go
@@ -4,11 +4,13 @@ package browse
 
 import (
 	"bytes"
+	"errors"
 	"html/template"
 	"net/http"
 	"net/url"
 	"os"
 	"path"
+	"sort"
 	"strings"
 	"time"
 
@@ -43,6 +45,12 @@ type Listing struct {
 
 	// The items (files and folders) in the path
 	Items []FileInfo
+
+	// Which sorting order is used
+	Sort string
+
+	// And which order
+	Order string
 }
 
 // FileInfo is the info about a particular file or directory
@@ -55,6 +63,61 @@ type FileInfo struct {
 	Mode    os.FileMode
 }
 
+// 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 {
+	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] }
+func (l bySize) Less(i, j int) bool { return l.Items[i].Size < l.Items[j].Size }
+
+// 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.Unix() < l.Items[j].ModTime.Unix() }
+
+// 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:
+			// If not one of the above, do nothing
+			return
+		}
+	}
+}
+
 // HumanSize returns the size of the file as a human-readable string.
 func (fi FileInfo) HumanSize() string {
 	return humanize.Bytes(uint64(fi.Size))
@@ -72,6 +135,42 @@ var IndexPages = []string{
 	"default.htm",
 }
 
+func directoryListing(files []os.FileInfo, urlPath string, canGoUp bool) (Listing, error) {
+	var fileinfos []FileInfo
+	for _, f := range files {
+		name := f.Name()
+
+		// Directory is not browsable if it contains index file
+		for _, indexName := range IndexPages {
+			if name == indexName {
+				return Listing{}, errors.New("Directory contains index file, not browsable!")
+			}
+		}
+
+		if f.IsDir() {
+			name += "/"
+		}
+
+		url := url.URL{Path: name}
+
+		fileinfos = append(fileinfos, FileInfo{
+			IsDir:   f.IsDir(),
+			Name:    f.Name(),
+			Size:    f.Size(),
+			URL:     url.String(),
+			ModTime: f.ModTime(),
+			Mode:    f.Mode(),
+		})
+	}
+
+	return Listing{
+		Name:    path.Base(urlPath),
+		Path:    urlPath,
+		CanGoUp: canGoUp,
+		Items:   fileinfos,
+	}, nil
+}
+
 // ServeHTTP implements the middleware.Handler interface.
 func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
 	filename := b.Root + r.URL.Path
@@ -113,42 +212,6 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
 			return http.StatusForbidden, err
 		}
 
-		// Assemble listing of directory contents
-		var fileinfos []FileInfo
-		var abort bool // we bail early if we find an index file
-		for _, f := range files {
-			name := f.Name()
-
-			// Directory is not browseable if it contains index file
-			for _, indexName := range IndexPages {
-				if name == indexName {
-					abort = true
-					break
-				}
-			}
-			if abort {
-				break
-			}
-
-			if f.IsDir() {
-				name += "/"
-			}
-			url := url.URL{Path: name}
-
-			fileinfos = append(fileinfos, FileInfo{
-				IsDir:   f.IsDir(),
-				Name:    f.Name(),
-				Size:    f.Size(),
-				URL:     url.String(),
-				ModTime: f.ModTime(),
-				Mode:    f.Mode(),
-			})
-		}
-		if abort {
-			// this dir has an index file, so not browsable
-			continue
-		}
-
 		// Determine if user can browse up another folder
 		var canGoUp bool
 		curPath := strings.TrimSuffix(r.URL.Path, "/")
@@ -158,14 +221,24 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
 				break
 			}
 		}
-
-		listing := Listing{
-			Name:    path.Base(r.URL.Path),
-			Path:    r.URL.Path,
-			CanGoUp: canGoUp,
-			Items:   fileinfos,
+		// Assemble listing of directory contents
+		listing, err := directoryListing(files, r.URL.Path, canGoUp)
+		if err != nil { // directory isn't browsable
+			continue
 		}
 
+		// Get the query vales and store them in the Listing struct
+		listing.Sort, listing.Order = r.URL.Query().Get("sort"), r.URL.Query().Get("order")
+
+		// If the query 'sort' is empty, default to "name" and "asc"
+		if listing.Sort == "" {
+			listing.Sort = "name"
+			listing.Order = "asc"
+		}
+
+		// Apply the sorting
+		listing.applySort()
+
 		var buf bytes.Buffer
 		err = bc.Template.Execute(&buf, listing)
 		if err != nil {
diff --git a/middleware/browse/browse_test.go b/middleware/browse/browse_test.go
new file mode 100644
index 000000000..33218a195
--- /dev/null
+++ b/middleware/browse/browse_test.go
@@ -0,0 +1,96 @@
+package browse
+
+import (
+	"sort"
+	"testing"
+	"time"
+)
+
+// "sort" package has "IsSorted" function, but no "IsReversed";
+func isReversed(data sort.Interface) bool {
+	n := data.Len()
+	for i := n - 1; i > 0; i-- {
+		if !data.Less(i, i-1) {
+			return false
+		}
+	}
+	return true
+}
+
+func TestSort(t *testing.T) {
+	// making up []fileInfo with bogus values;
+	// to be used to make up our "listing"
+	fileInfos := []FileInfo{
+		{
+			Name:    "fizz",
+			Size:    4,
+			ModTime: time.Now().AddDate(-1, 1, 0),
+		},
+		{
+			Name:    "buzz",
+			Size:    2,
+			ModTime: time.Now().AddDate(0, -3, 3),
+		},
+		{
+			Name:    "bazz",
+			Size:    1,
+			ModTime: time.Now().AddDate(0, -2, -23),
+		},
+		{
+			Name:    "jazz",
+			Size:    3,
+			ModTime: time.Now(),
+		},
+	}
+	listing := Listing{
+		Name:    "foobar",
+		Path:    "/fizz/buzz",
+		CanGoUp: false,
+		Items:   fileInfos,
+	}
+
+	// sort by name
+	listing.Sort = "name"
+	listing.applySort()
+	if !sort.IsSorted(byName(listing)) {
+		t.Errorf("The listing isn't name sorted: %v", listing.Items)
+	}
+
+	// sort by size
+	listing.Sort = "size"
+	listing.applySort()
+	if !sort.IsSorted(bySize(listing)) {
+		t.Errorf("The listing isn't size sorted: %v", listing.Items)
+	}
+
+	// sort by Time
+	listing.Sort = "time"
+	listing.applySort()
+	if !sort.IsSorted(byTime(listing)) {
+		t.Errorf("The listing isn't time sorted: %v", listing.Items)
+	}
+
+	// reverse by name
+	listing.Sort = "name"
+	listing.Order = "desc"
+	listing.applySort()
+	if !isReversed(byName(listing)) {
+		t.Errorf("The listing isn't reversed by name: %v", listing.Items)
+	}
+
+	// reverse by size
+	listing.Sort = "size"
+	listing.Order = "desc"
+	listing.applySort()
+	if !isReversed(bySize(listing)) {
+		t.Errorf("The listing isn't reversed by size: %v", listing.Items)
+	}
+
+	// reverse by time
+	listing.Sort = "time"
+	listing.Order = "desc"
+	listing.applySort()
+	if !isReversed(byTime(listing)) {
+		t.Errorf("The listing isn't reversed by time: %v", listing.Items)
+	}
+}