From 4362ca7bb9f8f2bd12a1a17071bcd88d0b9acf9e Mon Sep 17 00:00:00 2001
From: calistri <robert.calistri@gmail.com>
Date: Thu, 30 Apr 2020 14:24:11 -0400
Subject: [PATCH] serve http, serve webdav: Added a --template flag for user
 defined markup

---
 cmd/serve/http/http.go                        |   6 +-
 cmd/serve/http/testdata/golden/index.html     |   2 +-
 cmd/serve/httplib/httpflags/httpflags.go      |   1 +
 cmd/serve/httplib/httplib.go                  |  24 ++-
 .../httplib/serve/data/assets_vfsdata.go      |   4 +-
 cmd/serve/httplib/serve/data/data.go          |  39 +++--
 cmd/serve/httplib/serve/dir.go                | 152 +++++++++++++++++-
 cmd/serve/httplib/serve/dir_test.go           |  29 +++-
 cmd/serve/webdav/testdata/golden/index.html   |   2 +-
 cmd/serve/webdav/webdav.go                    |   7 +-
 10 files changed, 241 insertions(+), 25 deletions(-)

diff --git a/cmd/serve/http/http.go b/cmd/serve/http/http.go
index 0360336dc..6dcd40f73 100644
--- a/cmd/serve/http/http.go
+++ b/cmd/serve/http/http.go
@@ -131,9 +131,13 @@ func (s *server) serveDir(w http.ResponseWriter, r *http.Request, dirRemote stri
 	// Make the entries for display
 	directory := serve.NewDirectory(dirRemote, s.HTMLTemplate)
 	for _, node := range dirEntries {
-		directory.AddEntry(node.Path(), node.IsDir())
+		directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), node.ModTime())
 	}
 
+	sortParm := r.URL.Query().Get("sort")
+	orderParm := r.URL.Query().Get("order")
+	directory.ProcessQueryParams(sortParm, orderParm)
+
 	directory.Serve(w, r)
 }
 
diff --git a/cmd/serve/http/testdata/golden/index.html b/cmd/serve/http/testdata/golden/index.html
index 8afb9b697..2ddb71e39 100644
--- a/cmd/serve/http/testdata/golden/index.html
+++ b/cmd/serve/http/testdata/golden/index.html
@@ -6,8 +6,8 @@
 </head>
 <body>
 <h1>Directory listing of /</h1>
-<a href="one%25.txt">one%.txt</a><br />
 <a href="three/">three/</a><br />
+<a href="one%25.txt">one%.txt</a><br />
 <a href="two.txt">two.txt</a><br />
 </body>
 </html>
diff --git a/cmd/serve/httplib/httpflags/httpflags.go b/cmd/serve/httplib/httpflags/httpflags.go
index 49f672548..073715504 100644
--- a/cmd/serve/httplib/httpflags/httpflags.go
+++ b/cmd/serve/httplib/httpflags/httpflags.go
@@ -27,6 +27,7 @@ func AddFlagsPrefix(flagSet *pflag.FlagSet, prefix string, Opt *httplib.Options)
 	flags.StringVarP(flagSet, &Opt.BasicUser, prefix+"user", "", Opt.BasicUser, "User name for authentication.")
 	flags.StringVarP(flagSet, &Opt.BasicPass, prefix+"pass", "", Opt.BasicPass, "Password for authentication.")
 	flags.StringVarP(flagSet, &Opt.BaseURL, prefix+"baseurl", "", Opt.BaseURL, "Prefix for URLs - leave blank for root.")
+	flags.StringVarP(flagSet, &Opt.Template, prefix+"template", "", Opt.Template, "User Specified Template.")
 
 }
 
diff --git a/cmd/serve/httplib/httplib.go b/cmd/serve/httplib/httplib.go
index d2839fa81..26d195043 100644
--- a/cmd/serve/httplib/httplib.go
+++ b/cmd/serve/httplib/httplib.go
@@ -52,6 +52,27 @@ inserts leading and trailing "/" on --baseurl, so --baseurl "rclone",
 --baseurl "/rclone" and --baseurl "/rclone/" are all treated
 identically.
 
+--template allows a user to specify a custom markup template for http
+and webdav serve functions.  The server exports the following markup
+to be used within the template to server pages:
+.Name The full path of a file/directory.
+.Title "Directory listing of .Name".
+.Sort The current sort used.  This is changble via ?sort= parameter
+          Sort Options: namedirfist,name,size,time (defailt namedirfirst)
+.Order The current ordering used.  This is changable via ?order= paramter
+           Order Options: asc,desc (default asc)
+.Query Currently unused.
+.Breacrumb Allows for creating a relative navigation
+-- .Link The relative to the root link of the Text.
+-- .Text The Name of the directory.
+.Entries Information about a specific file/directory.
+-- .URL The 'url' of an entry. 
+-- .Leaf Currently same as 'URL' but intended to be 'just' the name.
+-- .IsDir Boolean for if an entry is a directory or not.
+-- .Size Size in Bytes of the entry.
+-- .ModTime The UTC timestamp of an entry.
+
+
 #### Authentication
 
 By default this will serve files without needing a login.
@@ -101,6 +122,7 @@ type Options struct {
 	BasicUser          string        // single username for basic auth if not using Htpasswd
 	BasicPass          string        // password for BasicUser
 	Auth               AuthFn        `json:"-"` // custom Auth (not set by command line flags)
+	Template           string        // User specified template
 }
 
 // AuthFn if used will be used to authenticate user, pass. If an error
@@ -281,7 +303,7 @@ func NewServer(handler http.Handler, opt *Options) *Server {
 		s.httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
 	}
 
-	htmlTemplate, templateErr := data.GetTemplate()
+	htmlTemplate, templateErr := data.GetTemplate(s.Opt.Template)
 	if templateErr != nil {
 		log.Fatalf(templateErr.Error())
 	}
diff --git a/cmd/serve/httplib/serve/data/assets_vfsdata.go b/cmd/serve/httplib/serve/data/assets_vfsdata.go
index 88d14efdd..cb8723368 100644
--- a/cmd/serve/httplib/serve/data/assets_vfsdata.go
+++ b/cmd/serve/httplib/serve/data/assets_vfsdata.go
@@ -21,11 +21,11 @@ var Assets = func() http.FileSystem {
 	fs := vfsgen۰FS{
 		"/": &vfsgen۰DirInfo{
 			name:    "/",
-			modTime: time.Date(2018, 12, 16, 6, 54, 42, 894445775, time.UTC),
+			modTime: time.Date(2020, 5, 4, 15, 36, 2, 723307530, time.UTC),
 		},
 		"/index.html": &vfsgen۰CompressedFileInfo{
 			name:             "index.html",
-			modTime:          time.Date(2018, 12, 16, 6, 54, 42, 790442328, time.UTC),
+			modTime:          time.Date(2020, 5, 4, 15, 36, 2, 527302371, time.UTC),
 			uncompressedSize: 226,
 
 			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\x8f\x31\xcf\x83\x20\x10\x86\x77\x7e\xc5\x7d\xc4\xf5\x93\xb8\x35\x0d\xb0\xb4\x6e\x26\x6d\x1a\x3b\x74\x3c\xeb\x29\x24\x4a\x13\xa4\x43\x43\xf8\xef\x0d\xea\xd4\x09\xee\x79\xef\x9e\xcb\xc9\xbf\xf3\xe5\xd4\x3e\xae\x35\x98\x30\x4f\x9a\xc9\xfc\xc0\x84\x6e\x54\x9c\x1c\xcf\x80\xb0\xd7\x4c\xce\x14\x10\x9e\x06\xfd\x42\x41\xf1\x77\x18\xfe\x0f\x39\x0d\x36\x4c\xa4\x63\x84\xb2\xcd\x3f\x48\x49\x8a\x8d\x31\x29\xf6\xd1\xee\xd5\x7f\xb2\xa8\xfa\xe9\x33\x95\x66\x31\x82\x47\x37\x12\x14\x16\x8e\x0a\xca\xda\x05\x6f\x69\xc9\x39\x82\xf1\x34\x28\x1e\x23\x14\xb6\xbc\xdf\x1a\x48\x89\xeb\xad\x6a\x08\x87\xd5\x81\x5a\x76\x1e\xc4\x2a\x22\xd7\xaf\x6c\xdf\x27\xb6\x8b\xbe\x01\x00\x00\xff\xff\x92\x2e\x35\x75\xe2\x00\x00\x00"),
diff --git a/cmd/serve/httplib/serve/data/data.go b/cmd/serve/httplib/serve/data/data.go
index d34fb2963..e976c58cb 100644
--- a/cmd/serve/httplib/serve/data/data.go
+++ b/cmd/serve/httplib/serve/data/data.go
@@ -11,22 +11,33 @@ import (
 	"github.com/rclone/rclone/fs"
 )
 
-// GetTemplate returns the HTML template for serving directories via HTTP
-func GetTemplate() (tpl *template.Template, err error) {
-	templateFile, err := Assets.Open("index.html")
-	if err != nil {
-		return nil, errors.Wrap(err, "get template open")
+// GetTemplate returns the HTML template for serving directories via HTTP/Webdav
+func GetTemplate(tmpl string) (tpl *template.Template, err error) {
+	var templateString string
+	if tmpl == "" {
+		templateFile, err := Assets.Open("index.html")
+		if err != nil {
+			return nil, errors.Wrap(err, "get template open")
+		}
+
+		defer fs.CheckClose(templateFile, &err)
+
+		templateBytes, err := ioutil.ReadAll(templateFile)
+		if err != nil {
+			return nil, errors.Wrap(err, "get template read")
+		}
+
+		templateString = string(templateBytes)
+
+	} else {
+		templateFile, err := ioutil.ReadFile(tmpl)
+		if err != nil {
+			return nil, errors.Wrap(err, "get template open")
+		}
+
+		templateString = string(templateFile)
 	}
 
-	defer fs.CheckClose(templateFile, &err)
-
-	templateBytes, err := ioutil.ReadAll(templateFile)
-	if err != nil {
-		return nil, errors.Wrap(err, "get template read")
-	}
-
-	var templateString = string(templateBytes)
-
 	tpl, err = template.New("index").Parse(templateString)
 	if err != nil {
 		return nil, errors.Wrap(err, "get template parse")
diff --git a/cmd/serve/httplib/serve/dir.go b/cmd/serve/httplib/serve/dir.go
index 601fc0152..c0801145f 100644
--- a/cmd/serve/httplib/serve/dir.go
+++ b/cmd/serve/httplib/serve/dir.go
@@ -7,6 +7,9 @@ import (
 	"net/http"
 	"net/url"
 	"path"
+	"sort"
+	"strings"
+	"time"
 
 	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/accounting"
@@ -15,26 +18,59 @@ import (
 
 // DirEntry is a directory entry
 type DirEntry struct {
-	remote string
-	URL    string
-	Leaf   string
+	remote  string
+	URL     string
+	Leaf    string
+	IsDir   bool
+	Size    int64
+	ModTime time.Time
 }
 
 // Directory represents a directory
 type Directory struct {
 	DirRemote    string
 	Title        string
+	Name         string
 	Entries      []DirEntry
 	Query        string
 	HTMLTemplate *template.Template
+	Breadcrumb   []Crumb
+	Sort         string
+	Order        string
+}
+
+// Crumb is a breadcrumb entry
+type Crumb struct {
+	Link string
+	Text string
 }
 
 // NewDirectory makes an empty Directory
 func NewDirectory(dirRemote string, htmlTemplate *template.Template) *Directory {
+	var breadcrumb []Crumb
+
+	// skip trailing slash
+	lpath := "/" + dirRemote
+	if lpath[len(lpath)-1] == '/' {
+		lpath = lpath[:len(lpath)-1]
+	}
+
+	parts := strings.Split(lpath, "/")
+	for i := range parts {
+		txt := parts[i]
+		if i == 0 && parts[i] == "" {
+			txt = "/"
+		}
+		lnk := strings.Repeat("../", len(parts)-i-1)
+		breadcrumb = append(breadcrumb, Crumb{Link: lnk, Text: txt})
+	}
+
 	d := &Directory{
 		DirRemote:    dirRemote,
 		Title:        fmt.Sprintf("Directory listing of /%s", dirRemote),
+		Name:         fmt.Sprintf("/%s", dirRemote),
 		HTMLTemplate: htmlTemplate,
+		Breadcrumb:   breadcrumb,
 	}
 	return d
 }
@@ -48,6 +84,27 @@ func (d *Directory) SetQuery(queryParams url.Values) *Directory {
 	return d
 }
 
+// AddHTMLEntry adds an entry to that directory
+func (d *Directory) AddHTMLEntry(remote string, isDir bool, size int64, modTime time.Time) {
+	leaf := path.Base(remote)
+	if leaf == "." {
+		leaf = ""
+	}
+	urlRemote := leaf
+	if isDir {
+		leaf += "/"
+		urlRemote += "/"
+	}
+	d.Entries = append(d.Entries, DirEntry{
+		remote:  remote,
+		URL:     rest.URLPathEscape(urlRemote) + d.Query,
+		Leaf:    leaf,
+		IsDir:   isDir,
+		Size:    size,
+		ModTime: modTime,
+	})
+}
+
 // AddEntry adds an entry to that directory
 func (d *Directory) AddEntry(remote string, isDir bool) {
 	leaf := path.Base(remote)
@@ -75,6 +132,95 @@ func Error(what interface{}, w http.ResponseWriter, text string, err error) {
 	}
 }
 
+// ProcessQueryParams takes and sorts/orders based on the request sort/order parameters and defailt is namedirfist/asc
+func (d *Directory) ProcessQueryParams(sortParm string, orderParm string) *Directory {
+	d.Sort = sortParm
+	d.Order = orderParm
+
+	var toSort sort.Interface
+
+	switch d.Sort {
+	case sortByName:
+		toSort = byName(*d)
+	case sortByNameDirFirst:
+		toSort = byNameDirFirst(*d)
+	case sortBySize:
+		toSort = bySize(*d)
+	case sortByTime:
+		toSort = byTime(*d)
+	default:
+		toSort = byNameDirFirst(*d)
+	}
+	if d.Order == "desc" && toSort != nil {
+		toSort = sort.Reverse(toSort)
+	}
+	if toSort != nil {
+		sort.Sort(toSort)
+	}
+
+	return d
+
+}
+
+type byName Directory
+type byNameDirFirst Directory
+type bySize Directory
+type byTime Directory
+
+func (d byName) Len() int      { return len(d.Entries) }
+func (d byName) Swap(i, j int) { d.Entries[i], d.Entries[j] = d.Entries[j], d.Entries[i] }
+
+func (d byName) Less(i, j int) bool {
+	return strings.ToLower(d.Entries[i].Leaf) < strings.ToLower(d.Entries[j].Leaf)
+}
+
+func (d byNameDirFirst) Len() int      { return len(d.Entries) }
+func (d byNameDirFirst) Swap(i, j int) { d.Entries[i], d.Entries[j] = d.Entries[j], d.Entries[i] }
+
+func (d byNameDirFirst) Less(i, j int) bool {
+	// sort by name if both are dir or file
+	if d.Entries[i].IsDir == d.Entries[j].IsDir {
+		return strings.ToLower(d.Entries[i].Leaf) < strings.ToLower(d.Entries[j].Leaf)
+	}
+	// sort dir ahead of file
+	return d.Entries[i].IsDir
+}
+
+func (d bySize) Len() int      { return len(d.Entries) }
+func (d bySize) Swap(i, j int) { d.Entries[i], d.Entries[j] = d.Entries[j], d.Entries[i] }
+
+func (d bySize) Less(i, j int) bool {
+	const directoryOffset = -1 << 31 // = -math.MinInt32
+
+	iSize, jSize := d.Entries[i].Size, d.Entries[j].Size
+
+	// directory sizes depend on the file system; to
+	// provide a consistent experience, put them up front
+	// and sort them by name
+	if d.Entries[i].IsDir {
+		iSize = directoryOffset
+	}
+	if d.Entries[j].IsDir {
+		jSize = directoryOffset
+	}
+	if d.Entries[i].IsDir && d.Entries[j].IsDir {
+		return strings.ToLower(d.Entries[i].Leaf) < strings.ToLower(d.Entries[j].Leaf)
+	}
+
+	return iSize < jSize
+}
+
+func (d byTime) Len() int           { return len(d.Entries) }
+func (d byTime) Swap(i, j int)      { d.Entries[i], d.Entries[j] = d.Entries[j], d.Entries[i] }
+func (d byTime) Less(i, j int) bool { return d.Entries[i].ModTime.Before(d.Entries[j].ModTime) }
+
+const (
+	sortByName         = "name"
+	sortByNameDirFirst = "namedirfirst"
+	sortBySize         = "size"
+	sortByTime         = "time"
+)
+
 // Serve serves a directory
 func (d *Directory) Serve(w http.ResponseWriter, r *http.Request) {
 	// Account the transfer
diff --git a/cmd/serve/httplib/serve/dir_test.go b/cmd/serve/httplib/serve/dir_test.go
index 87a8fbd07..cb49c1074 100644
--- a/cmd/serve/httplib/serve/dir_test.go
+++ b/cmd/serve/httplib/serve/dir_test.go
@@ -8,6 +8,7 @@ import (
 	"net/http/httptest"
 	"net/url"
 	"testing"
+	"time"
 
 	"github.com/rclone/rclone/cmd/serve/httplib/serve/data"
 	"github.com/stretchr/testify/assert"
@@ -15,7 +16,7 @@ import (
 )
 
 func GetTemplate(t *testing.T) *template.Template {
-	htmlTemplate, err := data.GetTemplate()
+	htmlTemplate, err := data.GetTemplate("")
 	require.NoError(t, err)
 	return htmlTemplate
 }
@@ -35,6 +36,32 @@ func TestSetQuery(t *testing.T) {
 	assert.Equal(t, "", d.Query)
 }
 
+func TestAddHTMLEntry(t *testing.T) {
+	var modtime = time.Now()
+	var d = NewDirectory("z", GetTemplate(t))
+	d.AddHTMLEntry("", true, 0, modtime)
+	d.AddHTMLEntry("dir", true, 0, modtime)
+	d.AddHTMLEntry("a/b/c/d.txt", false, 64, modtime)
+	d.AddHTMLEntry("a/b/c/colon:colon.txt", false, 64, modtime)
+	d.AddHTMLEntry("\"quotes\".txt", false, 64, modtime)
+	assert.Equal(t, []DirEntry{
+		{remote: "", URL: "/", Leaf: "/", IsDir: true, Size: 0, ModTime: modtime},
+		{remote: "dir", URL: "dir/", Leaf: "dir/", IsDir: true, Size: 0, ModTime: modtime},
+		{remote: "a/b/c/d.txt", URL: "d.txt", Leaf: "d.txt", IsDir: false, Size: 64, ModTime: modtime},
+		{remote: "a/b/c/colon:colon.txt", URL: "./colon:colon.txt", Leaf: "colon:colon.txt", IsDir: false, Size: 64, ModTime: modtime},
+		{remote: "\"quotes\".txt", URL: "%22quotes%22.txt", Leaf: "\"quotes\".txt", Size: 64, IsDir: false, ModTime: modtime},
+	}, d.Entries)
+
+	// Now test with a query parameter
+	d = NewDirectory("z", GetTemplate(t)).SetQuery(url.Values{"potato": []string{"42"}})
+	d.AddHTMLEntry("file", false, 64, modtime)
+	d.AddHTMLEntry("dir", true, 0, modtime)
+	assert.Equal(t, []DirEntry{
+		{remote: "file", URL: "file?potato=42", Leaf: "file", IsDir: false, Size: 64, ModTime: modtime},
+		{remote: "dir", URL: "dir/?potato=42", Leaf: "dir/", IsDir: true, Size: 0, ModTime: modtime},
+	}, d.Entries)
+}
+
 func TestAddEntry(t *testing.T) {
 	var d = NewDirectory("z", GetTemplate(t))
 	d.AddEntry("", true)
diff --git a/cmd/serve/webdav/testdata/golden/index.html b/cmd/serve/webdav/testdata/golden/index.html
index 8afb9b697..2ddb71e39 100644
--- a/cmd/serve/webdav/testdata/golden/index.html
+++ b/cmd/serve/webdav/testdata/golden/index.html
@@ -6,8 +6,8 @@
 </head>
 <body>
 <h1>Directory listing of /</h1>
-<a href="one%25.txt">one%.txt</a><br />
 <a href="three/">three/</a><br />
+<a href="one%25.txt">one%.txt</a><br />
 <a href="two.txt">two.txt</a><br />
 </body>
 </html>
diff --git a/cmd/serve/webdav/webdav.go b/cmd/serve/webdav/webdav.go
index c6cdf244e..5d7af1e05 100644
--- a/cmd/serve/webdav/webdav.go
+++ b/cmd/serve/webdav/webdav.go
@@ -204,6 +204,7 @@ func (w *WebDAV) serveDir(rw http.ResponseWriter, r *http.Request, dirRemote str
 	}
 	dir := node.(*vfs.Dir)
 	dirEntries, err := dir.ReadDirAll()
+
 	if err != nil {
 		serve.Error(dirRemote, rw, "Failed to list directory", err)
 		return
@@ -212,9 +213,13 @@ func (w *WebDAV) serveDir(rw http.ResponseWriter, r *http.Request, dirRemote str
 	// Make the entries for display
 	directory := serve.NewDirectory(dirRemote, w.HTMLTemplate)
 	for _, node := range dirEntries {
-		directory.AddEntry(node.Path(), node.IsDir())
+		directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), node.ModTime())
 	}
 
+	sortParm := r.URL.Query().Get("sort")
+	orderParm := r.URL.Query().Get("order")
+	directory.ProcessQueryParams(sortParm, orderParm)
+
 	directory.Serve(rw, r)
 }