diff --git a/backend/drive/drive.go b/backend/drive/drive.go index 5d844604e..33c5ce38f 100644 --- a/backend/drive/drive.go +++ b/backend/drive/drive.go @@ -2416,12 +2416,40 @@ func (f *Fs) resolveShortcut(ctx context.Context, item *drive.File) (newItem *dr return newItem, nil } +// shortcutLoop reports whether a folder shortcut at remote pointing to +// the directory targetID would create a recursive loop. +// +// This is the case when targetID is the ID of remote's parent or any +// other ancestor directory, as descending into it would list an +// ancestor again forever. The ancestor IDs are looked up in the +// directory cache which is always populated for the parents of remote +// as they are listed before their children. +func (f *Fs) shortcutLoop(remote, targetID string) bool { + for dir := path.Dir(remote); ; dir = path.Dir(dir) { + if dir == "." { + dir = "" + } + if id, ok := f.dirCache.Get(dir); ok && actualID(id) == targetID { + return true + } + if dir == "" { + return false + } + } +} + // itemToDirEntry converts a drive.File to an fs.DirEntry. // When the drive.File cannot be represented as an fs.DirEntry // (nil, nil) is returned. func (f *Fs) itemToDirEntry(ctx context.Context, remote string, item *drive.File) (entry fs.DirEntry, err error) { switch { case item.MimeType == driveFolderType: + // A folder shortcut pointing at one of its own ancestors + // would make rclone recurse forever, so drop it. + if isShortcutID(item.Id) && f.shortcutLoop(remote, actualID(item.Id)) { + fs.Errorf(remote, "Ignoring folder shortcut as it points to an ancestor directory creating a loop") + return nil, nil + } // cache the directory ID for later lookups f.dirCache.Put(remote, item.Id) // cache the resource key for later lookups diff --git a/backend/drive/drive_internal_test.go b/backend/drive/drive_internal_test.go index d5c2aff26..d2595d621 100644 --- a/backend/drive/drive_internal_test.go +++ b/backend/drive/drive_internal_test.go @@ -24,6 +24,7 @@ import ( "github.com/rclone/rclone/fs/sync" "github.com/rclone/rclone/fstest" "github.com/rclone/rclone/fstest/fstests" + "github.com/rclone/rclone/lib/dircache" "github.com/rclone/rclone/lib/random" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -145,6 +146,34 @@ func TestInternalFindExportFormat(t *testing.T) { } } +func TestShortcutLoop(t *testing.T) { + f := &Fs{} + f.dirCache = dircache.New("", "root", f) + // root is FolderX with ID "X" + f.dirCache.Put("", "X") + // a real subfolder + f.dirCache.Put("sub", "sub-id") + // a nested subfolder reached via a shortcut, composite ID + f.dirCache.Put("sub/nested", joinID("nested-id", "shortcut-id")) + + for _, test := range []struct { + name string + remote string + targetID string + want bool + }{ + {"shortcut to the root itself", "loop", "X", true}, + {"shortcut to the parent", "sub/loop", "sub-id", true}, + {"shortcut to a grandparent", "sub/nested/loop", "X", true}, + {"shortcut to a composite ancestor", "sub/nested/loop", "nested-id", true}, + {"shortcut to an unrelated folder", "sub/loop", "other-id", false}, + {"shortcut to a sibling", "sub/loop", "sub-id2", false}, + } { + got := f.shortcutLoop(test.remote, test.targetID) + assert.Equal(t, test.want, got, test.name) + } +} + func TestMimeTypesToExtension(t *testing.T) { for mimeType, extension := range _mimeTypeToExtension { extensions, err := mime.ExtensionsByType(mimeType) diff --git a/docs/content/drive.md b/docs/content/drive.md index 4cb758b6d..f9920a382 100644 --- a/docs/content/drive.md +++ b/docs/content/drive.md @@ -483,9 +483,10 @@ used to create shortcuts. Shortcuts can be completely ignored with the `--drive-skip-shortcuts` flag or the corresponding `skip_shortcuts` configuration setting. -If you have shortcuts that lead to an infinite recursion in your drive (e.g. a -shortcut pointing to a parent folder), `skip_shortcuts` might be mandatory to -be able to copy the drive. +If you have a folder shortcut that points at one of its own parent folders it +would lead to an infinite recursion. Rclone detects this, leaves the offending +shortcut out of the listing and logs an ERROR, so the rest of the drive can +still be copied. ### Emptying trash