diff --git a/cmd/touch/touch.go b/cmd/touch/touch.go index 34ea22321..741c920b3 100644 --- a/cmd/touch/touch.go +++ b/cmd/touch/touch.go @@ -5,11 +5,13 @@ import ( "context" "errors" "fmt" + "log" "time" "github.com/rclone/rclone/cmd" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/config/flags" + "github.com/rclone/rclone/fs/fspath" "github.com/rclone/rclone/fs/object" "github.com/rclone/rclone/fs/operations" "github.com/spf13/cobra" @@ -63,13 +65,32 @@ then add the ` + "`--localtime`" + ` flag. `, Run: func(command *cobra.Command, args []string) { cmd.CheckArgs(1, 1, command, args) - f, fileName := cmd.NewFsFile(args[0]) + f, remote := newFsDst(args) cmd.Run(true, false, command, func() error { - return Touch(context.Background(), f, fileName) + return Touch(context.Background(), f, remote) }) }, } +// newFsDst creates a new dst fs from the arguments. +// +// The returned fs will never point to a file. It will point to the +// parent directory of specified path, and is returned together with +// the basename of file or directory, except if argument is only a +// remote name. Similar to cmd.NewFsDstFile, but without raising fatal +// when name of file or directory is empty (e.g. "remote:" or "remote:path/"). +func newFsDst(args []string) (f fs.Fs, remote string) { + root, remote, err := fspath.Split(args[0]) + if err != nil { + log.Fatalf("Parsing %q failed: %v", args[0], err) + } + if root == "" { + root = "." + } + f = cmd.NewFsDir([]string{root}) + return f, remote +} + // parseTimeArgument parses a timestamp string according to specific layouts func parseTimeArgument(timeString string) (time.Time, error) { layout := defaultLayout @@ -107,47 +128,51 @@ func createEmptyObject(ctx context.Context, remote string, modTime time.Time, f } // Touch create new file or change file modification time. -func Touch(ctx context.Context, f fs.Fs, fileName string) error { +func Touch(ctx context.Context, f fs.Fs, remote string) error { t, err := timeOfTouch() if err != nil { return err } fs.Debugf(nil, "Touch time %v", t) - file, err := f.NewObject(ctx, fileName) + file, err := f.NewObject(ctx, remote) if err != nil { if errors.Is(err, fs.ErrorObjectNotFound) { - // Touch single non-existent file + // Touching non-existant path, possibly creating it as new file + if remote == "" { + fs.Logf(f, "Not touching empty directory") + return nil + } if notCreateNewFile { fs.Logf(f, "Not touching non-existent file due to --no-create") return nil } if recursive { + // For consistency, --recursive never creates new files. 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 { + fs.Debugf(f, "Touching (creating) %q", remote) + if err = createEmptyObject(ctx, remote, t, f); err != nil { return fmt.Errorf("failed to touch (create): %w", err) } } if errors.Is(err, fs.ErrorIsDir) { + // Touching existing directory if recursive { - // Touch existing directory, recursive - fs.Debugf(nil, "Touching files in directory recursively") - return operations.TouchDir(ctx, f, t, true) + fs.Debugf(f, "Touching recursively files in directory %q", remote) + return operations.TouchDir(ctx, f, remote, t, true) } - // Touch existing directory without recursing - fs.Debugf(nil, "Touching files in directory non-recursively") - return operations.TouchDir(ctx, f, t, false) + fs.Debugf(f, "Touching non-recursively files in directory %q", remote) + return operations.TouchDir(ctx, f, remote, t, false) } return err } // Touch single existing file - if !operations.SkipDestructive(ctx, fileName, "touch") { - fs.Debugf(f, "Touching %q", fileName) + if !operations.SkipDestructive(ctx, remote, "touch") { + fs.Debugf(f, "Touching %q", remote) err = file.SetModTime(ctx, t) if err != nil { return fmt.Errorf("failed to touch: %w", err) diff --git a/cmd/touch/touch_test.go b/cmd/touch/touch_test.go index b047e63e8..415615438 100644 --- a/cmd/touch/touch_test.go +++ b/cmd/touch/touch_test.go @@ -113,6 +113,15 @@ func TestTouchCreateMultipleDirAndFile(t *testing.T) { fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{file1}, []string{"a", "a/b"}, fs.ModTimeNotSupported) } +func TestTouchEmptyName(t *testing.T) { + r := fstest.NewRun(t) + defer r.Finalise() + + err := Touch(context.Background(), r.Fremote, "") + require.NoError(t, err) + fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{}, []string{}, fs.ModTimeNotSupported) +} + func TestTouchEmptyDir(t *testing.T) { r := fstest.NewRun(t) defer r.Finalise() diff --git a/fs/operations/operations.go b/fs/operations/operations.go index dcf296b90..6066eda29 100644 --- a/fs/operations/operations.go +++ b/fs/operations/operations.go @@ -1929,9 +1929,9 @@ 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 { +// TouchDir touches every file in directory with time t +func TouchDir(ctx context.Context, f fs.Fs, remote string, t time.Time, recursive bool) error { + return walk.ListR(ctx, f, remote, 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()) diff --git a/fs/operations/operations_test.go b/fs/operations/operations_test.go index 350f6c9be..55e91850d 100644 --- a/fs/operations/operations_test.go +++ b/fs/operations/operations_test.go @@ -1636,7 +1636,7 @@ func TestTouchDir(t *testing.T) { r.CheckRemoteItems(t, file1, file2, file3) timeValue := time.Date(2010, 9, 8, 7, 6, 5, 4, time.UTC) - err := operations.TouchDir(ctx, r.Fremote, timeValue, true) + err := operations.TouchDir(ctx, r.Fremote, "", timeValue, true) require.NoError(t, err) if accounting.Stats(ctx).GetErrors() != 0 { err = accounting.Stats(ctx).GetLastError()