rclone/vendor/github.com/a8m/tree/node.go
2017-07-26 23:06:48 +01:00

395 lines
8.2 KiB
Go

package tree
import (
"errors"
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
)
// Node represent some node in the tree
// contains FileInfo, and its childs
type Node struct {
os.FileInfo
path string
depth int
err error
nodes Nodes
vpaths map[string]bool
}
// List of nodes
type Nodes []*Node
// To use this package programmatically, you must implement this
// interface.
// For example: PTAL on 'cmd/tree/tree.go'
type Fs interface {
Stat(path string) (os.FileInfo, error)
ReadDir(path string) ([]string, error)
}
// Options store the configuration for specific tree.
// Note, that 'Fs', and 'OutFile' are required (OutFile can be os.Stdout).
type Options struct {
Fs Fs
OutFile io.Writer
// List
All bool
DirsOnly bool
FullPath bool
IgnoreCase bool
FollowLink bool
DeepLevel int
Pattern string
IPattern string
// File
ByteSize bool
UnitSize bool
FileMode bool
ShowUid bool
ShowGid bool
LastMod bool
Quotes bool
Inodes bool
Device bool
// Sort
NoSort bool
VerSort bool
ModSort bool
DirSort bool
NameSort bool
SizeSort bool
CTimeSort bool
ReverSort bool
// Graphics
NoIndent bool
Colorize bool
}
// New get path and create new node(root).
func New(path string) *Node {
return &Node{path: path, vpaths: make(map[string]bool)}
}
// Visit all files under the given node.
func (node *Node) Visit(opts *Options) (dirs, files int) {
// visited paths
if path, err := filepath.Abs(node.path); err == nil {
path = filepath.Clean(path)
node.vpaths[path] = true
}
// stat
fi, err := opts.Fs.Stat(node.path)
if err != nil {
node.err = err
return
}
node.FileInfo = fi
if !fi.IsDir() {
return 0, 1
}
// DeepLevel option
if opts.DeepLevel > 0 && opts.DeepLevel <= node.depth {
return 1, 0
}
names, err := opts.Fs.ReadDir(node.path)
if err != nil {
node.err = err
return
}
node.nodes = make(Nodes, 0)
for _, name := range names {
// "all" option
if !opts.All && strings.HasPrefix(name, ".") {
continue
}
nnode := &Node{
path: filepath.Join(node.path, name),
depth: node.depth + 1,
vpaths: node.vpaths,
}
d, f := nnode.Visit(opts)
if nnode.err == nil && !nnode.IsDir() {
// "dirs only" option
if opts.DirsOnly {
continue
}
var rePrefix string
if opts.IgnoreCase {
rePrefix = "(?i)"
}
// Pattern matching
if opts.Pattern != "" {
re, err := regexp.Compile(rePrefix + opts.Pattern)
if err == nil && !re.MatchString(name) {
continue
}
}
// IPattern matching
if opts.IPattern != "" {
re, err := regexp.Compile(rePrefix + opts.IPattern)
if err == nil && re.MatchString(name) {
continue
}
}
}
node.nodes = append(node.nodes, nnode)
dirs, files = dirs+d, files+f
}
// Sorting
if !opts.NoSort {
node.sort(opts)
}
return dirs + 1, files
}
func (node *Node) sort(opts *Options) {
var fn SortFunc
switch {
case opts.ModSort:
fn = ModSort
case opts.CTimeSort:
fn = CTimeSort
case opts.DirSort:
fn = DirSort
case opts.VerSort:
fn = VerSort
case opts.SizeSort:
fn = SizeSort
case opts.NameSort:
fn = NameSort
default:
fn = NameSort // Default should be sorted, not unsorted.
}
if fn != nil {
if opts.ReverSort {
sort.Sort(sort.Reverse(ByFunc{node.nodes, fn}))
} else {
sort.Sort(ByFunc{node.nodes, fn})
}
}
}
// Print nodes based on the given configuration.
func (node *Node) Print(opts *Options) { node.print("", opts) }
func dirRecursiveSize(opts *Options, node *Node) (size int64, err error) {
if opts.DeepLevel > 0 && node.depth >= opts.DeepLevel {
err = errors.New("Depth too high")
}
for _, nnode := range node.nodes {
if nnode.err != nil {
err = nnode.err
continue
}
if !nnode.IsDir() {
size += nnode.Size()
} else {
nsize, e := dirRecursiveSize(opts, nnode)
size += nsize
if e != nil {
err = e
}
}
}
return
}
func (node *Node) print(indent string, opts *Options) {
if node.err != nil {
err := node.err.Error()
if msgs := strings.Split(err, ": "); len(msgs) > 1 {
err = msgs[1]
}
fmt.Printf("%s [%s]\n", node.path, err)
return
}
if !node.IsDir() {
var props []string
ok, inode, device, uid, gid := getStat(node)
// inodes
if ok && opts.Inodes {
props = append(props, fmt.Sprintf("%d", inode))
}
// device
if ok && opts.Device {
props = append(props, fmt.Sprintf("%3d", device))
}
// Mode
if opts.FileMode {
props = append(props, node.Mode().String())
}
// Owner/Uid
if ok && opts.ShowUid {
uidStr := strconv.Itoa(int(uid))
if u, err := user.LookupId(uidStr); err != nil {
props = append(props, fmt.Sprintf("%-8s", uidStr))
} else {
props = append(props, fmt.Sprintf("%-8s", u.Username))
}
}
// Gorup/Gid
// TODO: support groupname
if ok && opts.ShowGid {
gidStr := strconv.Itoa(int(gid))
props = append(props, fmt.Sprintf("%-4s", gidStr))
}
// Size
if opts.ByteSize || opts.UnitSize {
var size string
if opts.UnitSize {
size = fmt.Sprintf("%4s", formatBytes(node.Size()))
} else {
size = fmt.Sprintf("%11d", node.Size())
}
props = append(props, size)
}
// Last modification
if opts.LastMod {
props = append(props, node.ModTime().Format("Jan 02 15:04"))
}
// Print properties
if len(props) > 0 {
fmt.Fprintf(opts.OutFile, "[%s] ", strings.Join(props, " "))
}
} else {
var props []string
// Size
if opts.ByteSize || opts.UnitSize {
var size string
rsize, err := dirRecursiveSize(opts, node)
if err != nil && rsize <= 0 {
if opts.UnitSize {
size = "????"
} else {
size = "???????????"
}
} else if opts.UnitSize {
size = fmt.Sprintf("%4s", formatBytes(rsize))
} else {
size = fmt.Sprintf("%11d", rsize)
}
props = append(props, size)
}
// Print properties
if len(props) > 0 {
fmt.Fprintf(opts.OutFile, "[%s] ", strings.Join(props, " "))
}
}
// name/path
var name string
if node.depth == 0 || opts.FullPath {
name = node.path
} else {
name = node.Name()
}
// Quotes
if opts.Quotes {
name = fmt.Sprintf("\"%s\"", name)
}
// Colorize
if opts.Colorize {
name = ANSIColor(node, name)
}
// IsSymlink
if node.Mode()&os.ModeSymlink == os.ModeSymlink {
vtarget, err := os.Readlink(node.path)
if err != nil {
vtarget = node.path
}
targetPath, err := filepath.EvalSymlinks(node.path)
if err != nil {
targetPath = vtarget
}
fi, err := opts.Fs.Stat(targetPath)
if opts.Colorize && fi != nil {
vtarget = ANSIColor(&Node{FileInfo: fi, path: vtarget}, vtarget)
}
name = fmt.Sprintf("%s -> %s", name, vtarget)
// Follow symbolic links like directories
if opts.FollowLink {
path, err := filepath.Abs(targetPath)
if err == nil && fi != nil && fi.IsDir() {
if _, ok := node.vpaths[filepath.Clean(path)]; !ok {
inf := &Node{FileInfo: fi, path: targetPath}
inf.vpaths = node.vpaths
inf.Visit(opts)
node.nodes = inf.nodes
} else {
name += " [recursive, not followed]"
}
}
}
}
// Print file details
// the main idea of the print logic came from here: github.com/campoy/tools/tree
fmt.Fprintln(opts.OutFile, name)
add := "│ "
for i, nnode := range node.nodes {
if opts.NoIndent {
add = ""
} else {
if i == len(node.nodes)-1 {
fmt.Fprintf(opts.OutFile, indent+"└── ")
add = " "
} else {
fmt.Fprintf(opts.OutFile, indent+"├── ")
}
}
nnode.print(indent+add, opts)
}
}
const (
_ = iota // ignore first value by assigning to blank identifier
KB int64 = 1 << (10 * iota)
MB
GB
TB
PB
EB
)
// Convert bytes to human readable string. Like a 2 MB, 64.2 KB, 52 B
func formatBytes(i int64) (result string) {
var n float64
sFmt, eFmt := "%.01f", ""
switch {
case i > EB:
eFmt = "E"
n = float64(i) / float64(EB)
case i > PB:
eFmt = "P"
n = float64(i) / float64(PB)
case i > TB:
eFmt = "T"
n = float64(i) / float64(TB)
case i > GB:
eFmt = "G"
n = float64(i) / float64(GB)
case i > MB:
eFmt = "M"
n = float64(i) / float64(MB)
case i > KB:
eFmt = "K"
n = float64(i) / float64(KB)
default:
sFmt = "%.0f"
n = float64(i)
}
if eFmt != "" && n >= 10 {
sFmt = "%.0f"
}
result = fmt.Sprintf(sFmt+eFmt, n)
result = strings.Trim(result, " ")
return
}