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/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, &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.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
}

View File

@ -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)

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
type ListFormat struct {
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.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)
}