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 }