mirror of
https://github.com/rclone/rclone
synced 2024-12-24 15:43:45 +01:00
Implement tree command - fixes #1528
This commit is contained in:
parent
bfef0bc2e9
commit
f3060caf04
@ -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
0
cmd/tree/testfiles/file1
Normal file
0
cmd/tree/testfiles/file2
Normal file
0
cmd/tree/testfiles/file2
Normal file
0
cmd/tree/testfiles/file3
Normal file
0
cmd/tree/testfiles/file3
Normal file
0
cmd/tree/testfiles/subdir/file4
Normal file
0
cmd/tree/testfiles/subdir/file4
Normal file
0
cmd/tree/testfiles/subdir/file5
Normal file
0
cmd/tree/testfiles/subdir/file5
Normal file
227
cmd/tree/tree.go
Normal file
227
cmd/tree/tree.go
Normal 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
36
cmd/tree/tree_test.go
Normal 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())
|
||||
}
|
Loading…
Reference in New Issue
Block a user