touch: add support for touching files in directory, with options for recursive, filtering and dry-run/interactive

Fixes #5301
This commit is contained in:
albertony 2021-05-22 21:06:24 +02:00
parent 2e72ec96c1
commit 41876dd669
4 changed files with 132 additions and 48 deletions

View File

@ -10,6 +10,7 @@ import (
"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/object" "github.com/rclone/rclone/fs/object"
"github.com/rclone/rclone/fs/operations"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -17,87 +18,139 @@ var (
notCreateNewFile bool notCreateNewFile bool
timeAsArgument string timeAsArgument string
localTime bool localTime bool
recursive bool
) )
const ( const (
defaultLayout string = "060102" defaultLayout string = "060102"
layoutDateWithTime = "2006-01-02T15:04:05" layoutDateWithTime string = "2006-01-02T15:04:05"
layoutDateWithTimeNano = "2006-01-02T15:04:05.999999999" layoutDateWithTimeNano string = "2006-01-02T15:04:05.999999999"
) )
func init() { func init() {
cmd.Root.AddCommand(commandDefinition) cmd.Root.AddCommand(commandDefinition)
cmdFlags := commandDefinition.Flags() cmdFlags := commandDefinition.Flags()
flags.BoolVarP(cmdFlags, &notCreateNewFile, "no-create", "C", false, "Do not create the file if it does not exist.") flags.BoolVarP(cmdFlags, &notCreateNewFile, "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.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, &localTime, "localtime", "", false, "Use localtime for timestamp, not UTC.")
flags.BoolVarP(cmdFlags, &recursive, "recursive", "R", false, "Recursively touch all files.")
} }
var commandDefinition = &cobra.Command{ var commandDefinition = &cobra.Command{
Use: "touch remote:path", Use: "touch remote:path",
Short: `Create new file or change file modification time.`, Short: `Create new file or change file modification time.`,
Long: ` 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. have the current time.
If remote:path does not exist then a zero sized object will be created If remote:path does not exist then a zero sized file will be created,
unless the --no-create flag is provided. 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: time instead of the current time. Times may be specified as one of:
- 'YYMMDD' - e.g. 17.10.30 - 'YYMMDD' - e.g. 17.10.30
- 'YYYY-MM-DDTHH:MM:SS' - e.g. 2006-01-02T15:04:05 - '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 - '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 Note that value of ` + "`--timestamp`" + ` is in UTC. If you want local time
--localtime flag. 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)
fsrc, srcFileName := cmd.NewFsDstFile(args) f, fileName := cmd.NewFsFile(args[0])
cmd.Run(true, false, command, func() error { 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. // parseTimeArgument parses a timestamp string according to specific layouts
func Touch(ctx context.Context, fsrc fs.Fs, srcFileName string) (err error) { func parseTimeArgument(timeString string) (time.Time, error) {
timeAtr := time.Now() layout := defaultLayout
if timeAsArgument != "" { if len(timeString) == len(layoutDateWithTime) {
layout := defaultLayout layout = layoutDateWithTime
if len(timeAsArgument) == len(layoutDateWithTime) { } else if len(timeString) > len(layoutDateWithTime) {
layout = layoutDateWithTime layout = layoutDateWithTimeNano
} 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
} }
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 err != nil {
if !notCreateNewFile { return err
var buffer []byte }
src := object.NewStaticObjectInfo(srcFileName, timeAtr, int64(len(buffer)), true, nil, fsrc) fs.Debugf(nil, "Touch time %v", t)
_, err = fsrc.Put(ctx, bytes.NewBuffer(buffer), src) file, err := f.NewObject(ctx, fileName)
if err != nil { if err != nil {
return err 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) // Touch single existing file
if err != nil { if !operations.SkipDestructive(ctx, fileName, "touch") {
return errors.Wrap(err, "touch: couldn't set mod time") fs.Debugf(f, "Touching %q", fileName)
err = file.SetModTime(ctx, t)
if err != nil {
return errors.Wrap(err, "failed to touch")
}
} }
return nil return nil
} }

View File

@ -3,7 +3,6 @@ package touch
import ( import (
"context" "context"
"testing" "testing"
"time"
_ "github.com/rclone/rclone/backend/local" _ "github.com/rclone/rclone/backend/local"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
@ -16,11 +15,7 @@ var (
) )
func checkFile(t *testing.T, r fs.Fs, path string, content string) { func checkFile(t *testing.T, r fs.Fs, path string, content string) {
layout := defaultLayout timeAtrFromFlags, err := timeOfTouch()
if len(timeAsArgument) == len(layoutDateWithTime) {
layout = layoutDateWithTime
}
timeAtrFromFlags, err := time.Parse(layout, timeAsArgument)
require.NoError(t, err) require.NoError(t, err)
file1 := fstest.NewItem(path, content, timeAtrFromFlags) file1 := fstest.NewItem(path, content, timeAtrFromFlags)
fstest.CheckItems(t, r, file1) fstest.CheckItems(t, r, file1)

View File

@ -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 // ListFormat defines files information print format
type ListFormat struct { type ListFormat struct {
separator string separator string

View File

@ -1573,3 +1573,22 @@ func TestCopyFileMaxTransfer(t *testing.T) {
fstest.CheckItems(t, r.Flocal, file1, file2, file3, file4) fstest.CheckItems(t, r.Flocal, file1, file2, file3, file4)
fstest.CheckItems(t, r.Fremote, file1, 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)
}