diff --git a/cmd/all/all.go b/cmd/all/all.go index 7e52e4c1d..5dd2cce12 100644 --- a/cmd/all/all.go +++ b/cmd/all/all.go @@ -25,6 +25,7 @@ import ( _ "github.com/ncw/rclone/cmd/ls" _ "github.com/ncw/rclone/cmd/ls2" _ "github.com/ncw/rclone/cmd/lsd" + _ "github.com/ncw/rclone/cmd/lsf" _ "github.com/ncw/rclone/cmd/lsjson" _ "github.com/ncw/rclone/cmd/lsl" _ "github.com/ncw/rclone/cmd/md5sum" diff --git a/cmd/lsf/lsf.go b/cmd/lsf/lsf.go new file mode 100644 index 000000000..c3d22ce74 --- /dev/null +++ b/cmd/lsf/lsf.go @@ -0,0 +1,78 @@ +package lsf + +import ( + "fmt" + "io" + "os" + + "github.com/ncw/rclone/cmd" + "github.com/ncw/rclone/fs" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + format string + separator string + dirSlash bool + recurse bool +) + +func init() { + cmd.Root.AddCommand(commandDefintion) + flags := commandDefintion.Flags() + flags.StringVarP(&format, "format", "F", "", "Output format.") + flags.StringVarP(&separator, "separator", "s", "", "Separator.") + flags.BoolVarP(&dirSlash, "dir-slash", "d", false, "Dir name contains slash one the end.") + commandDefintion.Flags().BoolVarP(&recurse, "recursive", "R", false, "Recurse into the listing.") +} + +var commandDefintion = &cobra.Command{ + Use: "lsf remote:path", + Short: `List all the objects in the path with modification time, size and path in specific format: 'p' - path, 's' - size, 't' - modification time, ex. 'tsp'. Default output contains only path. If format is empty, dir-slash flag is always true.`, + Run: func(command *cobra.Command, args []string) { + cmd.CheckArgs(1, 1, command, args) + fsrc := cmd.NewFsSrc(args) + cmd.Run(false, false, command, func() error { + return Lsf(fsrc, os.Stdout) + }) + }, +} + +//Lsf lists all the objects in the path with modification time, size and path in specific format. +func Lsf(fsrc fs.Fs, out io.Writer) error { + return fs.Walk(fsrc, "", false, fs.ConfigMaxDepth(recurse), func(path string, entries fs.DirEntries, err error) error { + if err != nil { + fs.Stats.Error(err) + fs.Errorf(path, "error listing: %v", err) + return nil + } + if format == "" { + format = "p" + dirSlash = true + } + if separator == "" { + separator = ";" + } + var list fs.ListFormat + list.SetSeparator(separator) + list.SetDirSlash(dirSlash) + + for _, char := range format { + switch char { + case 'p': + list.AddPath() + case 't': + list.AddModTime() + case 's': + list.AddSize() + default: + return errors.Wrap(err, "failed to parse format argument") + } + } + for _, entry := range entries { + fmt.Fprintln(out, fs.ListFormatted(&entry, &list)) + } + return nil + }) +} diff --git a/cmd/lsf/lsf_test.go b/cmd/lsf/lsf_test.go new file mode 100644 index 000000000..ec2268e73 --- /dev/null +++ b/cmd/lsf/lsf_test.go @@ -0,0 +1,194 @@ +package lsf + +import ( + "bytes" + "testing" + + "github.com/ncw/rclone/fs" + "github.com/ncw/rclone/fstest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + _ "github.com/ncw/rclone/local" +) + +func TestDefaultLsf(t *testing.T) { + fstest.Initialise() + buf := new(bytes.Buffer) + + f, err := fs.NewFs("testfiles") + require.NoError(t, err) + + err = Lsf(f, buf) + require.NoError(t, err) + assert.Equal(t, `file1 +file2 +file3 +subdir/ +`, buf.String()) +} + +func TestRecurseFlag(t *testing.T) { + fstest.Initialise() + buf := new(bytes.Buffer) + + f, err := fs.NewFs("testfiles") + require.NoError(t, err) + + recurse = true + err = Lsf(f, buf) + require.NoError(t, err) + assert.Equal(t, `file1 +file2 +file3 +subdir/ +subdir/file1 +subdir/file2 +subdir/file3 +`, buf.String()) + recurse = false +} + +func TestDirSlashFlag(t *testing.T) { + fstest.Initialise() + buf := new(bytes.Buffer) + + f, err := fs.NewFs("testfiles") + require.NoError(t, err) + + dirSlash = true + format = "p" + err = Lsf(f, buf) + require.NoError(t, err) + assert.Equal(t, `file1 +file2 +file3 +subdir/ +`, buf.String()) + + buf = new(bytes.Buffer) + dirSlash = false + err = Lsf(f, buf) + require.NoError(t, err) + assert.Equal(t, `file1 +file2 +file3 +subdir +`, buf.String()) +} + +func TestFormat(t *testing.T) { + fstest.Initialise() + f, err := fs.NewFs("testfiles") + require.NoError(t, err) + + buf := new(bytes.Buffer) + format = "p" + err = Lsf(f, buf) + require.NoError(t, err) + assert.Equal(t, `file1 +file2 +file3 +subdir +`, buf.String()) + + buf = new(bytes.Buffer) + format = "s" + err = Lsf(f, buf) + require.NoError(t, err) + assert.Equal(t, `0 +321 +1234 +-1 +`, buf.String()) + + buf = new(bytes.Buffer) + format = "t" + err = Lsf(f, buf) + require.NoError(t, err) + + items, _ := fs.ListDirSorted(f, true, "") + var expectedOutput string + for _, item := range items { + expectedOutput += item.ModTime().Format("2006-01-02 15:04:05") + "\n" + } + + assert.Equal(t, expectedOutput, buf.String()) + + buf = new(bytes.Buffer) + format = "sp" + err = Lsf(f, buf) + require.NoError(t, err) + assert.Equal(t, `0;file1 +321;file2 +1234;file3 +-1;subdir +`, buf.String()) + format = "" +} + +func TestSeparator(t *testing.T) { + fstest.Initialise() + f, err := fs.NewFs("testfiles") + require.NoError(t, err) + format = "ps" + + buf := new(bytes.Buffer) + err = Lsf(f, buf) + require.NoError(t, err) + assert.Equal(t, `file1;0 +file2;321 +file3;1234 +subdir;-1 +`, buf.String()) + + separator = "__SEP__" + buf = new(bytes.Buffer) + err = Lsf(f, buf) + require.NoError(t, err) + assert.Equal(t, `file1__SEP__0 +file2__SEP__321 +file3__SEP__1234 +subdir__SEP__-1 +`, buf.String()) + format = "" + separator = "" +} + +func TestWholeLsf(t *testing.T) { + fstest.Initialise() + f, err := fs.NewFs("testfiles") + require.NoError(t, err) + format = "pst" + separator = "_+_" + recurse = true + dirSlash = true + + buf := new(bytes.Buffer) + err = Lsf(f, buf) + require.NoError(t, err) + + items, _ := fs.ListDirSorted(f, true, "") + itemsInSubdir, _ := fs.ListDirSorted(f, true, "subdir") + var expectedOutput []string + for _, item := range items { + expectedOutput = append(expectedOutput, item.ModTime().Format("2006-01-02 15:04:05")) + } + for _, item := range itemsInSubdir { + expectedOutput = append(expectedOutput, item.ModTime().Format("2006-01-02 15:04:05")) + } + + assert.Equal(t, `file1_+_0_+_`+expectedOutput[0]+` +file2_+_321_+_`+expectedOutput[1]+` +file3_+_1234_+_`+expectedOutput[2]+` +subdir/_+_-1_+_`+expectedOutput[3]+` +subdir/file1_+_0_+_`+expectedOutput[4]+` +subdir/file2_+_1_+_`+expectedOutput[5]+` +subdir/file3_+_111_+_`+expectedOutput[6]+` +`, buf.String()) + + format = "" + separator = "" + recurse = false + dirSlash = false +} diff --git a/cmd/lsf/testfiles/file1 b/cmd/lsf/testfiles/file1 new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/lsf/testfiles/file2 b/cmd/lsf/testfiles/file2 new file mode 100644 index 000000000..413a3852b Binary files /dev/null and b/cmd/lsf/testfiles/file2 differ diff --git a/cmd/lsf/testfiles/file3 b/cmd/lsf/testfiles/file3 new file mode 100644 index 000000000..b74f273b3 Binary files /dev/null and b/cmd/lsf/testfiles/file3 differ diff --git a/cmd/lsf/testfiles/subdir/file1 b/cmd/lsf/testfiles/subdir/file1 new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/lsf/testfiles/subdir/file2 b/cmd/lsf/testfiles/subdir/file2 new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/cmd/lsf/testfiles/subdir/file2 differ diff --git a/cmd/lsf/testfiles/subdir/file3 b/cmd/lsf/testfiles/subdir/file3 new file mode 100644 index 000000000..8bb9b413f Binary files /dev/null and b/cmd/lsf/testfiles/subdir/file3 differ diff --git a/fs/operations.go b/fs/operations.go index 11fc15a36..a3487cbc3 100644 --- a/fs/operations.go +++ b/fs/operations.go @@ -11,6 +11,7 @@ import ( "mime" "path" "sort" + "strconv" "strings" "sync" "sync/atomic" @@ -1784,3 +1785,66 @@ func MoveFile(fdst Fs, fsrc Fs, dstFileName string, srcFileName string) (err err func CopyFile(fdst Fs, fsrc Fs, dstFileName string, srcFileName string) (err error) { return moveOrCopyFile(fdst, fsrc, dstFileName, srcFileName, true) } + +// ListFormat defines files information print format +type ListFormat struct { + separator string + dirSlash bool + output []func() string + entry DirEntry +} + +// SetSeparator changes separator in struct +func (l *ListFormat) SetSeparator(separator string) { + l.separator = separator +} + +// SetDirSlash defines if slash should be printed +func (l *ListFormat) SetDirSlash(dirSlash bool) { + l.dirSlash = dirSlash +} + +// SetOutput sets functions used to create files information +func (l *ListFormat) SetOutput(output []func() string) { + l.output = output +} + +// AddModTime adds file's Mod Time to output +func (l *ListFormat) AddModTime() { + l.AppendOutput(func() string { return l.entry.ModTime().Format("2006-01-02 15:04:05") }) +} + +// AddSize adds file's size to output +func (l *ListFormat) AddSize() { + l.AppendOutput(func() string { return strconv.FormatInt(l.entry.Size(), 10) }) +} + +// AddPath adds path to file to output +func (l *ListFormat) AddPath() { + l.AppendOutput(func() string { + _, isDir := l.entry.(Directory) + + if isDir && l.dirSlash { + return l.entry.Remote() + "/" + } + return l.entry.Remote() + }) +} + +// AppendOutput adds string generated by specific function to printed output +func (l *ListFormat) AppendOutput(functionToAppend func() string) { + if len(l.output) > 0 { + l.output = append(l.output, func() string { return l.separator }) + } + l.output = append(l.output, functionToAppend) +} + +// ListFormatted prints information about specific file in specific format +func ListFormatted(entry *DirEntry, list *ListFormat) string { + list.entry = *entry + var out string + for _, fun := range list.output { + out += fun() + } + return out +} diff --git a/fs/operations_test.go b/fs/operations_test.go index ccff529c4..9bd504505 100644 --- a/fs/operations_test.go +++ b/fs/operations_test.go @@ -958,3 +958,45 @@ func TestCheckEqualReaders(t *testing.T) { assert.Equal(t, myErr, err) assert.Equal(t, differ, true) } + +func TestListFormat(t *testing.T) { + r := fstest.NewRun(t) + defer r.Finalise() + file1 := r.WriteObject("a", "a", t1) + file2 := r.WriteObject("subdir/b", "b", t1) + + fstest.CheckItems(t, r.Fremote, file1, file2) + + items, _ := fs.ListDirSorted(r.Fremote, true, "") + var list fs.ListFormat + list.AddPath() + list.SetDirSlash(false) + assert.Equal(t, "subdir", fs.ListFormatted(&items[1], &list)) + + list.SetDirSlash(true) + assert.Equal(t, "subdir/", fs.ListFormatted(&items[1], &list)) + + list.SetOutput(nil) + assert.Equal(t, "", fs.ListFormatted(&items[1], &list)) + + list.AppendOutput(func() string { return "a" }) + list.AppendOutput(func() string { return "b" }) + assert.Equal(t, "ab", fs.ListFormatted(&items[1], &list)) + list.SetSeparator(":::") + assert.Equal(t, "a:::b", fs.ListFormatted(&items[1], &list)) + + list.SetOutput(nil) + list.AddModTime() + assert.Equal(t, items[0].ModTime().Format("2006-01-02 15:04:05"), fs.ListFormatted(&items[0], &list)) + + list.SetOutput(nil) + list.AddSize() + assert.Equal(t, "1", fs.ListFormatted(&items[0], &list)) + + list.AddPath() + list.AddModTime() + list.SetDirSlash(true) + list.SetSeparator("__SEP__") + assert.Equal(t, "1__SEP__a__SEP__"+items[0].ModTime().Format("2006-01-02 15:04:05"), fs.ListFormatted(&items[0], &list)) + assert.Equal(t, "-1__SEP__subdir/__SEP__"+items[1].ModTime().Format("2006-01-02 15:04:05"), fs.ListFormatted(&items[1], &list)) +}