From be4a61fbe602dd39c2895664bd720f29829d8269 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 31 May 2026 03:27:49 +0000 Subject: [PATCH] Add app backend factory wiring --- docs/internal/storage.md | 2 +- internal/app/backends.go | 45 ++++++++++++++++++++++ internal/app/backends_test.go | 72 +++++++++++++++++++++++++++++++++++ internal/app/inspect.go | 3 +- internal/app/run.go | 20 ++-------- internal/app/validate.go | 3 +- 6 files changed, 124 insertions(+), 21 deletions(-) create mode 100644 internal/app/backends.go create mode 100644 internal/app/backends_test.go diff --git a/docs/internal/storage.md b/docs/internal/storage.md index 86bf9f0..e0553fe 100644 --- a/docs/internal/storage.md +++ b/docs/internal/storage.md @@ -14,7 +14,7 @@ Entries report a logical path, type, and size when available. Entry types are `f Core packages should depend on `internal/storage`, not adapter packages. Adapter-specific path handling stays behind backend implementations. -The local adapter lives in `internal/adapters/local`. The fake backend lives in `internal/storage/fake` for tests and is not registered for runtime use. +The local adapter lives in `internal/adapters/local`. Runtime backend construction is wired through the app-level backend factory and storage registry. The fake backend lives in `internal/storage/fake` for tests and is not registered for runtime use. ## Paths diff --git a/internal/app/backends.go b/internal/app/backends.go new file mode 100644 index 0000000..8717379 --- /dev/null +++ b/internal/app/backends.go @@ -0,0 +1,45 @@ +package app + +import ( + "context" + "fmt" + + "gitea.maximumdirect.net/eric/distributor/internal/adapters/local" + "gitea.maximumdirect.net/eric/distributor/internal/config" + "gitea.maximumdirect.net/eric/distributor/internal/storage" +) + +const storagePathKey = "path" + +type backendFactory struct { + registry *storage.Registry +} + +func newBackendFactory() *backendFactory { + registry := storage.NewRegistry() + _ = registry.Register(config.BackendLocal, func(ctx context.Context, cfg storage.OpenConfig) (storage.Backend, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + return local.New(cfg[storagePathKey]) + }) + return &backendFactory{registry: registry} +} + +func (f *backendFactory) openSource(ctx context.Context, source config.Backend) (storage.Backend, error) { + if source.Backend != config.BackendLocal { + return nil, fmt.Errorf("source backend %s is not implemented for execution", source.Backend) + } + return f.registry.Open(ctx, source.Backend, storage.OpenConfig{storagePathKey: source.Path}) +} + +func (f *backendFactory) openDestination(ctx context.Context, destination config.Destination) (storage.Backend, error) { + if destination.Backend != config.BackendLocal { + return nil, fmt.Errorf("backend %s is not implemented for execution", destination.Backend) + } + return f.registry.Open(ctx, destination.Backend, storage.OpenConfig{storagePathKey: destination.Path}) +} + +func (f *backendFactory) openLocalPath(ctx context.Context, path string) (storage.Backend, error) { + return f.registry.Open(ctx, config.BackendLocal, storage.OpenConfig{storagePathKey: path}) +} diff --git a/internal/app/backends_test.go b/internal/app/backends_test.go new file mode 100644 index 0000000..ac42bc3 --- /dev/null +++ b/internal/app/backends_test.go @@ -0,0 +1,72 @@ +package app + +import ( + "context" + "strings" + "testing" + + "gitea.maximumdirect.net/eric/distributor/internal/config" +) + +func TestBackendFactoryOpensLocalSource(t *testing.T) { + factory := newBackendFactory() + backend, err := factory.openSource(context.Background(), config.Backend{ + Backend: config.BackendLocal, + Path: t.TempDir(), + }) + if err != nil { + t.Fatalf("openSource() error = %v", err) + } + if backend == nil { + t.Fatal("openSource() backend = nil") + } +} + +func TestBackendFactoryOpensLocalDestination(t *testing.T) { + factory := newBackendFactory() + backend, err := factory.openDestination(context.Background(), config.Destination{ + Backend: config.BackendLocal, + Path: t.TempDir(), + }) + if err != nil { + t.Fatalf("openDestination() error = %v", err) + } + if backend == nil { + t.Fatal("openDestination() backend = nil") + } +} + +func TestBackendFactoryOpensDirectLocalPath(t *testing.T) { + factory := newBackendFactory() + backend, err := factory.openLocalPath(context.Background(), t.TempDir()) + if err != nil { + t.Fatalf("openLocalPath() error = %v", err) + } + if backend == nil { + t.Fatal("openLocalPath() backend = nil") + } +} + +func TestBackendFactoryRejectsUnsupportedSource(t *testing.T) { + factory := newBackendFactory() + _, err := factory.openSource(context.Background(), config.Backend{ + Backend: config.BackendSSH, + URI: "ssh://reports@example.com:22", + Path: "/reports", + }) + if err == nil || !strings.Contains(err.Error(), "source backend ssh is not implemented for execution") { + t.Fatalf("openSource() error = %v, want not implemented", err) + } +} + +func TestBackendFactoryRejectsUnsupportedDestination(t *testing.T) { + factory := newBackendFactory() + _, err := factory.openDestination(context.Background(), config.Destination{ + Backend: config.BackendS3, + Endpoint: "https://s3.example.com", + Bucket: "reports", + }) + if err == nil || !strings.Contains(err.Error(), "backend s3 is not implemented for execution") { + t.Fatalf("openDestination() error = %v, want not implemented", err) + } +} diff --git a/internal/app/inspect.go b/internal/app/inspect.go index c347110..cb2b511 100644 --- a/internal/app/inspect.go +++ b/internal/app/inspect.go @@ -5,7 +5,6 @@ import ( "fmt" "io" - "gitea.maximumdirect.net/eric/distributor/internal/adapters/local" "gitea.maximumdirect.net/eric/distributor/internal/bundle" ) @@ -18,7 +17,7 @@ func Inspect(ctx context.Context, options InspectOptions) error { if options.Path == "" { return fmt.Errorf("inspect command requires a path") } - backend, err := local.New(options.Path) + backend, err := newBackendFactory().openLocalPath(ctx, options.Path) if err != nil { return err } diff --git a/internal/app/run.go b/internal/app/run.go index 5c3fac5..e03c5a8 100644 --- a/internal/app/run.go +++ b/internal/app/run.go @@ -7,7 +7,6 @@ import ( "io" "strings" - "gitea.maximumdirect.net/eric/distributor/internal/adapters/local" "gitea.maximumdirect.net/eric/distributor/internal/bundle" "gitea.maximumdirect.net/eric/distributor/internal/config" "gitea.maximumdirect.net/eric/distributor/internal/notify" @@ -44,18 +43,16 @@ func runConfig(ctx context.Context, cfg config.Config, options RunOptions) error } summary := runSummary{dryRun: options.DryRun} var failures runFailures + backends := newBackendFactory() if options.Stdout != nil { if _, err := fmt.Fprintf(options.Stdout, "Configured pipelines: %d\n", len(cfg.Pipelines)); err != nil { return err } } for _, pipeline := range cfg.Pipelines { - if pipeline.Source.Backend != config.BackendLocal { - return fmt.Errorf("pipeline %s source backend %s is not implemented for execution", pipeline.ID, pipeline.Source.Backend) - } - sourceBackend, err := local.New(pipeline.Source.Path) + sourceBackend, err := backends.openSource(ctx, pipeline.Source) if err != nil { - return err + return fmt.Errorf("pipeline %s: %w", pipeline.ID, err) } bundles, err := bundle.Discover(ctx, sourceBackend, "") if err != nil { @@ -68,16 +65,7 @@ func runConfig(ctx context.Context, cfg config.Config, options RunOptions) error } for _, sourceBundle := range bundles { for _, destination := range pipeline.Destinations { - if destination.Backend != config.BackendLocal { - err := fmt.Errorf("backend %s is not implemented for execution", destination.Backend) - failures.add(pipeline.ID, destination.ID, displayBundlePath(sourceBundle.RootRelativePath), err) - summary.recordFailure() - if options.Stdout != nil { - writeErrorLine(options.Stdout, sourceBundle.RootRelativePath, destination.ID, err) - } - continue - } - destinationBackend, err := local.New(destination.Path) + destinationBackend, err := backends.openDestination(ctx, destination) if err != nil { failures.add(pipeline.ID, destination.ID, displayBundlePath(sourceBundle.RootRelativePath), err) summary.recordFailure() diff --git a/internal/app/validate.go b/internal/app/validate.go index 4983f69..f29bfd9 100644 --- a/internal/app/validate.go +++ b/internal/app/validate.go @@ -5,7 +5,6 @@ import ( "fmt" "io" - "gitea.maximumdirect.net/eric/distributor/internal/adapters/local" "gitea.maximumdirect.net/eric/distributor/internal/bundle" ) @@ -18,7 +17,7 @@ func Validate(ctx context.Context, options ValidateOptions) error { if options.Path == "" { return fmt.Errorf("validate command requires a path") } - backend, err := local.New(options.Path) + backend, err := newBackendFactory().openLocalPath(ctx, options.Path) if err != nil { return err }