diff --git a/cmd/rmdirs/rmdirs.go b/cmd/rmdirs/rmdirs.go index b87e0f091..470f34ff5 100644 --- a/cmd/rmdirs/rmdirs.go +++ b/cmd/rmdirs/rmdirs.go @@ -35,7 +35,10 @@ empty directories in. For example the [delete](/commands/rclone_delete/) command will delete files but leave the directory structure (unless used with option ` + "`--rmdirs`" + `). -To delete a path and any objects in it, use [purge](/commands/rclone_purge/) +This will delete ` + "`--checkers`" + ` directories concurrently so +if you have thousands of empty directories consider increasing this number. + +To delete a path and any objects in it, use the [purge](/commands/rclone_purge/) command. `, Annotations: map[string]string{ diff --git a/fs/operations/operations.go b/fs/operations/operations.go index 4a42a33b0..669f1c156 100644 --- a/fs/operations/operations.go +++ b/fs/operations/operations.go @@ -1551,28 +1551,69 @@ func Rmdirs(ctx context.Context, f fs.Fs, dir string, leaveRoot bool) error { if err != nil { return fmt.Errorf("failed to rmdirs: %w", err) } - // Now delete the empty directories, starting from the longest path - var toDelete []string + + // Group directories to delete by level + var toDelete [][]string for dir, empty := range dirEmpty { if empty { - toDelete = append(toDelete, dir) + // If a filter matches the directory then that + // directory is a candidate for deletion + if fi.IncludeRemote(dir + "/") { + level := strings.Count(dir, "/") + 1 + // The root directory "" is at the top level + if dir == "" { + level = 0 + } + if len(toDelete) < level+1 { + toDelete = append(toDelete, make([][]string, level+1-len(toDelete))...) + } + toDelete[level] = append(toDelete[level], dir) + } } } - sort.Strings(toDelete) - for i := len(toDelete) - 1; i >= 0; i-- { - dir := toDelete[i] - // If a filter matches the directory then that - // directory is a candidate for deletion - if !fi.IncludeRemote(dir + "/") { + + var ( + errMu sync.Mutex + errCount int + lastError error + ) + // Delete all directories at the same level in parallel + for level := len(toDelete) - 1; level >= 0; level-- { + dirs := toDelete[level] + if len(dirs) == 0 { continue } - err = TryRmdir(ctx, f, dir) + fs.Debugf(nil, "removing %d level %d directories", len(dirs), level) + sort.Strings(dirs) + g, gCtx := errgroup.WithContext(ctx) + g.SetLimit(ci.Checkers) + for _, dir := range dirs { + // End early if error + if gCtx.Err() != nil { + break + } + dir := dir + g.Go(func() error { + err := TryRmdir(gCtx, f, dir) + if err != nil { + err = fs.CountError(err) + fs.Errorf(dir, "Failed to rmdir: %v", err) + errMu.Lock() + lastError = err + errCount += 1 + errMu.Unlock() + } + return nil // don't return errors, just count them + }) + } + err := g.Wait() if err != nil { - err = fs.CountError(err) - fs.Errorf(dir, "Failed to rmdir: %v", err) return err } } + if lastError != nil { + return fmt.Errorf("failed to remove %d directories: last error: %w", errCount, lastError) + } return nil } diff --git a/fs/operations/operations_test.go b/fs/operations/operations_test.go index 4886af133..c3cb40911 100644 --- a/fs/operations/operations_test.go +++ b/fs/operations/operations_test.go @@ -702,6 +702,22 @@ func TestRmdirsNoLeaveRoot(t *testing.T) { fs.GetModifyWindow(ctx, r.Fremote), ) + // Delete the files so we can remove everything including the root + for _, file := range []fstest.Item{file1, file2} { + o, err := r.Fremote.NewObject(ctx, file.Path) + require.NoError(t, err) + require.NoError(t, o.Remove(ctx)) + } + + require.NoError(t, operations.Rmdirs(ctx, r.Fremote, "", false)) + + fstest.CheckListingWithPrecision( + t, + r.Fremote, + []fstest.Item{}, + []string{}, + fs.GetModifyWindow(ctx, r.Fremote), + ) } func TestRmdirsLeaveRoot(t *testing.T) {