operations: Add skip all, do all, quit operations to --interactive - fixes #3886
This also adds SkipDestructive into all the remaing places --dry-run was used and adds documentation.
This commit is contained in:
parent
ba5eb230fb
commit
b23cf58a41
4 changed files with 122 additions and 33 deletions
|
@ -73,6 +73,9 @@ storage system in the config file then the sub path, eg
|
||||||
|
|
||||||
You can define as many storage paths as you like in the config file.
|
You can define as many storage paths as you like in the config file.
|
||||||
|
|
||||||
|
Please use the [`-i` / `--interactive`](#interactive) flag while
|
||||||
|
learning rclone to avoid accidental data loss.
|
||||||
|
|
||||||
Subcommands
|
Subcommands
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
@ -686,7 +689,45 @@ This can be useful as an additional layer of protection for immutable
|
||||||
or append-only data sets (notably backup archives), where modification
|
or append-only data sets (notably backup archives), where modification
|
||||||
implies corruption and should not be propagated.
|
implies corruption and should not be propagated.
|
||||||
|
|
||||||
## --leave-root ###
|
### -i / --interactive {#interactive}
|
||||||
|
|
||||||
|
This flag can be used to tell rclone that you wish a manual
|
||||||
|
confirmation before destructive operations.
|
||||||
|
|
||||||
|
It is **recommended** that you use this flag while learning rclone
|
||||||
|
especially with `rclone sync`.
|
||||||
|
|
||||||
|
For example
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rclone delete -i /tmp/dir
|
||||||
|
rclone: delete "important-file.txt"?
|
||||||
|
y) Yes, this is OK (default)
|
||||||
|
n) No, skip this
|
||||||
|
s) Skip all delete operations with no more questions
|
||||||
|
!) Do all delete operations with no more questions
|
||||||
|
q) Exit rclone now.
|
||||||
|
y/n/s/!/q> n
|
||||||
|
```
|
||||||
|
|
||||||
|
The options mean
|
||||||
|
|
||||||
|
- `y`: **Yes**, this operation should go ahead. You can also press Return
|
||||||
|
for this to happen. You'll be asked every time unless you choose `s`
|
||||||
|
or `!`.
|
||||||
|
- `n`: **No**, do not do this operation. You'll be asked every time unless
|
||||||
|
you choose `s` or `!`.
|
||||||
|
- `s`: **Skip** all the following operations of this type with no more
|
||||||
|
questions. This takes effect until rclone exits. If there are any
|
||||||
|
different kind of operations you'll be prompted for them.
|
||||||
|
- `!`: **Do all** the following operations with no more
|
||||||
|
questions. Useful if you've decided that you don't mind rclone doing
|
||||||
|
that kind of operation. This takes effect until rclone exits . If
|
||||||
|
there are any different kind of operations you'll be prompted for
|
||||||
|
them.
|
||||||
|
- `q`: **Quit** rclone now, just in case!
|
||||||
|
|
||||||
|
### --leave-root ####
|
||||||
|
|
||||||
During rmdirs it will not remove root directory, even if it's empty.
|
During rmdirs it will not remove root directory, even if it's empty.
|
||||||
|
|
||||||
|
|
|
@ -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 !skipDestructive(ctx, o, "rename") {
|
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)
|
||||||
|
@ -253,7 +253,7 @@ 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 !skipDestructive(ctx, dirs[0], "merge duplicate directories") {
|
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 {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -31,6 +32,7 @@ import (
|
||||||
"github.com/rclone/rclone/fs/march"
|
"github.com/rclone/rclone/fs/march"
|
||||||
"github.com/rclone/rclone/fs/object"
|
"github.com/rclone/rclone/fs/object"
|
||||||
"github.com/rclone/rclone/fs/walk"
|
"github.com/rclone/rclone/fs/walk"
|
||||||
|
"github.com/rclone/rclone/lib/atexit"
|
||||||
"github.com/rclone/rclone/lib/random"
|
"github.com/rclone/rclone/lib/random"
|
||||||
"github.com/rclone/rclone/lib/readers"
|
"github.com/rclone/rclone/lib/readers"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
@ -212,7 +214,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 !skipDestructive(ctx, src, "update modification time") {
|
if !SkipDestructive(ctx, src, "update modification time") {
|
||||||
// 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 {
|
||||||
|
@ -347,7 +349,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 skipDestructive(ctx, src, "copy") {
|
if SkipDestructive(ctx, src, "copy") {
|
||||||
return newDst, nil
|
return newDst, nil
|
||||||
}
|
}
|
||||||
maxTries := fs.Config.LowLevelRetries
|
maxTries := fs.Config.LowLevelRetries
|
||||||
|
@ -525,7 +527,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 skipDestructive(ctx, src, "move") {
|
if SkipDestructive(ctx, src, "move") {
|
||||||
return newDst, nil
|
return newDst, nil
|
||||||
}
|
}
|
||||||
// See if we have Move available
|
// See if we have Move available
|
||||||
|
@ -604,7 +606,8 @@ func DeleteFileWithBackupDir(ctx context.Context, dst fs.Object, backupDir fs.Fs
|
||||||
if backupDir != nil {
|
if backupDir != nil {
|
||||||
action, actioned = "move into backup dir", "Moved into backup dir"
|
action, actioned = "move into backup dir", "Moved into backup dir"
|
||||||
}
|
}
|
||||||
if skipDestructive(ctx, dst, action) {
|
skip := SkipDestructive(ctx, dst, action)
|
||||||
|
if skip {
|
||||||
// do nothing
|
// do nothing
|
||||||
} else if backupDir != nil {
|
} else if backupDir != nil {
|
||||||
err = MoveBackupDir(ctx, backupDir, dst)
|
err = MoveBackupDir(ctx, backupDir, dst)
|
||||||
|
@ -614,7 +617,7 @@ func DeleteFileWithBackupDir(ctx context.Context, dst fs.Object, backupDir fs.Fs
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Errorf(dst, "Couldn't %s: %v", action, err)
|
fs.Errorf(dst, "Couldn't %s: %v", action, err)
|
||||||
err = fs.CountError(err)
|
err = fs.CountError(err)
|
||||||
} else if !fs.Config.DryRun {
|
} else if !skip {
|
||||||
fs.Infof(dst, actioned)
|
fs.Infof(dst, actioned)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
@ -1135,8 +1138,7 @@ func ListDir(ctx context.Context, f fs.Fs, w io.Writer) error {
|
||||||
|
|
||||||
// Mkdir makes a destination directory or container
|
// Mkdir makes a destination directory or container
|
||||||
func Mkdir(ctx context.Context, f fs.Fs, dir string) error {
|
func Mkdir(ctx context.Context, f fs.Fs, dir string) error {
|
||||||
if fs.Config.DryRun {
|
if SkipDestructive(ctx, fs.LogDirName(f, dir), "make directory") {
|
||||||
fs.Logf(fs.LogDirName(f, dir), "Not making directory as dry run is set")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fs.Debugf(fs.LogDirName(f, dir), "Making directory")
|
fs.Debugf(fs.LogDirName(f, dir), "Making directory")
|
||||||
|
@ -1151,7 +1153,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 skipDestructive(ctx, fs.LogDirName(f, dir), "remove directory") {
|
if SkipDestructive(ctx, fs.LogDirName(f, dir), "remove directory") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fs.Debugf(fs.LogDirName(f, dir), "Removing directory")
|
fs.Debugf(fs.LogDirName(f, dir), "Removing directory")
|
||||||
|
@ -1176,16 +1178,15 @@ 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 skipDestructive(ctx, fs.LogDirName(f, dir), "purge directory") {
|
if SkipDestructive(ctx, fs.LogDirName(f, dir), "purge directory") {
|
||||||
return nil
|
return nil
|
||||||
} else {
|
}
|
||||||
err = doPurge(ctx)
|
err = doPurge(ctx)
|
||||||
if err == fs.ErrorCantPurge {
|
if err == fs.ErrorCantPurge {
|
||||||
doFallbackPurge = true
|
doFallbackPurge = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if doFallbackPurge {
|
if doFallbackPurge {
|
||||||
// DeleteFiles and Rmdir observe --dry-run
|
// DeleteFiles and Rmdir observe --dry-run
|
||||||
err = DeleteFiles(ctx, listToChan(ctx, f, dir))
|
err = DeleteFiles(ctx, listToChan(ctx, f, dir))
|
||||||
|
@ -1251,7 +1252,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 skipDestructive(ctx, f, "clean up old files") {
|
if SkipDestructive(ctx, f, "clean up old files") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return doCleanUp(ctx)
|
return doCleanUp(ctx)
|
||||||
|
@ -1389,7 +1390,7 @@ func Rcat(ctx context.Context, fdst fs.Fs, dstFileName string, in io.ReadCloser,
|
||||||
fStreamTo = tmpLocalFs
|
fStreamTo = tmpLocalFs
|
||||||
}
|
}
|
||||||
|
|
||||||
if skipDestructive(ctx, dstFileName, "upload from pipe") {
|
if SkipDestructive(ctx, dstFileName, "upload from pipe") {
|
||||||
// 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
|
||||||
|
@ -1671,7 +1672,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 skipDestructive(ctx, dstFileName, "upload from pipe") {
|
if SkipDestructive(ctx, dstFileName, "upload from pipe") {
|
||||||
// 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
|
||||||
|
@ -2187,25 +2188,73 @@ func GetFsInfo(f fs.Fs) *FsInfo {
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
var interactiveMutex sync.Mutex
|
var (
|
||||||
|
interactiveMu sync.Mutex
|
||||||
|
skipped = map[string]bool{}
|
||||||
|
)
|
||||||
|
|
||||||
func skipDestructive(ctx context.Context, subject interface{}, action string) bool {
|
// skipDestructiveChoose asks the user which action to take
|
||||||
var (
|
//
|
||||||
flag string
|
// Call with interactiveMu held
|
||||||
skip bool
|
func skipDestructiveChoose(ctx context.Context, subject interface{}, action string) (skip bool) {
|
||||||
)
|
fmt.Printf("rclone: %s \"%v\"?\n", action, subject)
|
||||||
|
switch i := config.CommandDefault([]string{
|
||||||
|
"yYes, this is OK",
|
||||||
|
"nNo, skip this",
|
||||||
|
fmt.Sprintf("sSkip all %s operations with no more questions", action),
|
||||||
|
fmt.Sprintf("!Do all %s operations with no more questions", action),
|
||||||
|
"qExit rclone now.",
|
||||||
|
}, 0); i {
|
||||||
|
case 'y':
|
||||||
|
skip = false
|
||||||
|
case 'n':
|
||||||
|
skip = true
|
||||||
|
case 's':
|
||||||
|
skip = true
|
||||||
|
skipped[action] = true
|
||||||
|
fs.Logf(nil, "Skipping all %s operations from now on without asking", action)
|
||||||
|
case '!':
|
||||||
|
skip = false
|
||||||
|
skipped[action] = false
|
||||||
|
fs.Logf(nil, "Doing all %s operations from now on without asking", action)
|
||||||
|
case 'q':
|
||||||
|
fs.Logf(nil, "Quitting rclone now")
|
||||||
|
atexit.Run()
|
||||||
|
os.Exit(0)
|
||||||
|
default:
|
||||||
|
skip = true
|
||||||
|
fs.Errorf(nil, "Bad choice %c", i)
|
||||||
|
}
|
||||||
|
return skip
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkipDestructive should be called whenever rclone is about to do an destructive operation.
|
||||||
|
//
|
||||||
|
// It will check the --dry-run flag and it will ask the user if the --interactive flag is set.
|
||||||
|
//
|
||||||
|
// subject should be the object or directory in use
|
||||||
|
//
|
||||||
|
// action should be a descriptive word or short phrase
|
||||||
|
//
|
||||||
|
// Together they should make sense in this sentence: "Rclone is about
|
||||||
|
// to action subject".
|
||||||
|
func SkipDestructive(ctx context.Context, subject interface{}, action string) (skip bool) {
|
||||||
|
var flag string
|
||||||
switch {
|
switch {
|
||||||
case fs.Config.DryRun:
|
case fs.Config.DryRun:
|
||||||
flag = "--dry-run"
|
flag = "--dry-run"
|
||||||
skip = true
|
skip = true
|
||||||
case fs.Config.Interactive:
|
case fs.Config.Interactive:
|
||||||
flag = "--interactive"
|
flag = "--interactive"
|
||||||
interactiveMutex.Lock()
|
interactiveMu.Lock()
|
||||||
defer interactiveMutex.Unlock()
|
defer interactiveMu.Unlock()
|
||||||
fmt.Printf("rclone: %s \"%v\"?\n", action, subject)
|
var found bool
|
||||||
skip = !config.Confirm(true)
|
skip, found = skipped[action]
|
||||||
|
if !found {
|
||||||
|
skip = skipDestructiveChoose(ctx, subject, action)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
skip = false
|
return false
|
||||||
}
|
}
|
||||||
if skip {
|
if skip {
|
||||||
fs.Logf(subject, "Skipped %s as %s is set", action, flag)
|
fs.Logf(subject, "Skipped %s as %s is set", action, flag)
|
||||||
|
|
|
@ -1063,8 +1063,7 @@ func MoveDir(ctx context.Context, fdst, fsrc fs.Fs, deleteEmptySrcDirs bool, cop
|
||||||
|
|
||||||
// First attempt to use DirMover if exists, same Fs and no filters are active
|
// First attempt to use DirMover if exists, same Fs and no filters are active
|
||||||
if fdstDirMove := fdst.Features().DirMove; fdstDirMove != nil && operations.SameConfig(fsrc, fdst) && filter.Active.InActive() {
|
if fdstDirMove := fdst.Features().DirMove; fdstDirMove != nil && operations.SameConfig(fsrc, fdst) && filter.Active.InActive() {
|
||||||
if fs.Config.DryRun {
|
if operations.SkipDestructive(ctx, fdst, "server side directory move") {
|
||||||
fs.Logf(fdst, "Not doing server side directory move as --dry-run")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fs.Debugf(fdst, "Using server side directory move")
|
fs.Debugf(fdst, "Using server side directory move")
|
||||||
|
|
Loading…
Add table
Reference in a new issue