Centralize storage walk emission
This commit is contained in:
63
internal/storage/walk.go
Normal file
63
internal/storage/walk.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type WalkEmitter struct {
|
||||
ctx context.Context
|
||||
backend string
|
||||
opts WalkOptions
|
||||
fn WalkFunc
|
||||
count int
|
||||
}
|
||||
|
||||
func NewWalkEmitter(ctx context.Context, backend string, opts WalkOptions, fn WalkFunc) *WalkEmitter {
|
||||
return &WalkEmitter{
|
||||
ctx: ctx,
|
||||
backend: backend,
|
||||
opts: opts,
|
||||
fn: fn,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *WalkEmitter) Emit(entry Entry) error {
|
||||
if err := e.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if e.opts.Limit > 0 && e.count >= e.opts.Limit {
|
||||
return ErrStopWalk
|
||||
}
|
||||
e.count++
|
||||
if err := e.fn(entry); err != nil {
|
||||
if errors.Is(err, ErrStopWalk) {
|
||||
return ErrStopWalk
|
||||
}
|
||||
return NewError(OpWalk, e.backend, entry.Path, ErrUnknown, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *WalkEmitter) LimitReached() bool {
|
||||
return e.opts.Limit > 0 && e.count >= e.opts.Limit
|
||||
}
|
||||
|
||||
func FinishWalk(err error) error {
|
||||
if errors.Is(err, ErrStopWalk) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func HasAny(ctx context.Context, backend Backend, prefix string) (bool, error) {
|
||||
found := false
|
||||
err := backend.Walk(ctx, prefix, WalkOptions{Recursive: false, Limit: 1}, func(Entry) error {
|
||||
found = true
|
||||
return ErrStopWalk
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return found, nil
|
||||
}
|
||||
107
internal/storage/walk_test.go
Normal file
107
internal/storage/walk_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWalkEmitterHonorsLimit(t *testing.T) {
|
||||
emitter := NewWalkEmitter(context.Background(), "test", WalkOptions{Limit: 2}, func(Entry) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := emitter.Emit(Entry{Path: "one"}); err != nil {
|
||||
t.Fatalf("first Emit() error = %v", err)
|
||||
}
|
||||
if err := emitter.Emit(Entry{Path: "two"}); err != nil {
|
||||
t.Fatalf("second Emit() error = %v", err)
|
||||
}
|
||||
if err := emitter.Emit(Entry{Path: "three"}); !errors.Is(err, ErrStopWalk) {
|
||||
t.Fatalf("third Emit() error = %v, want ErrStopWalk", err)
|
||||
}
|
||||
if !emitter.LimitReached() {
|
||||
t.Fatal("LimitReached() = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkEmitterStopsWithoutError(t *testing.T) {
|
||||
emitter := NewWalkEmitter(context.Background(), "test", WalkOptions{}, func(Entry) error {
|
||||
return ErrStopWalk
|
||||
})
|
||||
|
||||
err := emitter.Emit(Entry{Path: "one"})
|
||||
if !errors.Is(err, ErrStopWalk) {
|
||||
t.Fatalf("Emit() error = %v, want ErrStopWalk", err)
|
||||
}
|
||||
if err := FinishWalk(err); err != nil {
|
||||
t.Fatalf("FinishWalk() error = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkEmitterWrapsCallbackErrors(t *testing.T) {
|
||||
callbackErr := errors.New("callback failed")
|
||||
emitter := NewWalkEmitter(context.Background(), "test", WalkOptions{}, func(Entry) error {
|
||||
return callbackErr
|
||||
})
|
||||
|
||||
err := emitter.Emit(Entry{Path: "one"})
|
||||
if !errors.Is(err, callbackErr) {
|
||||
t.Fatalf("Emit() error = %v, want callback error", err)
|
||||
}
|
||||
var storageErr *Error
|
||||
if !errors.As(err, &storageErr) {
|
||||
t.Fatalf("Emit() error type = %T, want *Error", err)
|
||||
}
|
||||
if storageErr.Op != OpWalk || storageErr.Backend != "test" || storageErr.Path != "one" || storageErr.Kind != ErrUnknown {
|
||||
t.Fatalf("wrapped error = %#v", storageErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkEmitterHonorsContextCancellation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
called := false
|
||||
emitter := NewWalkEmitter(ctx, "test", WalkOptions{}, func(Entry) error {
|
||||
called = true
|
||||
return nil
|
||||
})
|
||||
|
||||
err := emitter.Emit(Entry{Path: "one"})
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Emit() error = %v, want context.Canceled", err)
|
||||
}
|
||||
if called {
|
||||
t.Fatal("callback was called after context cancellation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasAnyUsesNonRecursiveLimitOneWalk(t *testing.T) {
|
||||
backend := &recordingBackend{}
|
||||
|
||||
found, err := HasAny(context.Background(), backend, "bundle")
|
||||
if err != nil {
|
||||
t.Fatalf("HasAny() error = %v", err)
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("HasAny() = false, want true")
|
||||
}
|
||||
if backend.prefix != "bundle" {
|
||||
t.Fatalf("walk prefix = %q, want bundle", backend.prefix)
|
||||
}
|
||||
if backend.opts != (WalkOptions{Recursive: false, Limit: 1}) {
|
||||
t.Fatalf("walk options = %#v, want non-recursive limit one", backend.opts)
|
||||
}
|
||||
}
|
||||
|
||||
type recordingBackend struct {
|
||||
Backend
|
||||
prefix string
|
||||
opts WalkOptions
|
||||
}
|
||||
|
||||
func (b *recordingBackend) Walk(_ context.Context, prefix string, opts WalkOptions, fn WalkFunc) error {
|
||||
b.prefix = prefix
|
||||
b.opts = opts
|
||||
return FinishWalk(fn(Entry{Path: "bundle/file.txt", Type: EntryTypeFile}))
|
||||
}
|
||||
Reference in New Issue
Block a user