github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/lib/http/serve/dir.go (about)

     1  package serve
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"html/template"
     7  	"net/http"
     8  	"net/url"
     9  	"path"
    10  	"sort"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/rclone/rclone/fs"
    15  	"github.com/rclone/rclone/fs/accounting"
    16  	"github.com/rclone/rclone/lib/rest"
    17  )
    18  
    19  // DirEntry is a directory entry
    20  type DirEntry struct {
    21  	remote  string
    22  	URL     string
    23  	Leaf    string
    24  	IsDir   bool
    25  	Size    int64
    26  	ModTime time.Time
    27  }
    28  
    29  // Directory represents a directory
    30  type Directory struct {
    31  	DirRemote    string
    32  	Title        string
    33  	Name         string
    34  	Entries      []DirEntry
    35  	Query        string
    36  	HTMLTemplate *template.Template
    37  	Breadcrumb   []Crumb
    38  	Sort         string
    39  	Order        string
    40  }
    41  
    42  // Crumb is a breadcrumb entry
    43  type Crumb struct {
    44  	Link string
    45  	Text string
    46  }
    47  
    48  // NewDirectory makes an empty Directory
    49  func NewDirectory(dirRemote string, htmlTemplate *template.Template) *Directory {
    50  	var breadcrumb []Crumb
    51  
    52  	// skip trailing slash
    53  	lpath := "/" + dirRemote
    54  	if lpath[len(lpath)-1] == '/' {
    55  		lpath = lpath[:len(lpath)-1]
    56  	}
    57  
    58  	parts := strings.Split(lpath, "/")
    59  	for i := range parts {
    60  		txt := parts[i]
    61  		if i == 0 && parts[i] == "" {
    62  			txt = "/"
    63  		}
    64  		lnk := strings.Repeat("../", len(parts)-i-1)
    65  		breadcrumb = append(breadcrumb, Crumb{Link: lnk, Text: txt})
    66  	}
    67  
    68  	d := &Directory{
    69  		DirRemote:    dirRemote,
    70  		Title:        fmt.Sprintf("Directory listing of /%s", dirRemote),
    71  		Name:         fmt.Sprintf("/%s", dirRemote),
    72  		HTMLTemplate: htmlTemplate,
    73  		Breadcrumb:   breadcrumb,
    74  	}
    75  	return d
    76  }
    77  
    78  // SetQuery sets the query parameters for each URL
    79  func (d *Directory) SetQuery(queryParams url.Values) *Directory {
    80  	d.Query = ""
    81  	if len(queryParams) > 0 {
    82  		d.Query = "?" + queryParams.Encode()
    83  	}
    84  	return d
    85  }
    86  
    87  // AddHTMLEntry adds an entry to that directory
    88  func (d *Directory) AddHTMLEntry(remote string, isDir bool, size int64, modTime time.Time) {
    89  	leaf := path.Base(remote)
    90  	if leaf == "." {
    91  		leaf = ""
    92  	}
    93  	urlRemote := leaf
    94  	if isDir {
    95  		leaf += "/"
    96  		urlRemote += "/"
    97  	}
    98  	d.Entries = append(d.Entries, DirEntry{
    99  		remote:  remote,
   100  		URL:     rest.URLPathEscape(urlRemote) + d.Query,
   101  		Leaf:    leaf,
   102  		IsDir:   isDir,
   103  		Size:    size,
   104  		ModTime: modTime,
   105  	})
   106  }
   107  
   108  // AddEntry adds an entry to that directory
   109  func (d *Directory) AddEntry(remote string, isDir bool) {
   110  	leaf := path.Base(remote)
   111  	if leaf == "." {
   112  		leaf = ""
   113  	}
   114  	urlRemote := leaf
   115  	if isDir {
   116  		leaf += "/"
   117  		urlRemote += "/"
   118  	}
   119  	d.Entries = append(d.Entries, DirEntry{
   120  		remote: remote,
   121  		URL:    rest.URLPathEscape(urlRemote) + d.Query,
   122  		Leaf:   leaf,
   123  	})
   124  }
   125  
   126  // Error logs the error and if a ResponseWriter is given it writes an http.StatusInternalServerError
   127  func Error(what interface{}, w http.ResponseWriter, text string, err error) {
   128  	err = fs.CountError(err)
   129  	fs.Errorf(what, "%s: %v", text, err)
   130  	if w != nil {
   131  		http.Error(w, text+".", http.StatusInternalServerError)
   132  	}
   133  }
   134  
   135  // ProcessQueryParams takes and sorts/orders based on the request sort/order parameters and default is namedirfirst/asc
   136  func (d *Directory) ProcessQueryParams(sortParm string, orderParm string) *Directory {
   137  	d.Sort = sortParm
   138  	d.Order = orderParm
   139  
   140  	var toSort sort.Interface
   141  
   142  	switch d.Sort {
   143  	case sortByName:
   144  		toSort = byName(*d)
   145  	case sortByNameDirFirst:
   146  		toSort = byNameDirFirst(*d)
   147  	case sortBySize:
   148  		toSort = bySize(*d)
   149  	case sortByTime:
   150  		toSort = byTime(*d)
   151  	default:
   152  		toSort = byNameDirFirst(*d)
   153  	}
   154  	if d.Order == "desc" && toSort != nil {
   155  		toSort = sort.Reverse(toSort)
   156  	}
   157  	if toSort != nil {
   158  		sort.Sort(toSort)
   159  	}
   160  
   161  	return d
   162  
   163  }
   164  
   165  type byName Directory
   166  type byNameDirFirst Directory
   167  type bySize Directory
   168  type byTime Directory
   169  
   170  func (d byName) Len() int      { return len(d.Entries) }
   171  func (d byName) Swap(i, j int) { d.Entries[i], d.Entries[j] = d.Entries[j], d.Entries[i] }
   172  
   173  func (d byName) Less(i, j int) bool {
   174  	return strings.ToLower(d.Entries[i].Leaf) < strings.ToLower(d.Entries[j].Leaf)
   175  }
   176  
   177  func (d byNameDirFirst) Len() int      { return len(d.Entries) }
   178  func (d byNameDirFirst) Swap(i, j int) { d.Entries[i], d.Entries[j] = d.Entries[j], d.Entries[i] }
   179  
   180  func (d byNameDirFirst) Less(i, j int) bool {
   181  	// sort by name if both are dir or file
   182  	if d.Entries[i].IsDir == d.Entries[j].IsDir {
   183  		return strings.ToLower(d.Entries[i].Leaf) < strings.ToLower(d.Entries[j].Leaf)
   184  	}
   185  	// sort dir ahead of file
   186  	return d.Entries[i].IsDir
   187  }
   188  
   189  func (d bySize) Len() int      { return len(d.Entries) }
   190  func (d bySize) Swap(i, j int) { d.Entries[i], d.Entries[j] = d.Entries[j], d.Entries[i] }
   191  
   192  func (d bySize) Less(i, j int) bool {
   193  	const directoryOffset = -1 << 31 // = -math.MinInt32
   194  
   195  	iSize, jSize := d.Entries[i].Size, d.Entries[j].Size
   196  
   197  	// directory sizes depend on the file system; to
   198  	// provide a consistent experience, put them up front
   199  	// and sort them by name
   200  	if d.Entries[i].IsDir {
   201  		iSize = directoryOffset
   202  	}
   203  	if d.Entries[j].IsDir {
   204  		jSize = directoryOffset
   205  	}
   206  	if d.Entries[i].IsDir && d.Entries[j].IsDir {
   207  		return strings.ToLower(d.Entries[i].Leaf) < strings.ToLower(d.Entries[j].Leaf)
   208  	}
   209  
   210  	return iSize < jSize
   211  }
   212  
   213  func (d byTime) Len() int           { return len(d.Entries) }
   214  func (d byTime) Swap(i, j int)      { d.Entries[i], d.Entries[j] = d.Entries[j], d.Entries[i] }
   215  func (d byTime) Less(i, j int) bool { return d.Entries[i].ModTime.Before(d.Entries[j].ModTime) }
   216  
   217  const (
   218  	sortByName         = "name"
   219  	sortByNameDirFirst = "namedirfirst"
   220  	sortBySize         = "size"
   221  	sortByTime         = "time"
   222  )
   223  
   224  // Serve serves a directory
   225  func (d *Directory) Serve(w http.ResponseWriter, r *http.Request) {
   226  	// Account the transfer
   227  	tr := accounting.Stats(r.Context()).NewTransferRemoteSize(d.DirRemote, -1, nil, nil)
   228  	defer tr.Done(r.Context(), nil)
   229  
   230  	fs.Infof(d.DirRemote, "%s: Serving directory", r.RemoteAddr)
   231  
   232  	buf := &bytes.Buffer{}
   233  	err := d.HTMLTemplate.Execute(buf, d)
   234  	if err != nil {
   235  		Error(d.DirRemote, w, "Failed to render template", err)
   236  		return
   237  	}
   238  	w.Header().Set("Content-Length", fmt.Sprintf("%d", buf.Len()))
   239  	_, err = buf.WriteTo(w)
   240  	if err != nil {
   241  		Error(d.DirRemote, nil, "Failed to drain template buffer", err)
   242  	}
   243  }