mirror of
https://github.com/rclone/rclone.git
synced 2024-11-26 18:34:41 +01:00
3641993fab
Since rclone version 1.61.0 the tree command uses ANSI color sequences in output by default, but this lead to issues in Windows terminals that were not handling these (#6668). This commit ensures the tree command uses the terminal package for output. It relies on go-colorable to properly handle ANSI color sequences: If stdout is connected to a terminal the escape sequences are decoded and the text are written with color formatting using Windows Console API. If stdout is not connected to a terminal, e.g. redirected to file, the escape sequences are stripped off. The tree command has its own method for writing directly to a file, specified with flag --output, and then the output is not passed through the terminal package and must therefore be written without ansi codes.
246 lines
7.9 KiB
Go
246 lines
7.9 KiB
Go
// Package tree provides the tree command.
|
|
package tree
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/a8m/tree"
|
|
"github.com/rclone/rclone/cmd"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/config/flags"
|
|
"github.com/rclone/rclone/fs/dirtree"
|
|
"github.com/rclone/rclone/fs/log"
|
|
"github.com/rclone/rclone/fs/walk"
|
|
"github.com/rclone/rclone/lib/terminal"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
opts tree.Options
|
|
outFileName string
|
|
noReport bool
|
|
sort string
|
|
)
|
|
|
|
func init() {
|
|
cmd.Root.AddCommand(commandDefinition)
|
|
cmdFlags := commandDefinition.Flags()
|
|
// List
|
|
flags.BoolVarP(cmdFlags, &opts.All, "all", "a", false, "All files are listed (list . files too)")
|
|
flags.BoolVarP(cmdFlags, &opts.DirsOnly, "dirs-only", "d", false, "List directories only")
|
|
flags.BoolVarP(cmdFlags, &opts.FullPath, "full-path", "", false, "Print the full path prefix for each file")
|
|
//flags.BoolVarP(cmdFlags, &opts.IgnoreCase, "ignore-case", "", false, "Ignore case when pattern matching")
|
|
flags.BoolVarP(cmdFlags, &noReport, "noreport", "", false, "Turn off file/directory count at end of tree listing")
|
|
// flags.BoolVarP(cmdFlags, &opts.FollowLink, "follow", "l", false, "Follow symbolic links like directories")
|
|
flags.IntVarP(cmdFlags, &opts.DeepLevel, "level", "", 0, "Descend only level directories deep")
|
|
// flags.StringVarP(cmdFlags, &opts.Pattern, "pattern", "P", "", "List only those files that match the pattern given")
|
|
// flags.StringVarP(cmdFlags, &opts.IPattern, "exclude", "", "", "Do not list files that match the given pattern")
|
|
flags.StringVarP(cmdFlags, &outFileName, "output", "o", "", "Output to file instead of stdout")
|
|
// Files
|
|
flags.BoolVarP(cmdFlags, &opts.ByteSize, "size", "s", false, "Print the size in bytes of each file.")
|
|
flags.BoolVarP(cmdFlags, &opts.FileMode, "protections", "p", false, "Print the protections for each file.")
|
|
// flags.BoolVarP(cmdFlags, &opts.ShowUid, "uid", "", false, "Displays file owner or UID number.")
|
|
// flags.BoolVarP(cmdFlags, &opts.ShowGid, "gid", "", false, "Displays file group owner or GID number.")
|
|
flags.BoolVarP(cmdFlags, &opts.Quotes, "quote", "Q", false, "Quote filenames with double quotes.")
|
|
flags.BoolVarP(cmdFlags, &opts.LastMod, "modtime", "D", false, "Print the date of last modification.")
|
|
// flags.BoolVarP(cmdFlags, &opts.Inodes, "inodes", "", false, "Print inode number of each file.")
|
|
// flags.BoolVarP(cmdFlags, &opts.Device, "device", "", false, "Print device ID number to which each file belongs.")
|
|
// Sort
|
|
flags.BoolVarP(cmdFlags, &opts.NoSort, "unsorted", "U", false, "Leave files unsorted")
|
|
flags.BoolVarP(cmdFlags, &opts.VerSort, "version", "", false, "Sort files alphanumerically by version")
|
|
flags.BoolVarP(cmdFlags, &opts.ModSort, "sort-modtime", "t", false, "Sort files by last modification time")
|
|
flags.BoolVarP(cmdFlags, &opts.CTimeSort, "sort-ctime", "", false, "Sort files by last status change time")
|
|
flags.BoolVarP(cmdFlags, &opts.ReverSort, "sort-reverse", "r", false, "Reverse the order of the sort")
|
|
flags.BoolVarP(cmdFlags, &opts.DirSort, "dirsfirst", "", false, "List directories before files (-U disables)")
|
|
flags.StringVarP(cmdFlags, &sort, "sort", "", "", "Select sort: name,version,size,mtime,ctime")
|
|
// Graphics
|
|
flags.BoolVarP(cmdFlags, &opts.NoIndent, "noindent", "", false, "Don't print indentation lines")
|
|
}
|
|
|
|
var commandDefinition = &cobra.Command{
|
|
Use: "tree remote:path",
|
|
Short: `List the contents of the remote in a tree like fashion.`,
|
|
Long: `
|
|
rclone tree lists the contents of a remote in a similar way to the
|
|
unix tree command.
|
|
|
|
For example
|
|
|
|
$ rclone tree remote:path
|
|
/
|
|
├── file1
|
|
├── file2
|
|
├── file3
|
|
└── subdir
|
|
├── file4
|
|
└── file5
|
|
|
|
1 directories, 5 files
|
|
|
|
You can use any of the filtering options with the tree command (e.g.
|
|
` + "`--include` and `--exclude`" + `. You can also use ` + "`--fast-list`" + `.
|
|
|
|
The tree command has many options for controlling the listing which
|
|
are compatible with the tree command, for example you can include file
|
|
sizes with ` + "`--size`" + `. Note that not all of them have
|
|
short options as they conflict with rclone's short options.
|
|
|
|
For a more interactive navigation of the remote see the
|
|
[ncdu](/commands/rclone_ncdu/) command.
|
|
`,
|
|
Annotations: map[string]string{
|
|
"versionIntroduced": "v1.38",
|
|
},
|
|
RunE: func(command *cobra.Command, args []string) error {
|
|
cmd.CheckArgs(1, 1, command, args)
|
|
fsrc := cmd.NewFsSrc(args)
|
|
ci := fs.GetConfig(context.Background())
|
|
var outFile io.Writer
|
|
if outFileName != "" {
|
|
var err error
|
|
outFile, err = os.Create(outFileName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create output file: %w", err)
|
|
}
|
|
opts.Colorize = false
|
|
} else {
|
|
terminal.Start()
|
|
outFile = terminal.Out
|
|
opts.Colorize = true
|
|
}
|
|
opts.VerSort = opts.VerSort || sort == "version"
|
|
opts.ModSort = opts.ModSort || sort == "mtime"
|
|
opts.CTimeSort = opts.CTimeSort || sort == "ctime"
|
|
opts.NameSort = sort == "name"
|
|
opts.SizeSort = sort == "size"
|
|
opts.UnitSize = ci.HumanReadable
|
|
if opts.DeepLevel == 0 {
|
|
opts.DeepLevel = ci.MaxDepth
|
|
}
|
|
cmd.Run(false, false, command, func() error {
|
|
return Tree(fsrc, outFile, &opts)
|
|
})
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// Tree lists fsrc to outFile using the Options passed in
|
|
func Tree(fsrc fs.Fs, outFile io.Writer, opts *tree.Options) error {
|
|
dirs, err := walk.NewDirTree(context.Background(), fsrc, "", false, opts.DeepLevel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Fs = NewFs(dirs)
|
|
opts.OutFile = outFile
|
|
inf := tree.New("/")
|
|
var nd, nf int
|
|
if d, f := inf.Visit(opts); f != 0 {
|
|
nd, nf = nd+d, nf+f
|
|
}
|
|
inf.Print(opts)
|
|
// Print footer report
|
|
if !noReport {
|
|
footer := fmt.Sprintf("\n%d directories", nd)
|
|
if !opts.DirsOnly {
|
|
footer += fmt.Sprintf(", %d files", nf)
|
|
}
|
|
_, _ = fmt.Fprintln(outFile, footer)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FileInfo maps an fs.DirEntry into an os.FileInfo
|
|
type FileInfo struct {
|
|
entry fs.DirEntry
|
|
}
|
|
|
|
// Name is base name of the file
|
|
func (to *FileInfo) Name() string {
|
|
return path.Base(to.entry.Remote())
|
|
}
|
|
|
|
// Size in bytes for regular files; system-dependent for others
|
|
func (to *FileInfo) Size() int64 {
|
|
return to.entry.Size()
|
|
}
|
|
|
|
// Mode is file mode bits
|
|
func (to *FileInfo) Mode() os.FileMode {
|
|
if to.IsDir() {
|
|
return os.FileMode(0777)
|
|
}
|
|
return os.FileMode(0666)
|
|
}
|
|
|
|
// ModTime is modification time
|
|
func (to *FileInfo) ModTime() time.Time {
|
|
return to.entry.ModTime(context.Background())
|
|
}
|
|
|
|
// IsDir is abbreviation for Mode().IsDir()
|
|
func (to *FileInfo) IsDir() bool {
|
|
_, ok := to.entry.(fs.Directory)
|
|
return ok
|
|
}
|
|
|
|
// Sys is underlying data source (can return nil)
|
|
func (to *FileInfo) Sys() interface{} {
|
|
return nil
|
|
}
|
|
|
|
// String returns the full path
|
|
func (to *FileInfo) String() string {
|
|
return to.entry.Remote()
|
|
}
|
|
|
|
// Fs maps an fs.Fs into a tree.Fs
|
|
type Fs dirtree.DirTree
|
|
|
|
// NewFs creates a new tree
|
|
func NewFs(dirs dirtree.DirTree) Fs {
|
|
return Fs(dirs)
|
|
}
|
|
|
|
// Stat returns info about the file
|
|
func (dirs Fs) Stat(filePath string) (fi os.FileInfo, err error) {
|
|
defer log.Trace(nil, "filePath=%q", filePath)("fi=%+v, err=%v", &fi, &err)
|
|
filePath = filepath.ToSlash(filePath)
|
|
filePath = strings.TrimLeft(filePath, "/")
|
|
if filePath == "" {
|
|
return &FileInfo{fs.NewDir("", time.Now())}, nil
|
|
}
|
|
_, entry := dirtree.DirTree(dirs).Find(filePath)
|
|
if entry == nil {
|
|
return nil, fmt.Errorf("couldn't find %q in directory cache", filePath)
|
|
}
|
|
return &FileInfo{entry}, nil
|
|
}
|
|
|
|
// ReadDir returns info about the directory and fills up the directory cache
|
|
func (dirs Fs) ReadDir(dir string) (names []string, err error) {
|
|
defer log.Trace(nil, "dir=%s", dir)("names=%+v, err=%v", &names, &err)
|
|
dir = filepath.ToSlash(dir)
|
|
dir = strings.TrimLeft(dir, "/")
|
|
entries, ok := dirs[dir]
|
|
if !ok {
|
|
return nil, fmt.Errorf("couldn't find directory %q", dir)
|
|
}
|
|
for _, entry := range entries {
|
|
names = append(names, path.Base(entry.Remote()))
|
|
}
|
|
return
|
|
}
|
|
|
|
// check interfaces
|
|
var (
|
|
_ tree.Fs = (*Fs)(nil)
|
|
_ os.FileInfo = (*FileInfo)(nil)
|
|
)
|