399 lines
8.2 KiB
Go
399 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
|
|
}
|
|
// increase dirs only if it's a dir, but not the root.
|
|
if node.depth != 0 {
|
|
dirs++
|
|
}
|
|
// DeepLevel option
|
|
if opts.DeepLevel > 0 && opts.DeepLevel <= node.depth {
|
|
return
|
|
}
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|