serve http, serve webdav: Added a --template flag for user defined markup
This commit is contained in:
parent
dcf945ed58
commit
4362ca7bb9
10 changed files with 241 additions and 25 deletions
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
2
cmd/serve/http/testdata/golden/index.html
vendored
2
cmd/serve/http/testdata/golden/index.html
vendored
|
@ -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>
|
||||
|
|
|
@ -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.")
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
2
cmd/serve/webdav/testdata/golden/index.html
vendored
2
cmd/serve/webdav/testdata/golden/index.html
vendored
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue