From a9698728506c580bc38db2e122a5e6ef07f85ce6 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 20 May 2019 21:21:33 -0600 Subject: Default error handler; rename StaticFiles -> FileServer --- modules/caddyhttp/fileserver/browselisting.go | 245 ++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 modules/caddyhttp/fileserver/browselisting.go (limited to 'modules/caddyhttp/fileserver/browselisting.go') diff --git a/modules/caddyhttp/fileserver/browselisting.go b/modules/caddyhttp/fileserver/browselisting.go new file mode 100644 index 0000000..e17b7ae --- /dev/null +++ b/modules/caddyhttp/fileserver/browselisting.go @@ -0,0 +1,245 @@ +package fileserver + +import ( + "net/url" + "os" + "path" + "sort" + "strconv" + "strings" + "time" + + "bitbucket.org/lightcodelabs/caddy2" + "github.com/dustin/go-humanize" +) + +func (fsrv *FileServer) directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, repl caddy2.Replacer) browseListing { + filesToHide := fsrv.transformHidePaths(repl) + + var ( + fileInfos []fileInfo + dirCount, fileCount int + ) + + for _, f := range files { + name := f.Name() + + if fileHidden(name, filesToHide) { + continue + } + + isDir := f.IsDir() || isSymlinkTargetDir(f, urlPath) + + if isDir { + name += "/" + dirCount++ + } else { + fileCount++ + } + + u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name + + fileInfos = append(fileInfos, fileInfo{ + IsDir: isDir, + IsSymlink: isSymlink(f), + Name: f.Name(), + Size: f.Size(), + URL: u.String(), + ModTime: f.ModTime().UTC(), + Mode: f.Mode(), + }) + } + + return browseListing{ + Name: path.Base(urlPath), + Path: urlPath, + CanGoUp: canGoUp, + Items: fileInfos, + NumDirs: dirCount, + NumFiles: fileCount, + } +} + +type browseListing struct { + // The name of the directory (the last element of the path). + Name string + + // The full path of the request. + Path string + + // Whether the parent directory is browseable. + CanGoUp bool + + // The items (files and folders) in the path. + Items []fileInfo + + // The number of directories in the listing. + NumDirs int + + // The number of files (items that aren't directories) in the listing. + NumFiles int + + // Sort column used + Sort string + + // Sorting order + Order string + + // If ≠0 then Items have been limited to that many elements. + ItemsLimitedTo int +} + +// Breadcrumbs returns l.Path where every element maps +// the link to the text to display. +func (l browseListing) Breadcrumbs() []crumb { + var result []crumb + + if len(l.Path) == 0 { + return result + } + + // skip trailing slash + lpath := l.Path + 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) + result = append(result, crumb{Link: lnk, Text: txt}) + } + + return result +} + +func (l *browseListing) applySortAndLimit(sortParam, orderParam, limitParam string) { + 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)) + } + } + + if limitParam != "" { + limit, _ := strconv.Atoi(limitParam) + if limit > 0 && limit <= len(l.Items) { + l.Items = l.Items[:limit] + l.ItemsLimitedTo = limit + } + } +} + +// 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) +} + +type byName browseListing +type byNameDirFirst browseListing +type bySize browseListing +type byTime browseListing + +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" + sortByNameDirFirst = "name_dir_first" + sortBySize = "size" + sortByTime = "time" +) -- cgit v1.2.3