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, fsrv.Root, 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" )