touch: fix issue where directory is created instead of file

Detected on ftp, sftp and Dropbox backends.

Fixes #5952
This commit is contained in:
albertony 2022-01-27 19:46:53 +01:00
parent bd6d36b3f6
commit 07f53aebdc
4 changed files with 53 additions and 19 deletions

View file

@ -5,11 +5,13 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log"
"time" "time"
"github.com/rclone/rclone/cmd" "github.com/rclone/rclone/cmd"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/flags" "github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/fspath"
"github.com/rclone/rclone/fs/object" "github.com/rclone/rclone/fs/object"
"github.com/rclone/rclone/fs/operations" "github.com/rclone/rclone/fs/operations"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -63,13 +65,32 @@ then add the ` + "`--localtime`" + ` flag.
`, `,
Run: func(command *cobra.Command, args []string) { Run: func(command *cobra.Command, args []string) {
cmd.CheckArgs(1, 1, command, args) cmd.CheckArgs(1, 1, command, args)
f, fileName := cmd.NewFsFile(args[0]) f, remote := newFsDst(args)
cmd.Run(true, false, command, func() error { 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 // parseTimeArgument parses a timestamp string according to specific layouts
func parseTimeArgument(timeString string) (time.Time, error) { func parseTimeArgument(timeString string) (time.Time, error) {
layout := defaultLayout 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. // 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() t, err := timeOfTouch()
if err != nil { if err != nil {
return err return err
} }
fs.Debugf(nil, "Touch time %v", t) fs.Debugf(nil, "Touch time %v", t)
file, err := f.NewObject(ctx, fileName) file, err := f.NewObject(ctx, remote)
if err != nil { if err != nil {
if errors.Is(err, fs.ErrorObjectNotFound) { 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 { if notCreateNewFile {
fs.Logf(f, "Not touching non-existent file due to --no-create") fs.Logf(f, "Not touching non-existent file due to --no-create")
return nil return nil
} }
if recursive { if recursive {
// For consistency, --recursive never creates new files.
fs.Logf(f, "Not touching non-existent file due to --recursive") fs.Logf(f, "Not touching non-existent file due to --recursive")
return nil return nil
} }
if operations.SkipDestructive(ctx, f, "touch (create)") { if operations.SkipDestructive(ctx, f, "touch (create)") {
return nil return nil
} }
fs.Debugf(f, "Touching (creating)") fs.Debugf(f, "Touching (creating) %q", remote)
if err = createEmptyObject(ctx, fileName, t, f); err != nil { if err = createEmptyObject(ctx, remote, t, f); err != nil {
return fmt.Errorf("failed to touch (create): %w", err) return fmt.Errorf("failed to touch (create): %w", err)
} }
} }
if errors.Is(err, fs.ErrorIsDir) { if errors.Is(err, fs.ErrorIsDir) {
// Touching existing directory
if recursive { if recursive {
// Touch existing directory, recursive fs.Debugf(f, "Touching recursively files in directory %q", remote)
fs.Debugf(nil, "Touching files in directory recursively") return operations.TouchDir(ctx, f, remote, t, true)
return operations.TouchDir(ctx, f, t, true)
} }
// Touch existing directory without recursing fs.Debugf(f, "Touching non-recursively files in directory %q", remote)
fs.Debugf(nil, "Touching files in directory non-recursively") return operations.TouchDir(ctx, f, remote, t, false)
return operations.TouchDir(ctx, f, t, false)
} }
return err return err
} }
// Touch single existing file // Touch single existing file
if !operations.SkipDestructive(ctx, fileName, "touch") { if !operations.SkipDestructive(ctx, remote, "touch") {
fs.Debugf(f, "Touching %q", fileName) fs.Debugf(f, "Touching %q", remote)
err = file.SetModTime(ctx, t) err = file.SetModTime(ctx, t)
if err != nil { if err != nil {
return fmt.Errorf("failed to touch: %w", err) return fmt.Errorf("failed to touch: %w", err)

View file

@ -113,6 +113,15 @@ func TestTouchCreateMultipleDirAndFile(t *testing.T) {
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{file1}, []string{"a", "a/b"}, fs.ModTimeNotSupported) 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) { func TestTouchEmptyDir(t *testing.T) {
r := fstest.NewRun(t) r := fstest.NewRun(t)
defer r.Finalise() defer r.Finalise()

View file

@ -1929,9 +1929,9 @@ func SetTier(ctx context.Context, fsrc fs.Fs, tier string) error {
}) })
} }
// TouchDir touches every file in f with time t // TouchDir touches every file in directory with time t
func TouchDir(ctx context.Context, f fs.Fs, t time.Time, recursive bool) error { func TouchDir(ctx context.Context, f fs.Fs, remote string, t time.Time, recursive bool) error {
return walk.ListR(ctx, f, "", false, ConfigMaxDepth(ctx, recursive), walk.ListObjects, func(entries fs.DirEntries) error { return walk.ListR(ctx, f, remote, false, ConfigMaxDepth(ctx, recursive), walk.ListObjects, func(entries fs.DirEntries) error {
entries.ForObject(func(o fs.Object) { entries.ForObject(func(o fs.Object) {
if !SkipDestructive(ctx, o, "touch") { if !SkipDestructive(ctx, o, "touch") {
fs.Debugf(f, "Touching %q", o.Remote()) fs.Debugf(f, "Touching %q", o.Remote())

View file

@ -1636,7 +1636,7 @@ func TestTouchDir(t *testing.T) {
r.CheckRemoteItems(t, file1, file2, file3) r.CheckRemoteItems(t, file1, file2, file3)
timeValue := time.Date(2010, 9, 8, 7, 6, 5, 4, time.UTC) 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) require.NoError(t, err)
if accounting.Stats(ctx).GetErrors() != 0 { if accounting.Stats(ctx).GetErrors() != 0 {
err = accounting.Stats(ctx).GetLastError() err = accounting.Stats(ctx).GetLastError()