From 41876dd66987a209b54346066d2db6378a5decc0 Mon Sep 17 00:00:00 2001 From: albertony <12441419+albertony@users.noreply.github.com> Date: Sat, 22 May 2021 21:06:24 +0200 Subject: [PATCH] touch: add support for touching files in directory, with options for recursive, filtering and dry-run/interactive Fixes #5301 --- cmd/touch/touch.go | 137 +++++++++++++++++++++---------- cmd/touch/touch_test.go | 7 +- fs/operations/operations.go | 17 ++++ fs/operations/operations_test.go | 19 +++++ 4 files changed, 132 insertions(+), 48 deletions(-) diff --git a/cmd/touch/touch.go b/cmd/touch/touch.go index c96348cd2..82d49eec2 100644 --- a/cmd/touch/touch.go +++ b/cmd/touch/touch.go @@ -10,6 +10,7 @@ import ( "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/config/flags" "github.com/rclone/rclone/fs/object" + "github.com/rclone/rclone/fs/operations" "github.com/spf13/cobra" ) @@ -17,87 +18,139 @@ var ( notCreateNewFile bool timeAsArgument string localTime bool + recursive bool ) const ( defaultLayout string = "060102" - layoutDateWithTime = "2006-01-02T15:04:05" - layoutDateWithTimeNano = "2006-01-02T15:04:05.999999999" + layoutDateWithTime string = "2006-01-02T15:04:05" + layoutDateWithTimeNano string = "2006-01-02T15:04:05.999999999" ) func init() { cmd.Root.AddCommand(commandDefinition) cmdFlags := commandDefinition.Flags() - flags.BoolVarP(cmdFlags, ¬CreateNewFile, "no-create", "C", false, "Do not create the file if it does not exist.") + flags.BoolVarP(cmdFlags, ¬CreateNewFile, "no-create", "C", false, "Do not create the file if it does not exist. Implied with --recursive.") flags.StringVarP(cmdFlags, &timeAsArgument, "timestamp", "t", "", "Use specified time instead of the current time of day.") flags.BoolVarP(cmdFlags, &localTime, "localtime", "", false, "Use localtime for timestamp, not UTC.") + flags.BoolVarP(cmdFlags, &recursive, "recursive", "R", false, "Recursively touch all files.") } var commandDefinition = &cobra.Command{ Use: "touch remote:path", Short: `Create new file or change file modification time.`, Long: ` -Set the modification time on object(s) as specified by remote:path to +Set the modification time on file(s) as specified by remote:path to have the current time. -If remote:path does not exist then a zero sized object will be created -unless the --no-create flag is provided. +If remote:path does not exist then a zero sized file will be created, +unless ` + "`--no-create`" + ` or ` + "`--recursive`" + ` is provided. -If --timestamp is used then it will set the modification time to that +If ` + "`--recursive`" + ` is used then recursively sets the modification +time on all existing files that is found under the path. Filters are supported, +and you can test with the ` + "`--dry-run`" + ` or the ` + "`--interactive`" + ` flag. + +If ` + "`--timestamp`" + ` is used then sets the modification time to that time instead of the current time. Times may be specified as one of: - 'YYMMDD' - e.g. 17.10.30 - 'YYYY-MM-DDTHH:MM:SS' - e.g. 2006-01-02T15:04:05 - 'YYYY-MM-DDTHH:MM:SS.SSS' - e.g. 2006-01-02T15:04:05.123456789 -Note that --timestamp is in UTC if you want local time then add the ---localtime flag. +Note that value of ` + "`--timestamp`" + ` is in UTC. If you want local time +then add the ` + "`--localtime`" + ` flag. `, Run: func(command *cobra.Command, args []string) { cmd.CheckArgs(1, 1, command, args) - fsrc, srcFileName := cmd.NewFsDstFile(args) + f, fileName := cmd.NewFsFile(args[0]) cmd.Run(true, false, command, func() error { - return Touch(context.Background(), fsrc, srcFileName) + return Touch(context.Background(), f, fileName) }) }, } -//Touch create new file or change file modification time. -func Touch(ctx context.Context, fsrc fs.Fs, srcFileName string) (err error) { - timeAtr := time.Now() - if timeAsArgument != "" { - layout := defaultLayout - if len(timeAsArgument) == len(layoutDateWithTime) { - layout = layoutDateWithTime - } else if len(timeAsArgument) > len(layoutDateWithTime) { - layout = layoutDateWithTimeNano - } - var timeAtrFromFlags time.Time - if localTime { - timeAtrFromFlags, err = time.ParseInLocation(layout, timeAsArgument, time.Local) - } else { - timeAtrFromFlags, err = time.Parse(layout, timeAsArgument) - } - if err != nil { - return errors.Wrap(err, "failed to parse date/time argument") - } - timeAtr = timeAtrFromFlags +// parseTimeArgument parses a timestamp string according to specific layouts +func parseTimeArgument(timeString string) (time.Time, error) { + layout := defaultLayout + if len(timeString) == len(layoutDateWithTime) { + layout = layoutDateWithTime + } else if len(timeString) > len(layoutDateWithTime) { + layout = layoutDateWithTimeNano } - file, err := fsrc.NewObject(ctx, srcFileName) + if localTime { + return time.ParseInLocation(layout, timeString, time.Local) + } + return time.Parse(layout, timeString) +} + +// timeOfTouch returns the time value set on files +func timeOfTouch() (time.Time, error) { + var t time.Time + if timeAsArgument != "" { + var err error + if t, err = parseTimeArgument(timeAsArgument); err != nil { + return t, errors.Wrap(err, "failed to parse timestamp argument") + } + } else { + t = time.Now() + } + return t, nil +} + +// createEmptyObject creates an empty object (file) with specified timestamp +func createEmptyObject(ctx context.Context, remote string, modTime time.Time, f fs.Fs) error { + var buffer []byte + src := object.NewStaticObjectInfo(remote, modTime, int64(len(buffer)), true, nil, f) + _, err := f.Put(ctx, bytes.NewBuffer(buffer), src) + return err +} + +// Touch create new file or change file modification time. +func Touch(ctx context.Context, f fs.Fs, fileName string) error { + t, err := timeOfTouch() if err != nil { - if !notCreateNewFile { - var buffer []byte - src := object.NewStaticObjectInfo(srcFileName, timeAtr, int64(len(buffer)), true, nil, fsrc) - _, err = fsrc.Put(ctx, bytes.NewBuffer(buffer), src) - if err != nil { - return err + return err + } + fs.Debugf(nil, "Touch time %v", t) + file, err := f.NewObject(ctx, fileName) + if err != nil { + if errors.Cause(err) == fs.ErrorObjectNotFound { + // Touch single non-existent file + if notCreateNewFile { + fs.Logf(f, "Not touching non-existent file due to --no-create") + return nil + } + if recursive { + fs.Logf(f, "Not touching non-existent file due to --recursive") + return nil + } + if operations.SkipDestructive(ctx, f, "touch (create)") { + return nil + } + fs.Debugf(f, "Touching (creating)") + if err = createEmptyObject(ctx, fileName, t, f); err != nil { + return errors.Wrap(err, "failed to touch (create)") } } - return nil + if errors.Cause(err) == fs.ErrorNotAFile { + if recursive { + // Touch existing directory, recursive + fs.Debugf(nil, "Touching files in directory recursively") + return operations.TouchDir(ctx, f, t, true) + } + // Touch existing directory without recursing + fs.Debugf(nil, "Touching files in directory non-recursively") + return operations.TouchDir(ctx, f, t, false) + } + return err } - err = file.SetModTime(ctx, timeAtr) - if err != nil { - return errors.Wrap(err, "touch: couldn't set mod time") + // Touch single existing file + if !operations.SkipDestructive(ctx, fileName, "touch") { + fs.Debugf(f, "Touching %q", fileName) + err = file.SetModTime(ctx, t) + if err != nil { + return errors.Wrap(err, "failed to touch") + } } return nil } diff --git a/cmd/touch/touch_test.go b/cmd/touch/touch_test.go index 438bfb358..a56dc7c9e 100644 --- a/cmd/touch/touch_test.go +++ b/cmd/touch/touch_test.go @@ -3,7 +3,6 @@ package touch import ( "context" "testing" - "time" _ "github.com/rclone/rclone/backend/local" "github.com/rclone/rclone/fs" @@ -16,11 +15,7 @@ var ( ) func checkFile(t *testing.T, r fs.Fs, path string, content string) { - layout := defaultLayout - if len(timeAsArgument) == len(layoutDateWithTime) { - layout = layoutDateWithTime - } - timeAtrFromFlags, err := time.Parse(layout, timeAsArgument) + timeAtrFromFlags, err := timeOfTouch() require.NoError(t, err) file1 := fstest.NewItem(path, content, timeAtrFromFlags) fstest.CheckItems(t, r, file1) diff --git a/fs/operations/operations.go b/fs/operations/operations.go index 4b5667d49..98a13cc62 100644 --- a/fs/operations/operations.go +++ b/fs/operations/operations.go @@ -1906,6 +1906,23 @@ func SetTier(ctx context.Context, fsrc fs.Fs, tier string) error { }) } +// TouchDir touches every file in f with time t +func TouchDir(ctx context.Context, f fs.Fs, t time.Time, recursive bool) error { + return walk.ListR(ctx, f, "", false, ConfigMaxDepth(ctx, recursive), walk.ListObjects, func(entries fs.DirEntries) error { + entries.ForObject(func(o fs.Object) { + if !SkipDestructive(ctx, o, "touch") { + fs.Debugf(f, "Touching %q", o.Remote()) + err := o.SetModTime(ctx, t) + if err != nil { + err = fs.CountError(err) + fs.Errorf(o, "Failed to touch %v", err) + } + } + }) + return nil + }) +} + // ListFormat defines files information print format type ListFormat struct { separator string diff --git a/fs/operations/operations_test.go b/fs/operations/operations_test.go index 703aa20f2..cdfe6df0d 100644 --- a/fs/operations/operations_test.go +++ b/fs/operations/operations_test.go @@ -1573,3 +1573,22 @@ func TestCopyFileMaxTransfer(t *testing.T) { fstest.CheckItems(t, r.Flocal, file1, file2, file3, file4) fstest.CheckItems(t, r.Fremote, file1, file4) } + +func TestTouchDir(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + defer r.Finalise() + + file1 := r.WriteBoth(ctx, "potato2", "------------------------------------------------------------", t1) + file2 := r.WriteBoth(ctx, "empty space", "-", t2) + file3 := r.WriteBoth(ctx, "sub dir/potato3", "hello", t2) + fstest.CheckItems(t, r.Fremote, file1, file2, file3) + + timeValue := time.Date(2010, 9, 8, 7, 6, 5, 4, time.UTC) + err := operations.TouchDir(ctx, r.Fremote, timeValue, true) + require.NoError(t, err) + file1.ModTime = timeValue + file2.ModTime = timeValue + file3.ModTime = timeValue + fstest.CheckItems(t, r.Fremote, file1, file2, file3) +}