Implement tree command - fixes #1528

s3-about
Nick Craig-Wood 2017-07-24 12:39:39 +01:00
parent bfef0bc2e9
commit f3060caf04
8 changed files with 264 additions and 0 deletions

View File

@ -39,5 +39,6 @@ import (
_ "github.com/ncw/rclone/cmd/sha1sum"
_ "github.com/ncw/rclone/cmd/size"
_ "github.com/ncw/rclone/cmd/sync"
_ "github.com/ncw/rclone/cmd/tree"
_ "github.com/ncw/rclone/cmd/version"
)

0
cmd/tree/testfiles/file1 Normal file
View File

0
cmd/tree/testfiles/file2 Normal file
View File

0
cmd/tree/testfiles/file3 Normal file
View File

View File

View File

227
cmd/tree/tree.go Normal file
View File

@ -0,0 +1,227 @@
package tree
import (
"fmt"
"io"
"log"
"os"
"path"
"strings"
"time"
"github.com/a8m/tree"
"github.com/ncw/rclone/cmd"
"github.com/ncw/rclone/fs"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
var (
opts tree.Options
outFileName string
noReport bool
sort string
)
func init() {
cmd.Root.AddCommand(commandDefintion)
flags := commandDefintion.Flags()
// List
flags.BoolVarP(&opts.All, "all", "a", false, "All files are listed (list . files too).")
flags.BoolVarP(&opts.DirsOnly, "dirs-only", "d", false, "List directories only.")
flags.BoolVarP(&opts.FullPath, "full-path", "", false, "Print the full path prefix for each file.")
//flags.BoolVarP(&opts.IgnoreCase, "ignore-case", "", false, "Ignore case when pattern matching.")
flags.BoolVarP(&noReport, "noreport", "", false, "Turn off file/directory count at end of tree listing.")
// flags.BoolVarP(&opts.FollowLink, "follow", "l", false, "Follow symbolic links like directories.")
flags.IntVarP(&opts.DeepLevel, "level", "", 0, "Descend only level directories deep.")
// flags.StringVarP(&opts.Pattern, "pattern", "P", "", "List only those files that match the pattern given.")
// flags.StringVarP(&opts.IPattern, "exclude", "", "", "Do not list files that match the given pattern.")
flags.StringVarP(&outFileName, "output", "o", "", "Output to file instead of stdout.")
// Files
flags.BoolVarP(&opts.ByteSize, "size", "s", false, "Print the size in bytes of each file.")
flags.BoolVarP(&opts.UnitSize, "human", "", false, "Print the size in a more human readable way.")
flags.BoolVarP(&opts.FileMode, "protections", "p", false, "Print the protections for each file.")
// flags.BoolVarP(&opts.ShowUid, "uid", "", false, "Displays file owner or UID number.")
// flags.BoolVarP(&opts.ShowGid, "gid", "", false, "Displays file group owner or GID number.")
flags.BoolVarP(&opts.Quotes, "quote", "Q", false, "Quote filenames with double quotes.")
flags.BoolVarP(&opts.LastMod, "modtime", "D", false, "Print the date of last modification.")
// flags.BoolVarP(&opts.Inodes, "inodes", "", false, "Print inode number of each file.")
// flags.BoolVarP(&opts.Device, "device", "", false, "Print device ID number to which each file belongs.")
// Sort
flags.BoolVarP(&opts.NoSort, "unsorted", "U", false, "Leave files unsorted.")
flags.BoolVarP(&opts.VerSort, "version", "", false, "Sort files alphanumerically by version.")
flags.BoolVarP(&opts.ModSort, "sort-modtime", "t", false, "Sort files by last modification time.")
flags.BoolVarP(&opts.CTimeSort, "sort-ctime", "", false, "Sort files by last status change time.")
flags.BoolVarP(&opts.ReverSort, "sort-reverse", "r", false, "Reverse the order of the sort.")
flags.BoolVarP(&opts.DirSort, "dirsfirst", "", false, "List directories before files (-U disables).")
flags.StringVarP(&sort, "sort", "", "", "Select sort: name,version,size,mtime,ctime.")
// Graphics
flags.BoolVarP(&opts.NoIndent, "noindent", "i", false, "Don't print indentation lines.")
flags.BoolVarP(&opts.Colorize, "color", "C", false, "Turn colorization on always.")
}
var commandDefintion = &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 (eg
--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. Note that not all of them have
short options as they conflict with rclone's short options.
`,
Run: func(command *cobra.Command, args []string) {
cmd.CheckArgs(1, 1, command, args)
fsrc := cmd.NewFsSrc(args)
outFile := os.Stdout
if outFileName != "" {
var err error
outFile, err = os.Create(outFileName)
if err != nil {
log.Fatalf("Failed to create output file: %v", err)
}
}
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"
if opts.DeepLevel == 0 {
opts.DeepLevel = fs.Config.MaxDepth
}
cmd.Run(false, false, command, func() error {
return Tree(fsrc, outFile, &opts)
})
},
}
// Tree lists fsrc to outFile using the Options passed in
func Tree(fsrc fs.Fs, outFile io.Writer, opts *tree.Options) error {
dirs, err := fs.NewDirTree(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 {
if d > 0 {
d--
}
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 a 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()
}
// 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 fs.DirTree
// NewFs creates a new tree
func NewFs(dirs fs.DirTree) Fs {
return Fs(dirs)
}
// Stat returns info about the file
func (dirs Fs) Stat(filePath string) (fi os.FileInfo, err error) {
defer fs.Trace(nil, "filePath=%q", filePath)("fi=%+v, err=%v", &fi, &err)
filePath = strings.TrimLeft(filePath, "/")
if filePath == "" {
return &FileInfo{fs.NewDir("", time.Now())}, nil
}
_, entry := fs.DirTree(dirs).Find(filePath)
if entry == nil {
return nil, errors.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 fs.Trace(nil, "dir=%s", dir)("names=%+v, err=%v", &names, &err)
dir = strings.TrimLeft(dir, "/")
entries, ok := dirs[dir]
if !ok {
return nil, errors.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)
)

36
cmd/tree/tree_test.go Normal file
View File

@ -0,0 +1,36 @@
package tree
import (
"bytes"
"testing"
"github.com/a8m/tree"
"github.com/ncw/rclone/fs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
_ "github.com/ncw/rclone/local"
)
func TestTree(t *testing.T) {
buf := new(bytes.Buffer)
// Never ask for passwords, fail instead.
// If your local config is encrypted set environment variable
// "RCLONE_CONFIG_PASS=hunter2" (or your password)
*fs.AskPassword = false
fs.LoadConfig()
f, err := fs.NewFs("testfiles")
require.NoError(t, err)
err = Tree(f, buf, new(tree.Options))
require.NoError(t, err)
assert.Equal(t, `/
file1
file2
file3
subdir
file4
file5
1 directories, 5 files
`, buf.String())
}