forked from TrueCloudLab/rclone
operations: interactive mode -i/--interactive for destructive operations #3886
This commit is contained in:
parent
2ea15a72bc
commit
ba5eb230fb
4 changed files with 44 additions and 27 deletions
|
@ -44,6 +44,7 @@ type ConfigInfo struct {
|
||||||
StatsLogLevel LogLevel
|
StatsLogLevel LogLevel
|
||||||
UseJSONLog bool
|
UseJSONLog bool
|
||||||
DryRun bool
|
DryRun bool
|
||||||
|
Interactive bool
|
||||||
CheckSum bool
|
CheckSum bool
|
||||||
SizeOnly bool
|
SizeOnly bool
|
||||||
IgnoreTimes bool
|
IgnoreTimes bool
|
||||||
|
|
|
@ -51,6 +51,7 @@ func AddFlags(flagSet *pflag.FlagSet) {
|
||||||
flags.BoolVarP(flagSet, &fs.Config.IgnoreExisting, "ignore-existing", "", fs.Config.IgnoreExisting, "Skip all files that exist on destination")
|
flags.BoolVarP(flagSet, &fs.Config.IgnoreExisting, "ignore-existing", "", fs.Config.IgnoreExisting, "Skip all files that exist on destination")
|
||||||
flags.BoolVarP(flagSet, &fs.Config.IgnoreErrors, "ignore-errors", "", fs.Config.IgnoreErrors, "delete even if there are I/O errors")
|
flags.BoolVarP(flagSet, &fs.Config.IgnoreErrors, "ignore-errors", "", fs.Config.IgnoreErrors, "delete even if there are I/O errors")
|
||||||
flags.BoolVarP(flagSet, &fs.Config.DryRun, "dry-run", "n", fs.Config.DryRun, "Do a trial run with no permanent changes")
|
flags.BoolVarP(flagSet, &fs.Config.DryRun, "dry-run", "n", fs.Config.DryRun, "Do a trial run with no permanent changes")
|
||||||
|
flags.BoolVarP(flagSet, &fs.Config.Interactive, "interactive", "i", fs.Config.Interactive, "Enable interactive mode")
|
||||||
flags.DurationVarP(flagSet, &fs.Config.ConnectTimeout, "contimeout", "", fs.Config.ConnectTimeout, "Connect timeout")
|
flags.DurationVarP(flagSet, &fs.Config.ConnectTimeout, "contimeout", "", fs.Config.ConnectTimeout, "Connect timeout")
|
||||||
flags.DurationVarP(flagSet, &fs.Config.Timeout, "timeout", "", fs.Config.Timeout, "IO idle timeout")
|
flags.DurationVarP(flagSet, &fs.Config.Timeout, "timeout", "", fs.Config.Timeout, "IO idle timeout")
|
||||||
flags.DurationVarP(flagSet, &fs.Config.ExpectContinueTimeout, "expect-continue-timeout", "", fs.Config.ExpectContinueTimeout, "Timeout when using expect / 100-continue in HTTP")
|
flags.DurationVarP(flagSet, &fs.Config.ExpectContinueTimeout, "expect-continue-timeout", "", fs.Config.ExpectContinueTimeout, "Timeout when using expect / 100-continue in HTTP")
|
||||||
|
|
|
@ -44,7 +44,7 @@ outer:
|
||||||
newName = fmt.Sprintf("%s-%d%s", base, i+suffix, ext)
|
newName = fmt.Sprintf("%s-%d%s", base, i+suffix, ext)
|
||||||
_, err = f.NewObject(ctx, newName)
|
_, err = f.NewObject(ctx, newName)
|
||||||
}
|
}
|
||||||
if !fs.Config.DryRun {
|
if !skipDestructive(ctx, o, "rename") {
|
||||||
newObj, err := doMove(ctx, o, newName)
|
newObj, err := doMove(ctx, o, newName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fs.CountError(err)
|
err = fs.CountError(err)
|
||||||
|
@ -52,8 +52,6 @@ outer:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fs.Infof(newObj, "renamed from: %v", o)
|
fs.Infof(newObj, "renamed from: %v", o)
|
||||||
} else {
|
|
||||||
fs.Logf(remote, "Not renaming to %q as --dry-run", newName)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -255,15 +253,13 @@ func dedupeMergeDuplicateDirs(ctx context.Context, f fs.Fs, duplicateDirs [][]fs
|
||||||
return errors.Errorf("%v: can't flush dir cache", f)
|
return errors.Errorf("%v: can't flush dir cache", f)
|
||||||
}
|
}
|
||||||
for _, dirs := range duplicateDirs {
|
for _, dirs := range duplicateDirs {
|
||||||
if !fs.Config.DryRun {
|
if !skipDestructive(ctx, dirs[0], "merge duplicate directories") {
|
||||||
fs.Infof(dirs[0], "Merging contents of duplicate directories")
|
fs.Infof(dirs[0], "Merging contents of duplicate directories")
|
||||||
err := mergeDirs(ctx, dirs)
|
err := mergeDirs(ctx, dirs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fs.CountError(err)
|
err = fs.CountError(err)
|
||||||
fs.Errorf(nil, "merge duplicate dirs: %v", err)
|
fs.Errorf(nil, "merge duplicate dirs: %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
fs.Infof(dirs[0], "NOT Merging contents of duplicate directories as --dry-run")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dirCacheFlush()
|
dirCacheFlush()
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/accounting"
|
"github.com/rclone/rclone/fs/accounting"
|
||||||
"github.com/rclone/rclone/fs/cache"
|
"github.com/rclone/rclone/fs/cache"
|
||||||
|
"github.com/rclone/rclone/fs/config"
|
||||||
"github.com/rclone/rclone/fs/fserrors"
|
"github.com/rclone/rclone/fs/fserrors"
|
||||||
"github.com/rclone/rclone/fs/fshttp"
|
"github.com/rclone/rclone/fs/fshttp"
|
||||||
"github.com/rclone/rclone/fs/hash"
|
"github.com/rclone/rclone/fs/hash"
|
||||||
|
@ -211,9 +212,7 @@ func equal(ctx context.Context, src fs.ObjectInfo, dst fs.Object, opt equalOpt)
|
||||||
|
|
||||||
// mod time differs but hash is the same to reset mod time if required
|
// mod time differs but hash is the same to reset mod time if required
|
||||||
if opt.updateModTime {
|
if opt.updateModTime {
|
||||||
if fs.Config.DryRun {
|
if !skipDestructive(ctx, src, "update modification time") {
|
||||||
fs.Logf(src, "Not updating modification time as --dry-run")
|
|
||||||
} else {
|
|
||||||
// Size and hash the same but mtime different
|
// Size and hash the same but mtime different
|
||||||
// Error if objects are treated as immutable
|
// Error if objects are treated as immutable
|
||||||
if fs.Config.Immutable {
|
if fs.Config.Immutable {
|
||||||
|
@ -348,8 +347,7 @@ func Copy(ctx context.Context, f fs.Fs, dst fs.Object, remote string, src fs.Obj
|
||||||
tr.Done(err)
|
tr.Done(err)
|
||||||
}()
|
}()
|
||||||
newDst = dst
|
newDst = dst
|
||||||
if fs.Config.DryRun {
|
if skipDestructive(ctx, src, "copy") {
|
||||||
fs.Logf(src, "Not copying as --dry-run")
|
|
||||||
return newDst, nil
|
return newDst, nil
|
||||||
}
|
}
|
||||||
maxTries := fs.Config.LowLevelRetries
|
maxTries := fs.Config.LowLevelRetries
|
||||||
|
@ -527,8 +525,7 @@ func Move(ctx context.Context, fdst fs.Fs, dst fs.Object, remote string, src fs.
|
||||||
tr.Done(err)
|
tr.Done(err)
|
||||||
}()
|
}()
|
||||||
newDst = dst
|
newDst = dst
|
||||||
if fs.Config.DryRun {
|
if skipDestructive(ctx, src, "move") {
|
||||||
fs.Logf(src, "Not moving as --dry-run")
|
|
||||||
return newDst, nil
|
return newDst, nil
|
||||||
}
|
}
|
||||||
// See if we have Move available
|
// See if we have Move available
|
||||||
|
@ -603,12 +600,12 @@ func DeleteFileWithBackupDir(ctx context.Context, dst fs.Object, backupDir fs.Fs
|
||||||
if fs.Config.MaxDelete != -1 && numDeletes > fs.Config.MaxDelete {
|
if fs.Config.MaxDelete != -1 && numDeletes > fs.Config.MaxDelete {
|
||||||
return fserrors.FatalError(errors.New("--max-delete threshold reached"))
|
return fserrors.FatalError(errors.New("--max-delete threshold reached"))
|
||||||
}
|
}
|
||||||
action, actioned, actioning := "delete", "Deleted", "deleting"
|
action, actioned := "delete", "Deleted"
|
||||||
if backupDir != nil {
|
if backupDir != nil {
|
||||||
action, actioned, actioning = "move into backup dir", "Moved into backup dir", "moving into backup dir"
|
action, actioned = "move into backup dir", "Moved into backup dir"
|
||||||
}
|
}
|
||||||
if fs.Config.DryRun {
|
if skipDestructive(ctx, dst, action) {
|
||||||
fs.Logf(dst, "Not %s as --dry-run", actioning)
|
// do nothing
|
||||||
} else if backupDir != nil {
|
} else if backupDir != nil {
|
||||||
err = MoveBackupDir(ctx, backupDir, dst)
|
err = MoveBackupDir(ctx, backupDir, dst)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1154,8 +1151,7 @@ func Mkdir(ctx context.Context, f fs.Fs, dir string) error {
|
||||||
// TryRmdir removes a container but not if not empty. It doesn't
|
// TryRmdir removes a container but not if not empty. It doesn't
|
||||||
// count errors but may return one.
|
// count errors but may return one.
|
||||||
func TryRmdir(ctx context.Context, f fs.Fs, dir string) error {
|
func TryRmdir(ctx context.Context, f fs.Fs, dir string) error {
|
||||||
if fs.Config.DryRun {
|
if skipDestructive(ctx, fs.LogDirName(f, dir), "remove directory") {
|
||||||
fs.Logf(fs.LogDirName(f, dir), "Not deleting as dry run is set")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fs.Debugf(fs.LogDirName(f, dir), "Removing directory")
|
fs.Debugf(fs.LogDirName(f, dir), "Removing directory")
|
||||||
|
@ -1180,8 +1176,8 @@ func Purge(ctx context.Context, f fs.Fs, dir string) error {
|
||||||
// FIXME change the Purge interface so it takes a dir - see #1891
|
// FIXME change the Purge interface so it takes a dir - see #1891
|
||||||
if doPurge := f.Features().Purge; doPurge != nil {
|
if doPurge := f.Features().Purge; doPurge != nil {
|
||||||
doFallbackPurge = false
|
doFallbackPurge = false
|
||||||
if fs.Config.DryRun {
|
if skipDestructive(ctx, fs.LogDirName(f, dir), "purge directory") {
|
||||||
fs.Logf(f, "Not purging as --dry-run set")
|
return nil
|
||||||
} else {
|
} else {
|
||||||
err = doPurge(ctx)
|
err = doPurge(ctx)
|
||||||
if err == fs.ErrorCantPurge {
|
if err == fs.ErrorCantPurge {
|
||||||
|
@ -1255,8 +1251,7 @@ func CleanUp(ctx context.Context, f fs.Fs) error {
|
||||||
if doCleanUp == nil {
|
if doCleanUp == nil {
|
||||||
return errors.Errorf("%v doesn't support cleanup", f)
|
return errors.Errorf("%v doesn't support cleanup", f)
|
||||||
}
|
}
|
||||||
if fs.Config.DryRun {
|
if skipDestructive(ctx, f, "clean up old files") {
|
||||||
fs.Logf(f, "Not running cleanup as --dry-run set")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return doCleanUp(ctx)
|
return doCleanUp(ctx)
|
||||||
|
@ -1394,8 +1389,7 @@ func Rcat(ctx context.Context, fdst fs.Fs, dstFileName string, in io.ReadCloser,
|
||||||
fStreamTo = tmpLocalFs
|
fStreamTo = tmpLocalFs
|
||||||
}
|
}
|
||||||
|
|
||||||
if fs.Config.DryRun {
|
if skipDestructive(ctx, dstFileName, "upload from pipe") {
|
||||||
fs.Logf("stdin", "Not uploading as --dry-run")
|
|
||||||
// prevents "broken pipe" errors
|
// prevents "broken pipe" errors
|
||||||
_, err = io.Copy(ioutil.Discard, in)
|
_, err = io.Copy(ioutil.Discard, in)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1677,8 +1671,7 @@ func RcatSize(ctx context.Context, fdst fs.Fs, dstFileName string, in io.ReadClo
|
||||||
body := ioutil.NopCloser(in) // we let the server close the body
|
body := ioutil.NopCloser(in) // we let the server close the body
|
||||||
in := tr.Account(body) // account the transfer (no buffering)
|
in := tr.Account(body) // account the transfer (no buffering)
|
||||||
|
|
||||||
if fs.Config.DryRun {
|
if skipDestructive(ctx, dstFileName, "upload from pipe") {
|
||||||
fs.Logf("stdin", "Not uploading as --dry-run")
|
|
||||||
// prevents "broken pipe" errors
|
// prevents "broken pipe" errors
|
||||||
_, err = io.Copy(ioutil.Discard, in)
|
_, err = io.Copy(ioutil.Discard, in)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -2193,3 +2186,29 @@ func GetFsInfo(f fs.Fs) *FsInfo {
|
||||||
}
|
}
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var interactiveMutex sync.Mutex
|
||||||
|
|
||||||
|
func skipDestructive(ctx context.Context, subject interface{}, action string) bool {
|
||||||
|
var (
|
||||||
|
flag string
|
||||||
|
skip bool
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case fs.Config.DryRun:
|
||||||
|
flag = "--dry-run"
|
||||||
|
skip = true
|
||||||
|
case fs.Config.Interactive:
|
||||||
|
flag = "--interactive"
|
||||||
|
interactiveMutex.Lock()
|
||||||
|
defer interactiveMutex.Unlock()
|
||||||
|
fmt.Printf("rclone: %s \"%v\"?\n", action, subject)
|
||||||
|
skip = !config.Confirm(true)
|
||||||
|
default:
|
||||||
|
skip = false
|
||||||
|
}
|
||||||
|
if skip {
|
||||||
|
fs.Logf(subject, "Skipped %s as %s is set", action, flag)
|
||||||
|
}
|
||||||
|
return skip
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue