diff --git a/fs/operations/operations.go b/fs/operations/operations.go index 95d987d05..d7c61e0b5 100644 --- a/fs/operations/operations.go +++ b/fs/operations/operations.go @@ -35,6 +35,7 @@ import ( "github.com/rclone/rclone/fs/object" "github.com/rclone/rclone/fs/walk" "github.com/rclone/rclone/lib/atexit" + "github.com/rclone/rclone/lib/errcount" "github.com/rclone/rclone/lib/random" "github.com/rclone/rclone/lib/readers" "golang.org/x/sync/errgroup" @@ -1353,9 +1354,7 @@ func Rmdirs(ctx context.Context, f fs.Fs, dir string, leaveRoot bool) error { } var ( - errMu sync.Mutex - errCount int - lastError error + errCount = errcount.New() ) // Delete all directories at the same level in parallel for level := len(toDelete) - 1; level >= 0; level-- { @@ -1378,10 +1377,7 @@ func Rmdirs(ctx context.Context, f fs.Fs, dir string, leaveRoot bool) error { if err != nil { err = fs.CountError(err) fs.Errorf(dir, "Failed to rmdir: %v", err) - errMu.Lock() - lastError = err - errCount += 1 - errMu.Unlock() + errCount.Add(err) } return nil // don't return errors, just count them }) @@ -1391,10 +1387,7 @@ func Rmdirs(ctx context.Context, f fs.Fs, dir string, leaveRoot bool) error { return err } } - if lastError != nil { - return fmt.Errorf("failed to remove %d directories: last error: %w", errCount, lastError) - } - return nil + return errCount.Err("failed to remove directories") } // GetCompareDest sets up --compare-dest diff --git a/lib/errcount/errcount.go b/lib/errcount/errcount.go new file mode 100644 index 000000000..a22c8da81 --- /dev/null +++ b/lib/errcount/errcount.go @@ -0,0 +1,58 @@ +// Package errcount provides an easy to use error counter which +// returns error count and last error so as to not overwhelm the user +// with errors. +package errcount + +import ( + "fmt" + "sync" +) + +// ErrCount stores the state of the error counter. +type ErrCount struct { + mu sync.Mutex + lastErr error + count int +} + +// New makes a new error counter +func New() *ErrCount { + return new(ErrCount) +} + +// Add an error to the error count. +// +// err may be nil. +// +// Thread safe. +func (ec *ErrCount) Add(err error) { + if err == nil { + return + } + ec.mu.Lock() + ec.count++ + ec.lastErr = err + ec.mu.Unlock() +} + +// Err returns the error summary so far - may be nil +// +// txt is put in front of the error summary +// +// txt: %d errors: last error: %w +// +// or this if only one error +// +// txt: %w +// +// Thread safe. +func (ec *ErrCount) Err(txt string) error { + ec.mu.Lock() + defer ec.mu.Unlock() + if ec.count == 0 { + return nil + } else if ec.count == 1 { + return fmt.Errorf("%s: %w", txt, ec.lastErr) + } + return fmt.Errorf("%s: %d errors: last error: %w", txt, ec.count, ec.lastErr) +} diff --git a/lib/errcount/errcount_test.go b/lib/errcount/errcount_test.go new file mode 100644 index 000000000..c45f897be --- /dev/null +++ b/lib/errcount/errcount_test.go @@ -0,0 +1,27 @@ +package errcount + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestErrCount(t *testing.T) { + ec := New() + assert.Equal(t, nil, ec.Err("none")) + + e1 := errors.New("one") + ec.Add(e1) + + err := ec.Err("stuff") + assert.True(t, errors.Is(err, e1), err) + assert.Equal(t, "stuff: one", err.Error()) + + e2 := errors.New("two") + ec.Add(e2) + + err = ec.Err("stuff") + assert.True(t, errors.Is(err, e2), err) + assert.Equal(t, "stuff: 2 errors: last error: two", err.Error()) +}