mirror of
https://github.com/rclone/rclone.git
synced 2024-12-23 07:29:35 +01:00
touch: add support for touching files in directory, with options for recursive, filtering and dry-run/interactive
Fixes #5301
This commit is contained in:
parent
2e72ec96c1
commit
41876dd669
@ -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, ¬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.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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user