2019-07-01 06:07:58 +08:00
|
|
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2019-05-21 11:21:33 +08:00
|
|
|
package fileserver
|
2019-05-21 05:46:34 +08:00
|
|
|
|
|
|
|
import (
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"sort"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2019-07-03 02:37:06 +08:00
|
|
|
"github.com/caddyserver/caddy/v2"
|
2021-11-23 05:59:09 +08:00
|
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
2019-05-21 05:46:34 +08:00
|
|
|
"github.com/dustin/go-humanize"
|
|
|
|
)
|
|
|
|
|
2021-11-23 05:59:09 +08:00
|
|
|
func (fsrv *FileServer) directoryListing(files []os.FileInfo, canGoUp bool, root, urlPath string, repl *caddy.Replacer) browseTemplateContext {
|
2019-05-21 11:21:33 +08:00
|
|
|
filesToHide := fsrv.transformHidePaths(repl)
|
2019-05-21 05:46:34 +08:00
|
|
|
|
2020-11-23 05:50:29 +08:00
|
|
|
var dirCount, fileCount int
|
|
|
|
fileInfos := []fileInfo{}
|
2019-05-21 05:46:34 +08:00
|
|
|
|
|
|
|
for _, f := range files {
|
|
|
|
name := f.Name()
|
|
|
|
|
|
|
|
if fileHidden(name, filesToHide) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-11-25 03:24:44 +08:00
|
|
|
isDir := f.IsDir() || isSymlinkTargetDir(f, root, urlPath)
|
2019-05-21 05:46:34 +08:00
|
|
|
|
2021-09-17 04:40:31 +08:00
|
|
|
// add the slash after the escape of path to avoid escaping the slash as well
|
2019-05-21 05:46:34 +08:00
|
|
|
if isDir {
|
2021-12-11 22:26:21 +08:00
|
|
|
name += "/"
|
2019-05-21 05:46:34 +08:00
|
|
|
dirCount++
|
|
|
|
} else {
|
|
|
|
fileCount++
|
|
|
|
}
|
|
|
|
|
2021-09-18 19:51:59 +08:00
|
|
|
size := f.Size()
|
2021-11-23 05:59:09 +08:00
|
|
|
fileIsSymlink := isSymlink(f)
|
2021-09-18 19:51:59 +08:00
|
|
|
if fileIsSymlink {
|
2021-11-23 05:59:09 +08:00
|
|
|
path := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, f.Name()))
|
|
|
|
fileInfo, err := os.Stat(path)
|
|
|
|
if err == nil {
|
|
|
|
size = fileInfo.Size()
|
2021-09-18 19:51:59 +08:00
|
|
|
}
|
2021-11-23 05:59:09 +08:00
|
|
|
// An error most likely means the symlink target doesn't exist,
|
|
|
|
// which isn't entirely unusual and shouldn't fail the listing.
|
|
|
|
// In this case, just use the size of the symlink itself, which
|
|
|
|
// was already set above.
|
2021-09-18 19:51:59 +08:00
|
|
|
}
|
|
|
|
|
2021-12-11 22:26:21 +08:00
|
|
|
u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
|
|
|
|
|
2019-05-21 05:46:34 +08:00
|
|
|
fileInfos = append(fileInfos, fileInfo{
|
|
|
|
IsDir: isDir,
|
2021-09-18 19:51:59 +08:00
|
|
|
IsSymlink: fileIsSymlink,
|
|
|
|
Name: name,
|
|
|
|
Size: size,
|
2019-05-21 05:46:34 +08:00
|
|
|
URL: u.String(),
|
|
|
|
ModTime: f.ModTime().UTC(),
|
|
|
|
Mode: f.Mode(),
|
|
|
|
})
|
|
|
|
}
|
2021-11-23 16:13:09 +08:00
|
|
|
name, _ := url.PathUnescape(urlPath)
|
2021-05-01 10:17:23 +08:00
|
|
|
return browseTemplateContext{
|
2021-11-23 16:13:09 +08:00
|
|
|
Name: path.Base(name),
|
2019-05-21 05:46:34 +08:00
|
|
|
Path: urlPath,
|
|
|
|
CanGoUp: canGoUp,
|
|
|
|
Items: fileInfos,
|
|
|
|
NumDirs: dirCount,
|
|
|
|
NumFiles: fileCount,
|
2021-11-23 05:59:09 +08:00
|
|
|
}
|
2019-05-21 05:46:34 +08:00
|
|
|
}
|
|
|
|
|
2021-05-01 10:17:23 +08:00
|
|
|
// browseTemplateContext provides the template context for directory listings.
|
|
|
|
type browseTemplateContext struct {
|
2019-05-21 05:46:34 +08:00
|
|
|
// The name of the directory (the last element of the path).
|
2020-09-01 02:33:43 +08:00
|
|
|
Name string `json:"name"`
|
2019-05-21 05:46:34 +08:00
|
|
|
|
|
|
|
// The full path of the request.
|
2020-09-01 02:33:43 +08:00
|
|
|
Path string `json:"path"`
|
2019-05-21 05:46:34 +08:00
|
|
|
|
|
|
|
// Whether the parent directory is browseable.
|
2020-09-01 02:33:43 +08:00
|
|
|
CanGoUp bool `json:"can_go_up"`
|
2019-05-21 05:46:34 +08:00
|
|
|
|
|
|
|
// The items (files and folders) in the path.
|
2020-09-01 02:33:43 +08:00
|
|
|
Items []fileInfo `json:"items,omitempty"`
|
|
|
|
|
|
|
|
// If ≠0 then Items starting from that many elements.
|
|
|
|
Offset int `json:"offset,omitempty"`
|
|
|
|
|
|
|
|
// If ≠0 then Items have been limited to that many elements.
|
|
|
|
Limit int `json:"limit,omitempty"`
|
2019-05-21 05:46:34 +08:00
|
|
|
|
|
|
|
// The number of directories in the listing.
|
2020-09-01 02:33:43 +08:00
|
|
|
NumDirs int `json:"num_dirs"`
|
2019-05-21 05:46:34 +08:00
|
|
|
|
|
|
|
// The number of files (items that aren't directories) in the listing.
|
2020-09-01 02:33:43 +08:00
|
|
|
NumFiles int `json:"num_files"`
|
2019-05-21 05:46:34 +08:00
|
|
|
|
|
|
|
// Sort column used
|
2020-09-01 02:33:43 +08:00
|
|
|
Sort string `json:"sort,omitempty"`
|
2019-05-21 05:46:34 +08:00
|
|
|
|
|
|
|
// Sorting order
|
2020-09-01 02:33:43 +08:00
|
|
|
Order string `json:"order,omitempty"`
|
2019-05-21 05:46:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Breadcrumbs returns l.Path where every element maps
|
|
|
|
// the link to the text to display.
|
2021-05-01 10:17:23 +08:00
|
|
|
func (l browseTemplateContext) Breadcrumbs() []crumb {
|
2019-05-21 05:46:34 +08:00
|
|
|
if len(l.Path) == 0 {
|
2020-11-23 05:50:29 +08:00
|
|
|
return []crumb{}
|
2019-05-21 05:46:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// skip trailing slash
|
|
|
|
lpath := l.Path
|
|
|
|
if lpath[len(lpath)-1] == '/' {
|
|
|
|
lpath = lpath[:len(lpath)-1]
|
|
|
|
}
|
|
|
|
parts := strings.Split(lpath, "/")
|
2020-11-23 05:50:29 +08:00
|
|
|
result := make([]crumb, len(parts))
|
|
|
|
for i, p := range parts {
|
|
|
|
if i == 0 && p == "" {
|
|
|
|
p = "/"
|
2019-05-21 05:46:34 +08:00
|
|
|
}
|
2021-11-23 16:13:09 +08:00
|
|
|
// the directory name could include an encoded slash in its path,
|
|
|
|
// so the item name should be unescaped in the loop rather than unescaping the
|
|
|
|
// entire path outside the loop.
|
|
|
|
p, _ = url.PathUnescape(p)
|
2019-05-21 05:46:34 +08:00
|
|
|
lnk := strings.Repeat("../", len(parts)-i-1)
|
2020-11-23 05:50:29 +08:00
|
|
|
result[i] = crumb{Link: lnk, Text: p}
|
2019-05-21 05:46:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2021-05-01 10:17:23 +08:00
|
|
|
func (l *browseTemplateContext) applySortAndLimit(sortParam, orderParam, limitParam string, offsetParam string) {
|
2019-05-21 05:46:34 +08:00
|
|
|
l.Sort = sortParam
|
|
|
|
l.Order = orderParam
|
|
|
|
|
|
|
|
if l.Order == "desc" {
|
|
|
|
switch l.Sort {
|
|
|
|
case sortByName:
|
|
|
|
sort.Sort(sort.Reverse(byName(*l)))
|
|
|
|
case sortByNameDirFirst:
|
|
|
|
sort.Sort(sort.Reverse(byNameDirFirst(*l)))
|
|
|
|
case sortBySize:
|
|
|
|
sort.Sort(sort.Reverse(bySize(*l)))
|
|
|
|
case sortByTime:
|
|
|
|
sort.Sort(sort.Reverse(byTime(*l)))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
switch l.Sort {
|
|
|
|
case sortByName:
|
|
|
|
sort.Sort(byName(*l))
|
|
|
|
case sortByNameDirFirst:
|
|
|
|
sort.Sort(byNameDirFirst(*l))
|
|
|
|
case sortBySize:
|
|
|
|
sort.Sort(bySize(*l))
|
|
|
|
case sortByTime:
|
|
|
|
sort.Sort(byTime(*l))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-09 13:56:15 +08:00
|
|
|
if offsetParam != "" {
|
|
|
|
offset, _ := strconv.Atoi(offsetParam)
|
|
|
|
if offset > 0 && offset <= len(l.Items) {
|
|
|
|
l.Items = l.Items[offset:]
|
2020-09-01 02:33:43 +08:00
|
|
|
l.Offset = offset
|
2020-07-09 13:56:15 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-21 05:46:34 +08:00
|
|
|
if limitParam != "" {
|
|
|
|
limit, _ := strconv.Atoi(limitParam)
|
2020-07-09 13:56:15 +08:00
|
|
|
|
2019-05-21 05:46:34 +08:00
|
|
|
if limit > 0 && limit <= len(l.Items) {
|
|
|
|
l.Items = l.Items[:limit]
|
2020-09-01 02:33:43 +08:00
|
|
|
l.Limit = limit
|
2019-05-21 05:46:34 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// crumb represents part of a breadcrumb menu,
|
|
|
|
// pairing a link with the text to display.
|
|
|
|
type crumb struct {
|
|
|
|
Link, Text string
|
|
|
|
}
|
|
|
|
|
|
|
|
// fileInfo contains serializable information
|
|
|
|
// about a file or directory.
|
|
|
|
type fileInfo struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
Size int64 `json:"size"`
|
|
|
|
URL string `json:"url"`
|
|
|
|
ModTime time.Time `json:"mod_time"`
|
|
|
|
Mode os.FileMode `json:"mode"`
|
|
|
|
IsDir bool `json:"is_dir"`
|
|
|
|
IsSymlink bool `json:"is_symlink"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// HumanSize returns the size of the file as a
|
|
|
|
// human-readable string in IEC format (i.e.
|
|
|
|
// power of 2 or base 1024).
|
|
|
|
func (fi fileInfo) HumanSize() string {
|
|
|
|
return humanize.IBytes(uint64(fi.Size))
|
|
|
|
}
|
|
|
|
|
|
|
|
// HumanModTime returns the modified time of the file
|
|
|
|
// as a human-readable string given by format.
|
|
|
|
func (fi fileInfo) HumanModTime(format string) string {
|
|
|
|
return fi.ModTime.Format(format)
|
|
|
|
}
|
|
|
|
|
2021-05-01 10:17:23 +08:00
|
|
|
type (
|
|
|
|
byName browseTemplateContext
|
|
|
|
byNameDirFirst browseTemplateContext
|
|
|
|
bySize browseTemplateContext
|
|
|
|
byTime browseTemplateContext
|
|
|
|
)
|
2019-05-21 05:46:34 +08:00
|
|
|
|
|
|
|
func (l byName) Len() int { return len(l.Items) }
|
|
|
|
func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
|
|
|
|
|
|
|
func (l byName) Less(i, j int) bool {
|
|
|
|
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l byNameDirFirst) Len() int { return len(l.Items) }
|
|
|
|
func (l byNameDirFirst) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
|
|
|
|
|
|
|
func (l byNameDirFirst) Less(i, j int) bool {
|
|
|
|
// sort by name if both are dir or file
|
|
|
|
if l.Items[i].IsDir == l.Items[j].IsDir {
|
|
|
|
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
|
|
|
|
}
|
|
|
|
// sort dir ahead of file
|
|
|
|
return l.Items[i].IsDir
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l bySize) Len() int { return len(l.Items) }
|
|
|
|
func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
|
|
|
|
|
|
|
func (l bySize) Less(i, j int) bool {
|
|
|
|
const directoryOffset = -1 << 31 // = -math.MinInt32
|
|
|
|
|
|
|
|
iSize, jSize := l.Items[i].Size, l.Items[j].Size
|
|
|
|
|
|
|
|
// directory sizes depend on the file system; to
|
|
|
|
// provide a consistent experience, put them up front
|
|
|
|
// and sort them by name
|
|
|
|
if l.Items[i].IsDir {
|
|
|
|
iSize = directoryOffset
|
|
|
|
}
|
|
|
|
if l.Items[j].IsDir {
|
|
|
|
jSize = directoryOffset
|
|
|
|
}
|
|
|
|
if l.Items[i].IsDir && l.Items[j].IsDir {
|
|
|
|
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
return iSize < jSize
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l byTime) Len() int { return len(l.Items) }
|
|
|
|
func (l byTime) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
|
|
|
func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j].ModTime) }
|
|
|
|
|
|
|
|
const (
|
|
|
|
sortByName = "name"
|
2021-07-07 14:54:54 +08:00
|
|
|
sortByNameDirFirst = "namedirfirst"
|
2019-05-21 05:46:34 +08:00
|
|
|
sortBySize = "size"
|
|
|
|
sortByTime = "time"
|
|
|
|
)
|