forked from TrueCloudLab/rclone
drive: implement backend command untrash
rclone backend untrash drive:directory This was based on: https://gitlab.com/B4dM4n/drive-untrash See: https://forum.rclone.org/t/rclone-teamdrive-undelete/18278/3
This commit is contained in:
parent
4d7f91309b
commit
8e7eb37456
2 changed files with 148 additions and 1 deletions
|
@ -37,6 +37,7 @@ import (
|
||||||
"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"
|
||||||
|
"github.com/rclone/rclone/fs/operations"
|
||||||
"github.com/rclone/rclone/fs/walk"
|
"github.com/rclone/rclone/fs/walk"
|
||||||
"github.com/rclone/rclone/lib/dircache"
|
"github.com/rclone/rclone/lib/dircache"
|
||||||
"github.com/rclone/rclone/lib/encoder"
|
"github.com/rclone/rclone/lib/encoder"
|
||||||
|
@ -69,7 +70,7 @@ const (
|
||||||
// 1<<18 is the minimum size supported by the Google uploader, and there is no maximum.
|
// 1<<18 is the minimum size supported by the Google uploader, and there is no maximum.
|
||||||
minChunkSize = 256 * fs.KibiByte
|
minChunkSize = 256 * fs.KibiByte
|
||||||
defaultChunkSize = 8 * fs.MebiByte
|
defaultChunkSize = 8 * fs.MebiByte
|
||||||
partialFields = "id,name,size,md5Checksum,trashed,modifiedTime,createdTime,mimeType,parents,webViewLink,shortcutDetails"
|
partialFields = "id,name,size,md5Checksum,trashed,explicitlyTrashed,modifiedTime,createdTime,mimeType,parents,webViewLink,shortcutDetails"
|
||||||
listRGrouping = 50 // number of IDs to search at once when using ListR
|
listRGrouping = 50 // number of IDs to search at once when using ListR
|
||||||
listRInputBuffer = 1000 // size of input buffer when using ListR
|
listRInputBuffer = 1000 // size of input buffer when using ListR
|
||||||
)
|
)
|
||||||
|
@ -2869,6 +2870,75 @@ func (f *Fs) listTeamDrives(ctx context.Context) (drives []*drive.TeamDrive, err
|
||||||
return drives, nil
|
return drives, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type unTrashResult struct {
|
||||||
|
Untrashed int
|
||||||
|
Errors int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r unTrashResult) Error() string {
|
||||||
|
return fmt.Sprintf("%d errors while untrashing - see log", r.Errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the trashed files from dir, directoryID recursing if needed
|
||||||
|
func (f *Fs) unTrash(ctx context.Context, dir string, directoryID string, recurse bool) (r unTrashResult, err error) {
|
||||||
|
directoryID = actualID(directoryID)
|
||||||
|
fs.Debugf(dir, "finding trash to restore in directory %q", directoryID)
|
||||||
|
_, err = f.list(ctx, []string{directoryID}, "", false, false, true, func(item *drive.File) bool {
|
||||||
|
remote := path.Join(dir, item.Name)
|
||||||
|
if item.ExplicitlyTrashed {
|
||||||
|
fs.Infof(remote, "restoring %q", item.Id)
|
||||||
|
if operations.SkipDestructive(ctx, remote, "restore") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
update := drive.File{
|
||||||
|
ForceSendFields: []string{"Trashed"}, // necessary to set false value
|
||||||
|
Trashed: false,
|
||||||
|
}
|
||||||
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
|
_, err := f.svc.Files.Update(item.Id, &update).
|
||||||
|
SupportsAllDrives(true).
|
||||||
|
Fields("trashed").
|
||||||
|
Do()
|
||||||
|
return f.shouldRetry(err)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "failed to restore")
|
||||||
|
r.Errors++
|
||||||
|
fs.Errorf(remote, "%v", err)
|
||||||
|
} else {
|
||||||
|
r.Untrashed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if recurse && item.MimeType == "application/vnd.google-apps.folder" {
|
||||||
|
if !isShortcutID(item.Id) {
|
||||||
|
rNew, _ := f.unTrash(ctx, remote, item.Id, recurse)
|
||||||
|
r.Untrashed += rNew.Untrashed
|
||||||
|
r.Errors += rNew.Errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "failed to list directory")
|
||||||
|
r.Errors++
|
||||||
|
fs.Errorf(dir, "%v", err)
|
||||||
|
}
|
||||||
|
if r.Errors != 0 {
|
||||||
|
return r, r
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Untrash dir
|
||||||
|
func (f *Fs) unTrashDir(ctx context.Context, dir string, recurse bool) (r unTrashResult, err error) {
|
||||||
|
directoryID, err := f.dirCache.FindDir(ctx, dir, false)
|
||||||
|
if err != nil {
|
||||||
|
r.Errors++
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
return f.unTrash(ctx, dir, directoryID, true)
|
||||||
|
}
|
||||||
|
|
||||||
var commandHelp = []fs.CommandHelp{{
|
var commandHelp = []fs.CommandHelp{{
|
||||||
Name: "get",
|
Name: "get",
|
||||||
Short: "Get command for fetching the drive config parameters",
|
Short: "Get command for fetching the drive config parameters",
|
||||||
|
@ -2946,6 +3016,29 @@ This will return a JSON list of objects like this
|
||||||
]
|
]
|
||||||
|
|
||||||
`,
|
`,
|
||||||
|
}, {
|
||||||
|
Name: "untrash",
|
||||||
|
Short: "Untrash files and directories",
|
||||||
|
Long: `This command untrashes all the files and directories in the directory
|
||||||
|
passed in recursively.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
This takes an optional directory to trash which make this easier to
|
||||||
|
use via the API.
|
||||||
|
|
||||||
|
rclone backend untrash drive:directory
|
||||||
|
rclone backend -i untrash drive:directory subdir
|
||||||
|
|
||||||
|
Use the -i flag to see what would be restored before restoring it.
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
{
|
||||||
|
"Untrashed": 17,
|
||||||
|
"Errors": 0
|
||||||
|
}
|
||||||
|
`,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Command the backend to run a named command
|
// Command the backend to run a named command
|
||||||
|
@ -3011,6 +3104,12 @@ func (f *Fs) Command(ctx context.Context, name string, arg []string, opt map[str
|
||||||
return f.makeShortcut(ctx, arg[0], dstFs, arg[1])
|
return f.makeShortcut(ctx, arg[0], dstFs, arg[1])
|
||||||
case "drives":
|
case "drives":
|
||||||
return f.listTeamDrives(ctx)
|
return f.listTeamDrives(ctx)
|
||||||
|
case "untrash":
|
||||||
|
dir := ""
|
||||||
|
if len(arg) > 0 {
|
||||||
|
dir = arg[0]
|
||||||
|
}
|
||||||
|
return f.unTrashDir(ctx, dir, true)
|
||||||
default:
|
default:
|
||||||
return nil, fs.ErrorCommandNotFound
|
return nil, fs.ErrorCommandNotFound
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,16 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
_ "github.com/rclone/rclone/backend/local"
|
_ "github.com/rclone/rclone/backend/local"
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/hash"
|
"github.com/rclone/rclone/fs/hash"
|
||||||
"github.com/rclone/rclone/fs/operations"
|
"github.com/rclone/rclone/fs/operations"
|
||||||
|
"github.com/rclone/rclone/fstest"
|
||||||
"github.com/rclone/rclone/fstest/fstests"
|
"github.com/rclone/rclone/fstest/fstests"
|
||||||
|
"github.com/rclone/rclone/lib/random"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/api/drive/v3"
|
"google.golang.org/api/drive/v3"
|
||||||
|
@ -361,6 +364,50 @@ func (f *Fs) InternalTestShortcuts(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestIntegration/FsMkdir/FsPutFiles/Internal/UnTrash
|
||||||
|
func (f *Fs) InternalTestUnTrash(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Make some objects, one in a subdir
|
||||||
|
contents := random.String(100)
|
||||||
|
file1 := fstest.NewItem("trashDir/toBeTrashed", contents, time.Now())
|
||||||
|
_, obj1 := fstests.PutTestContents(ctx, t, f, &file1, contents, false)
|
||||||
|
file2 := fstest.NewItem("trashDir/subdir/toBeTrashed", contents, time.Now())
|
||||||
|
_, _ = fstests.PutTestContents(ctx, t, f, &file2, contents, false)
|
||||||
|
|
||||||
|
// Check objects
|
||||||
|
checkObjects := func() {
|
||||||
|
fstest.CheckListingWithRoot(t, f, "trashDir", []fstest.Item{
|
||||||
|
file1,
|
||||||
|
file2,
|
||||||
|
}, []string{
|
||||||
|
"trashDir/subdir",
|
||||||
|
}, f.Precision())
|
||||||
|
}
|
||||||
|
checkObjects()
|
||||||
|
|
||||||
|
// Make sure we are using the trash
|
||||||
|
require.Equal(t, true, f.opt.UseTrash)
|
||||||
|
|
||||||
|
// Remove the object and the dir
|
||||||
|
require.NoError(t, obj1.Remove(ctx))
|
||||||
|
require.NoError(t, f.Purge(ctx, "trashDir/subdir"))
|
||||||
|
|
||||||
|
// Check objects gone
|
||||||
|
fstest.CheckListingWithRoot(t, f, "trashDir", []fstest.Item{}, []string{}, f.Precision())
|
||||||
|
|
||||||
|
// Restore the object and directory
|
||||||
|
r, err := f.unTrashDir(ctx, "trashDir", true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, unTrashResult{Errors: 0, Untrashed: 2}, r)
|
||||||
|
|
||||||
|
// Check objects restored
|
||||||
|
checkObjects()
|
||||||
|
|
||||||
|
// Remove the test dir
|
||||||
|
require.NoError(t, f.Purge(ctx, "trashDir"))
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fs) InternalTest(t *testing.T) {
|
func (f *Fs) InternalTest(t *testing.T) {
|
||||||
// These tests all depend on each other so run them as nested tests
|
// These tests all depend on each other so run them as nested tests
|
||||||
t.Run("DocumentImport", func(t *testing.T) {
|
t.Run("DocumentImport", func(t *testing.T) {
|
||||||
|
@ -376,6 +423,7 @@ func (f *Fs) InternalTest(t *testing.T) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
t.Run("Shortcuts", f.InternalTestShortcuts)
|
t.Run("Shortcuts", f.InternalTestShortcuts)
|
||||||
|
t.Run("UnTrash", f.InternalTestUnTrash)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ fstests.InternalTester = (*Fs)(nil)
|
var _ fstests.InternalTester = (*Fs)(nil)
|
||||||
|
|
Loading…
Reference in a new issue