2018-10-28 14:08:20 +00:00
|
|
|
package serve
|
|
|
|
|
|
|
|
import (
|
2019-09-22 20:31:11 +00:00
|
|
|
"bytes"
|
2018-10-28 14:08:20 +00:00
|
|
|
"fmt"
|
|
|
|
"html/template"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"path"
|
2020-04-30 18:24:11 +00:00
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
"time"
|
2018-10-28 14:08:20 +00:00
|
|
|
|
2019-07-28 17:47:38 +00:00
|
|
|
"github.com/rclone/rclone/fs"
|
|
|
|
"github.com/rclone/rclone/fs/accounting"
|
|
|
|
"github.com/rclone/rclone/lib/rest"
|
2018-10-28 14:08:20 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// DirEntry is a directory entry
|
|
|
|
type DirEntry struct {
|
2020-04-30 18:24:11 +00:00
|
|
|
remote string
|
|
|
|
URL string
|
|
|
|
Leaf string
|
|
|
|
IsDir bool
|
|
|
|
Size int64
|
|
|
|
ModTime time.Time
|
2018-10-28 14:08:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Directory represents a directory
|
|
|
|
type Directory struct {
|
2018-12-23 00:16:50 +00:00
|
|
|
DirRemote string
|
|
|
|
Title string
|
2020-04-30 18:24:11 +00:00
|
|
|
Name string
|
2018-12-23 00:16:50 +00:00
|
|
|
Entries []DirEntry
|
|
|
|
Query string
|
|
|
|
HTMLTemplate *template.Template
|
2020-04-30 18:24:11 +00:00
|
|
|
Breadcrumb []Crumb
|
|
|
|
Sort string
|
|
|
|
Order string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Crumb is a breadcrumb entry
|
|
|
|
type Crumb struct {
|
|
|
|
Link string
|
|
|
|
Text string
|
2018-10-28 14:08:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewDirectory makes an empty Directory
|
2018-12-23 00:16:50 +00:00
|
|
|
func NewDirectory(dirRemote string, htmlTemplate *template.Template) *Directory {
|
2020-04-30 18:24:11 +00:00
|
|
|
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})
|
|
|
|
}
|
|
|
|
|
2018-10-28 14:08:20 +00:00
|
|
|
d := &Directory{
|
2018-12-23 00:16:50 +00:00
|
|
|
DirRemote: dirRemote,
|
|
|
|
Title: fmt.Sprintf("Directory listing of /%s", dirRemote),
|
2020-04-30 18:24:11 +00:00
|
|
|
Name: fmt.Sprintf("/%s", dirRemote),
|
2018-12-23 00:16:50 +00:00
|
|
|
HTMLTemplate: htmlTemplate,
|
2020-04-30 18:24:11 +00:00
|
|
|
Breadcrumb: breadcrumb,
|
2018-10-28 14:08:20 +00:00
|
|
|
}
|
|
|
|
return d
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetQuery sets the query parameters for each URL
|
|
|
|
func (d *Directory) SetQuery(queryParams url.Values) *Directory {
|
|
|
|
d.Query = ""
|
|
|
|
if len(queryParams) > 0 {
|
|
|
|
d.Query = "?" + queryParams.Encode()
|
|
|
|
}
|
|
|
|
return d
|
|
|
|
}
|
|
|
|
|
2020-04-30 18:24:11 +00:00
|
|
|
// 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,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-10-28 14:08:20 +00:00
|
|
|
// AddEntry adds an entry to that directory
|
|
|
|
func (d *Directory) AddEntry(remote string, isDir bool) {
|
|
|
|
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,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-05-20 10:39:20 +00:00
|
|
|
// Error logs the error and if a ResponseWriter is given it writes an http.StatusInternalServerError
|
2018-10-28 14:08:20 +00:00
|
|
|
func Error(what interface{}, w http.ResponseWriter, text string, err error) {
|
2019-11-18 14:13:02 +00:00
|
|
|
err = fs.CountError(err)
|
2018-10-28 14:08:20 +00:00
|
|
|
fs.Errorf(what, "%s: %v", text, err)
|
2019-09-22 20:31:11 +00:00
|
|
|
if w != nil {
|
|
|
|
http.Error(w, text+".", http.StatusInternalServerError)
|
|
|
|
}
|
2018-10-28 14:08:20 +00:00
|
|
|
}
|
|
|
|
|
Spelling fixes
Fix spelling of: above, already, anonymous, associated,
authentication, bandwidth, because, between, blocks, calculate,
candidates, cautious, changelog, cleaner, clipboard, command,
completely, concurrently, considered, constructs, corrupt, current,
daemon, dependencies, deprecated, directory, dispatcher, download,
eligible, ellipsis, encrypter, endpoint, entrieslist, essentially,
existing writers, existing, expires, filesystem, flushing, frequently,
hierarchy, however, implementation, implements, inaccurate,
individually, insensitive, longer, maximum, metadata, modified,
multipart, namedirfirst, nextcloud, obscured, opened, optional,
owncloud, pacific, passphrase, password, permanently, persimmon,
positive, potato, protocol, quota, receiving, recommends, referring,
requires, revisited, satisfied, satisfies, satisfy, semver,
serialized, session, storage, strategies, stringlist, successful,
supported, surprise, temporarily, temporary, transactions, unneeded,
update, uploads, wrapped
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2020-10-09 00:17:24 +00:00
|
|
|
// ProcessQueryParams takes and sorts/orders based on the request sort/order parameters and default is namedirfirst/asc
|
2020-04-30 18:24:11 +00:00
|
|
|
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"
|
|
|
|
)
|
|
|
|
|
2018-10-28 14:08:20 +00:00
|
|
|
// Serve serves a directory
|
|
|
|
func (d *Directory) Serve(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// Account the transfer
|
2019-07-18 10:13:54 +00:00
|
|
|
tr := accounting.Stats(r.Context()).NewTransferRemoteSize(d.DirRemote, -1)
|
2020-11-05 16:59:59 +00:00
|
|
|
defer tr.Done(r.Context(), nil)
|
2018-10-28 14:08:20 +00:00
|
|
|
|
|
|
|
fs.Infof(d.DirRemote, "%s: Serving directory", r.RemoteAddr)
|
2018-12-23 00:16:50 +00:00
|
|
|
|
2019-09-22 20:31:11 +00:00
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
err := d.HTMLTemplate.Execute(buf, d)
|
2018-10-28 14:08:20 +00:00
|
|
|
if err != nil {
|
|
|
|
Error(d.DirRemote, w, "Failed to render template", err)
|
|
|
|
return
|
|
|
|
}
|
2019-09-22 20:31:11 +00:00
|
|
|
_, err = buf.WriteTo(w)
|
|
|
|
if err != nil {
|
|
|
|
Error(d.DirRemote, nil, "Failed to drain template buffer", err)
|
|
|
|
}
|
2018-10-28 14:08:20 +00:00
|
|
|
}
|