@streetjs/storage 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +719 -0
- package/dist/access.d.ts +95 -0
- package/dist/access.d.ts.map +1 -0
- package/dist/access.js +114 -0
- package/dist/access.js.map +1 -0
- package/dist/cli/commands.d.ts +88 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +369 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/generators.d.ts +41 -0
- package/dist/cli/generators.d.ts.map +1 -0
- package/dist/cli/generators.js +119 -0
- package/dist/cli/generators.js.map +1 -0
- package/dist/directory.d.ts +88 -0
- package/dist/directory.d.ts.map +1 -0
- package/dist/directory.js +148 -0
- package/dist/directory.js.map +1 -0
- package/dist/driver.d.ts +83 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +16 -0
- package/dist/driver.js.map +1 -0
- package/dist/drivers/azure.d.ts +239 -0
- package/dist/drivers/azure.d.ts.map +1 -0
- package/dist/drivers/azure.js +476 -0
- package/dist/drivers/azure.js.map +1 -0
- package/dist/drivers/backblaze.d.ts +105 -0
- package/dist/drivers/backblaze.d.ts.map +1 -0
- package/dist/drivers/backblaze.js +279 -0
- package/dist/drivers/backblaze.js.map +1 -0
- package/dist/drivers/gcs.d.ts +239 -0
- package/dist/drivers/gcs.d.ts.map +1 -0
- package/dist/drivers/gcs.js +454 -0
- package/dist/drivers/gcs.js.map +1 -0
- package/dist/drivers/local.d.ts +126 -0
- package/dist/drivers/local.d.ts.map +1 -0
- package/dist/drivers/local.js +321 -0
- package/dist/drivers/local.js.map +1 -0
- package/dist/drivers/memory.d.ts +104 -0
- package/dist/drivers/memory.d.ts.map +1 -0
- package/dist/drivers/memory.js +197 -0
- package/dist/drivers/memory.js.map +1 -0
- package/dist/drivers/minio.d.ts +90 -0
- package/dist/drivers/minio.d.ts.map +1 -0
- package/dist/drivers/minio.js +276 -0
- package/dist/drivers/minio.js.map +1 -0
- package/dist/drivers/r2.d.ts +100 -0
- package/dist/drivers/r2.d.ts.map +1 -0
- package/dist/drivers/r2.js +240 -0
- package/dist/drivers/r2.js.map +1 -0
- package/dist/drivers/s3-base.d.ts +270 -0
- package/dist/drivers/s3-base.d.ts.map +1 -0
- package/dist/drivers/s3-base.js +436 -0
- package/dist/drivers/s3-base.js.map +1 -0
- package/dist/drivers/s3.d.ts +111 -0
- package/dist/drivers/s3.d.ts.map +1 -0
- package/dist/drivers/s3.js +311 -0
- package/dist/drivers/s3.js.map +1 -0
- package/dist/drivers/supabase.d.ts +246 -0
- package/dist/drivers/supabase.d.ts.map +1 -0
- package/dist/drivers/supabase.js +502 -0
- package/dist/drivers/supabase.js.map +1 -0
- package/dist/errors.d.ts +115 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +131 -0
- package/dist/errors.js.map +1 -0
- package/dist/examples/storage-demo.d.ts +53 -0
- package/dist/examples/storage-demo.d.ts.map +1 -0
- package/dist/examples/storage-demo.js +242 -0
- package/dist/examples/storage-demo.js.map +1 -0
- package/dist/facade.d.ts +192 -0
- package/dist/facade.d.ts.map +1 -0
- package/dist/facade.js +898 -0
- package/dist/facade.js.map +1 -0
- package/dist/image.d.ts +94 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +224 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/events.d.ts +108 -0
- package/dist/integrations/events.d.ts.map +1 -0
- package/dist/integrations/events.js +111 -0
- package/dist/integrations/events.js.map +1 -0
- package/dist/integrations/queue.d.ts +89 -0
- package/dist/integrations/queue.d.ts.map +1 -0
- package/dist/integrations/queue.js +91 -0
- package/dist/integrations/queue.js.map +1 -0
- package/dist/integrations/realtime.d.ts +95 -0
- package/dist/integrations/realtime.d.ts.map +1 -0
- package/dist/integrations/realtime.js +90 -0
- package/dist/integrations/realtime.js.map +1 -0
- package/dist/lifecycle.d.ts +137 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +240 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/metadata.d.ts +109 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +139 -0
- package/dist/metadata.js.map +1 -0
- package/dist/multipart.d.ts +108 -0
- package/dist/multipart.d.ts.map +1 -0
- package/dist/multipart.js +200 -0
- package/dist/multipart.js.map +1 -0
- package/dist/observability.d.ts +82 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +175 -0
- package/dist/observability.js.map +1 -0
- package/dist/plugin.d.ts +82 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +100 -0
- package/dist/plugin.js.map +1 -0
- package/dist/resumable.d.ts +118 -0
- package/dist/resumable.d.ts.map +1 -0
- package/dist/resumable.js +223 -0
- package/dist/resumable.js.map +1 -0
- package/dist/search.d.ts +57 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +172 -0
- package/dist/search.js.map +1 -0
- package/dist/signed-url.d.ts +110 -0
- package/dist/signed-url.d.ts.map +1 -0
- package/dist/signed-url.js +237 -0
- package/dist/signed-url.js.map +1 -0
- package/dist/testing/index.d.ts +219 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +393 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/tests/access.test.d.ts +2 -0
- package/dist/tests/access.test.d.ts.map +1 -0
- package/dist/tests/access.test.js +270 -0
- package/dist/tests/access.test.js.map +1 -0
- package/dist/tests/azure-driver.test.d.ts +2 -0
- package/dist/tests/azure-driver.test.d.ts.map +1 -0
- package/dist/tests/azure-driver.test.js +183 -0
- package/dist/tests/azure-driver.test.js.map +1 -0
- package/dist/tests/backblaze-driver.test.d.ts +2 -0
- package/dist/tests/backblaze-driver.test.d.ts.map +1 -0
- package/dist/tests/backblaze-driver.test.js +185 -0
- package/dist/tests/backblaze-driver.test.js.map +1 -0
- package/dist/tests/bridge-degradation.test.d.ts +2 -0
- package/dist/tests/bridge-degradation.test.d.ts.map +1 -0
- package/dist/tests/bridge-degradation.test.js +138 -0
- package/dist/tests/bridge-degradation.test.js.map +1 -0
- package/dist/tests/cli.test.d.ts +2 -0
- package/dist/tests/cli.test.d.ts.map +1 -0
- package/dist/tests/cli.test.js +127 -0
- package/dist/tests/cli.test.js.map +1 -0
- package/dist/tests/contract.d.ts +131 -0
- package/dist/tests/contract.d.ts.map +1 -0
- package/dist/tests/contract.js +222 -0
- package/dist/tests/contract.js.map +1 -0
- package/dist/tests/contract.test.d.ts +2 -0
- package/dist/tests/contract.test.d.ts.map +1 -0
- package/dist/tests/contract.test.js +110 -0
- package/dist/tests/contract.test.js.map +1 -0
- package/dist/tests/directory.test.d.ts +2 -0
- package/dist/tests/directory.test.d.ts.map +1 -0
- package/dist/tests/directory.test.js +125 -0
- package/dist/tests/directory.test.js.map +1 -0
- package/dist/tests/errors.test.d.ts +2 -0
- package/dist/tests/errors.test.d.ts.map +1 -0
- package/dist/tests/errors.test.js +93 -0
- package/dist/tests/errors.test.js.map +1 -0
- package/dist/tests/events-bridge.test.d.ts +2 -0
- package/dist/tests/events-bridge.test.d.ts.map +1 -0
- package/dist/tests/events-bridge.test.js +228 -0
- package/dist/tests/events-bridge.test.js.map +1 -0
- package/dist/tests/example-smoke.test.d.ts +2 -0
- package/dist/tests/example-smoke.test.d.ts.map +1 -0
- package/dist/tests/example-smoke.test.js +34 -0
- package/dist/tests/example-smoke.test.js.map +1 -0
- package/dist/tests/gcs-driver.test.d.ts +2 -0
- package/dist/tests/gcs-driver.test.d.ts.map +1 -0
- package/dist/tests/gcs-driver.test.js +197 -0
- package/dist/tests/gcs-driver.test.js.map +1 -0
- package/dist/tests/image.test.d.ts +2 -0
- package/dist/tests/image.test.d.ts.map +1 -0
- package/dist/tests/image.test.js +102 -0
- package/dist/tests/image.test.js.map +1 -0
- package/dist/tests/lifecycle.test.d.ts +2 -0
- package/dist/tests/lifecycle.test.d.ts.map +1 -0
- package/dist/tests/lifecycle.test.js +171 -0
- package/dist/tests/lifecycle.test.js.map +1 -0
- package/dist/tests/local-driver.test.d.ts +2 -0
- package/dist/tests/local-driver.test.d.ts.map +1 -0
- package/dist/tests/local-driver.test.js +153 -0
- package/dist/tests/local-driver.test.js.map +1 -0
- package/dist/tests/local-streaming.test.d.ts +2 -0
- package/dist/tests/local-streaming.test.d.ts.map +1 -0
- package/dist/tests/local-streaming.test.js +121 -0
- package/dist/tests/local-streaming.test.js.map +1 -0
- package/dist/tests/memory-driver.test.d.ts +2 -0
- package/dist/tests/memory-driver.test.d.ts.map +1 -0
- package/dist/tests/memory-driver.test.js +180 -0
- package/dist/tests/memory-driver.test.js.map +1 -0
- package/dist/tests/memory-streaming.test.d.ts +2 -0
- package/dist/tests/memory-streaming.test.d.ts.map +1 -0
- package/dist/tests/memory-streaming.test.js +92 -0
- package/dist/tests/memory-streaming.test.js.map +1 -0
- package/dist/tests/minio-driver.test.d.ts +2 -0
- package/dist/tests/minio-driver.test.d.ts.map +1 -0
- package/dist/tests/minio-driver.test.js +146 -0
- package/dist/tests/minio-driver.test.js.map +1 -0
- package/dist/tests/non-s3-integration.test.d.ts +2 -0
- package/dist/tests/non-s3-integration.test.d.ts.map +1 -0
- package/dist/tests/non-s3-integration.test.js +201 -0
- package/dist/tests/non-s3-integration.test.js.map +1 -0
- package/dist/tests/observability.registration.test.d.ts +2 -0
- package/dist/tests/observability.registration.test.d.ts.map +1 -0
- package/dist/tests/observability.registration.test.js +103 -0
- package/dist/tests/observability.registration.test.js.map +1 -0
- package/dist/tests/observability.test.d.ts +2 -0
- package/dist/tests/observability.test.d.ts.map +1 -0
- package/dist/tests/observability.test.js +240 -0
- package/dist/tests/observability.test.js.map +1 -0
- package/dist/tests/plugin.test.d.ts +2 -0
- package/dist/tests/plugin.test.d.ts.map +1 -0
- package/dist/tests/plugin.test.js +112 -0
- package/dist/tests/plugin.test.js.map +1 -0
- package/dist/tests/prop-concurrent.test.d.ts +2 -0
- package/dist/tests/prop-concurrent.test.d.ts.map +1 -0
- package/dist/tests/prop-concurrent.test.js +59 -0
- package/dist/tests/prop-concurrent.test.js.map +1 -0
- package/dist/tests/prop-copy.test.d.ts +2 -0
- package/dist/tests/prop-copy.test.d.ts.map +1 -0
- package/dist/tests/prop-copy.test.js +63 -0
- package/dist/tests/prop-copy.test.js.map +1 -0
- package/dist/tests/prop-delete.test.d.ts +2 -0
- package/dist/tests/prop-delete.test.d.ts.map +1 -0
- package/dist/tests/prop-delete.test.js +35 -0
- package/dist/tests/prop-delete.test.js.map +1 -0
- package/dist/tests/prop-directory.test.d.ts +2 -0
- package/dist/tests/prop-directory.test.d.ts.map +1 -0
- package/dist/tests/prop-directory.test.js +118 -0
- package/dist/tests/prop-directory.test.js.map +1 -0
- package/dist/tests/prop-driver-conformance.test.d.ts +2 -0
- package/dist/tests/prop-driver-conformance.test.d.ts.map +1 -0
- package/dist/tests/prop-driver-conformance.test.js +110 -0
- package/dist/tests/prop-driver-conformance.test.js.map +1 -0
- package/dist/tests/prop-driver-equivalence.test.d.ts +2 -0
- package/dist/tests/prop-driver-equivalence.test.d.ts.map +1 -0
- package/dist/tests/prop-driver-equivalence.test.js +163 -0
- package/dist/tests/prop-driver-equivalence.test.js.map +1 -0
- package/dist/tests/prop-lifecycle.test.d.ts +2 -0
- package/dist/tests/prop-lifecycle.test.d.ts.map +1 -0
- package/dist/tests/prop-lifecycle.test.js +83 -0
- package/dist/tests/prop-lifecycle.test.js.map +1 -0
- package/dist/tests/prop-list.test.d.ts +2 -0
- package/dist/tests/prop-list.test.d.ts.map +1 -0
- package/dist/tests/prop-list.test.js +53 -0
- package/dist/tests/prop-list.test.js.map +1 -0
- package/dist/tests/prop-metadata.test.d.ts +2 -0
- package/dist/tests/prop-metadata.test.d.ts.map +1 -0
- package/dist/tests/prop-metadata.test.js +87 -0
- package/dist/tests/prop-metadata.test.js.map +1 -0
- package/dist/tests/prop-move.test.d.ts +2 -0
- package/dist/tests/prop-move.test.d.ts.map +1 -0
- package/dist/tests/prop-move.test.js +67 -0
- package/dist/tests/prop-move.test.js.map +1 -0
- package/dist/tests/prop-multipart-abort.test.d.ts +2 -0
- package/dist/tests/prop-multipart-abort.test.d.ts.map +1 -0
- package/dist/tests/prop-multipart-abort.test.js +48 -0
- package/dist/tests/prop-multipart-abort.test.js.map +1 -0
- package/dist/tests/prop-multipart.test.d.ts +2 -0
- package/dist/tests/prop-multipart.test.d.ts.map +1 -0
- package/dist/tests/prop-multipart.test.js +65 -0
- package/dist/tests/prop-multipart.test.js.map +1 -0
- package/dist/tests/prop-resumable.test.d.ts +2 -0
- package/dist/tests/prop-resumable.test.d.ts.map +1 -0
- package/dist/tests/prop-resumable.test.js +78 -0
- package/dist/tests/prop-resumable.test.js.map +1 -0
- package/dist/tests/prop-search.test.d.ts +2 -0
- package/dist/tests/prop-search.test.d.ts.map +1 -0
- package/dist/tests/prop-search.test.js +120 -0
- package/dist/tests/prop-search.test.js.map +1 -0
- package/dist/tests/prop-signed-url.test.d.ts +2 -0
- package/dist/tests/prop-signed-url.test.d.ts.map +1 -0
- package/dist/tests/prop-signed-url.test.js +74 -0
- package/dist/tests/prop-signed-url.test.js.map +1 -0
- package/dist/tests/prop-streaming.test.d.ts +2 -0
- package/dist/tests/prop-streaming.test.d.ts.map +1 -0
- package/dist/tests/prop-streaming.test.js +34 -0
- package/dist/tests/prop-streaming.test.js.map +1 -0
- package/dist/tests/prop-upload-download.test.d.ts +2 -0
- package/dist/tests/prop-upload-download.test.d.ts.map +1 -0
- package/dist/tests/prop-upload-download.test.js +30 -0
- package/dist/tests/prop-upload-download.test.js.map +1 -0
- package/dist/tests/prop-validation.test.d.ts +2 -0
- package/dist/tests/prop-validation.test.d.ts.map +1 -0
- package/dist/tests/prop-validation.test.js +50 -0
- package/dist/tests/prop-validation.test.js.map +1 -0
- package/dist/tests/prop-versioning.test.d.ts +2 -0
- package/dist/tests/prop-versioning.test.d.ts.map +1 -0
- package/dist/tests/prop-versioning.test.js +74 -0
- package/dist/tests/prop-versioning.test.js.map +1 -0
- package/dist/tests/queue-bridge.test.d.ts +2 -0
- package/dist/tests/queue-bridge.test.d.ts.map +1 -0
- package/dist/tests/queue-bridge.test.js +111 -0
- package/dist/tests/queue-bridge.test.js.map +1 -0
- package/dist/tests/r2-driver.test.d.ts +2 -0
- package/dist/tests/r2-driver.test.d.ts.map +1 -0
- package/dist/tests/r2-driver.test.js +143 -0
- package/dist/tests/r2-driver.test.js.map +1 -0
- package/dist/tests/realtime-bridge.test.d.ts +2 -0
- package/dist/tests/realtime-bridge.test.d.ts.map +1 -0
- package/dist/tests/realtime-bridge.test.js +106 -0
- package/dist/tests/realtime-bridge.test.js.map +1 -0
- package/dist/tests/resumable.test.d.ts +2 -0
- package/dist/tests/resumable.test.d.ts.map +1 -0
- package/dist/tests/resumable.test.js +104 -0
- package/dist/tests/resumable.test.js.map +1 -0
- package/dist/tests/s3-base.test.d.ts +2 -0
- package/dist/tests/s3-base.test.d.ts.map +1 -0
- package/dist/tests/s3-base.test.js +289 -0
- package/dist/tests/s3-base.test.js.map +1 -0
- package/dist/tests/s3-integration.test.d.ts +2 -0
- package/dist/tests/s3-integration.test.d.ts.map +1 -0
- package/dist/tests/s3-integration.test.js +270 -0
- package/dist/tests/s3-integration.test.js.map +1 -0
- package/dist/tests/s3.test.d.ts +2 -0
- package/dist/tests/s3.test.d.ts.map +1 -0
- package/dist/tests/s3.test.js +203 -0
- package/dist/tests/s3.test.js.map +1 -0
- package/dist/tests/scaffold.test.d.ts +2 -0
- package/dist/tests/scaffold.test.d.ts.map +1 -0
- package/dist/tests/scaffold.test.js +13 -0
- package/dist/tests/scaffold.test.js.map +1 -0
- package/dist/tests/search.test.d.ts +2 -0
- package/dist/tests/search.test.d.ts.map +1 -0
- package/dist/tests/search.test.js +106 -0
- package/dist/tests/search.test.js.map +1 -0
- package/dist/tests/supabase-driver.test.d.ts +2 -0
- package/dist/tests/supabase-driver.test.d.ts.map +1 -0
- package/dist/tests/supabase-driver.test.js +196 -0
- package/dist/tests/supabase-driver.test.js.map +1 -0
- package/dist/tests/testing-utilities.test.d.ts +2 -0
- package/dist/tests/testing-utilities.test.d.ts.map +1 -0
- package/dist/tests/testing-utilities.test.js +138 -0
- package/dist/tests/testing-utilities.test.js.map +1 -0
- package/dist/tests/validation.test.d.ts +2 -0
- package/dist/tests/validation.test.d.ts.map +1 -0
- package/dist/tests/validation.test.js +135 -0
- package/dist/tests/validation.test.js.map +1 -0
- package/dist/tests/versioning.test.d.ts +2 -0
- package/dist/tests/versioning.test.d.ts.map +1 -0
- package/dist/tests/versioning.test.js +128 -0
- package/dist/tests/versioning.test.js.map +1 -0
- package/dist/types.d.ts +232 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +105 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +225 -0
- package/dist/validation.js.map +1 -0
- package/dist/versioning.d.ts +100 -0
- package/dist/versioning.d.ts.map +1 -0
- package/dist/versioning.js +179 -0
- package/dist/versioning.js.map +1 -0
- package/package.json +137 -0
package/dist/facade.js
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @streetjs/storage — the storage facade and configuration-driven construction.
|
|
3
|
+
*
|
|
4
|
+
* Application code talks only to the {@link Storage} facade returned by
|
|
5
|
+
* {@link createStorage}. The facade presents one public surface that is
|
|
6
|
+
* identical no matter which {@link StorageDriver} backs it, delegating the
|
|
7
|
+
* actual byte persistence to the driver while running (in later tasks) the
|
|
8
|
+
* cross-cutting layers (validation, access control, versioning, lifecycle,
|
|
9
|
+
* streaming/multipart/resumable sessions, signed URLs, images, directory,
|
|
10
|
+
* search, observability, integration bridges).
|
|
11
|
+
*
|
|
12
|
+
* This module (task 5.1) implements **construction and provider selection**:
|
|
13
|
+
*
|
|
14
|
+
* - A built-in driver registry resolves the zero-dependency providers `memory`
|
|
15
|
+
* ({@link MemoryStorageDriver}) and `local` ({@link LocalStorageDriver},
|
|
16
|
+
* which requires `config.root`).
|
|
17
|
+
* - Cloud providers are supplied as an already-constructed `config.driver`
|
|
18
|
+
* instance (imported from the relevant submodule), which takes precedence and
|
|
19
|
+
* works for any provider name.
|
|
20
|
+
* - An unknown provider name with no supplied driver yields a descriptive
|
|
21
|
+
* {@link StorageConfigError} and **no** {@link Storage} instance is produced
|
|
22
|
+
* (Requirement 1.5). Because the error is simply thrown (never caught and
|
|
23
|
+
* swallowed into a fabricated instance), if constructing the error object
|
|
24
|
+
* itself fails for some unexpected internal reason, that internal failure
|
|
25
|
+
* surfaces as a thrown error and, again, no instance is produced
|
|
26
|
+
* (Requirement 1.6).
|
|
27
|
+
*
|
|
28
|
+
* The object-operation method bodies (`put`/`get`/`exists`/`delete`/`copy`/
|
|
29
|
+
* `move`/`rename`/`list`/`stat`), streaming, multipart, resumable, signed URLs,
|
|
30
|
+
* versioning, images, directory, search, and observability are implemented in
|
|
31
|
+
* later tasks (5.2 onward). They are stubbed here so the file compiles, while
|
|
32
|
+
* the facade is structured as a class holding the resolved `driver` + `config`
|
|
33
|
+
* so those tasks can fill in the behavior cleanly.
|
|
34
|
+
*
|
|
35
|
+
* _Requirements: 1.1, 1.2, 1.4, 1.5, 1.6_
|
|
36
|
+
*/
|
|
37
|
+
import { createHash } from "node:crypto";
|
|
38
|
+
import { Buffer } from "node:buffer";
|
|
39
|
+
import { AccessController } from "./access.js";
|
|
40
|
+
import { StorageDirectoryApi } from "./directory.js";
|
|
41
|
+
import { StorageImageProcessor } from "./image.js";
|
|
42
|
+
import { MemoryStorageDriver } from "./drivers/memory.js";
|
|
43
|
+
import { LocalStorageDriver } from "./drivers/local.js";
|
|
44
|
+
import { StorageConfigError, ValidationError } from "./errors.js";
|
|
45
|
+
import { LifecycleEngine } from "./lifecycle.js";
|
|
46
|
+
import { DEFAULT_ACCESS_LEVEL, normalizeMetadata, toWriteMetadata } from "./metadata.js";
|
|
47
|
+
import { MultipartManager } from "./multipart.js";
|
|
48
|
+
import { ResumableManager } from "./resumable.js";
|
|
49
|
+
import { searchObjects } from "./search.js";
|
|
50
|
+
import { SignedUrlService } from "./signed-url.js";
|
|
51
|
+
import { ValidationPipeline } from "./validation.js";
|
|
52
|
+
import { VersioningManager } from "./versioning.js";
|
|
53
|
+
import { bridgeStorageEvents } from "./integrations/events.js";
|
|
54
|
+
import { bridgeStorageQueue } from "./integrations/queue.js";
|
|
55
|
+
import { bridgeStorageRealtime } from "./integrations/realtime.js";
|
|
56
|
+
import { registerStorageObservability } from "./observability.js";
|
|
57
|
+
/**
|
|
58
|
+
* The built-in, zero-dependency provider registry (Requirement 1.2). Cloud
|
|
59
|
+
* providers are never listed here — they are supplied as a pre-constructed
|
|
60
|
+
* `config.driver` instead (see {@link resolveDriver}).
|
|
61
|
+
*/
|
|
62
|
+
const BUILT_IN_DRIVERS = {
|
|
63
|
+
memory: (config) => new MemoryStorageDriver({ clock: config.clock }),
|
|
64
|
+
local: (config) => {
|
|
65
|
+
if (config.root === undefined || config.root === "") {
|
|
66
|
+
throw new StorageConfigError('The "local" storage provider requires a "root" directory in its configuration.', { provider: "local" });
|
|
67
|
+
}
|
|
68
|
+
return new LocalStorageDriver({ root: config.root, clock: config.clock });
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the {@link StorageDriver} for the given configuration.
|
|
73
|
+
*
|
|
74
|
+
* Precedence:
|
|
75
|
+
* 1. A pre-constructed `config.driver` (used for cloud providers imported from
|
|
76
|
+
* submodules) is used as-is, regardless of the `provider` name.
|
|
77
|
+
* 2. Otherwise the `provider` name is looked up in the built-in registry and the
|
|
78
|
+
* corresponding zero-dependency driver is constructed.
|
|
79
|
+
* 3. If the `provider` name is unknown and no `driver` was supplied, a
|
|
80
|
+
* descriptive {@link StorageConfigError} is thrown and no driver (and hence
|
|
81
|
+
* no {@link Storage} instance) is produced (Requirement 1.5).
|
|
82
|
+
*
|
|
83
|
+
* The error is thrown, never caught-and-swallowed, so an unexpected failure
|
|
84
|
+
* while constructing the error object itself simply propagates as a thrown
|
|
85
|
+
* internal error — neither an instance nor a fabricated error is returned
|
|
86
|
+
* (Requirement 1.6).
|
|
87
|
+
*/
|
|
88
|
+
function resolveDriver(config) {
|
|
89
|
+
if (config.driver !== undefined) {
|
|
90
|
+
return config.driver;
|
|
91
|
+
}
|
|
92
|
+
const factory = BUILT_IN_DRIVERS[config.provider];
|
|
93
|
+
if (factory !== undefined) {
|
|
94
|
+
return factory(config);
|
|
95
|
+
}
|
|
96
|
+
throw new StorageConfigError(`Unknown storage provider "${config.provider}". Provide a built-in provider ` +
|
|
97
|
+
`("memory" or "local"), or supply a pre-constructed "driver" for cloud providers.`, { provider: config.provider });
|
|
98
|
+
}
|
|
99
|
+
// ── Facade implementation ──────────────────────────────────────────────────
|
|
100
|
+
/**
|
|
101
|
+
* Concrete {@link Storage} implementation holding the resolved {@link StorageDriver}
|
|
102
|
+
* and the originating {@link StorageConfig}. Task 5.1 wires construction and
|
|
103
|
+
* provider selection; the object-operation, streaming, multipart, resumable,
|
|
104
|
+
* signed-URL, versioning, image, directory, search, and observability method
|
|
105
|
+
* bodies are filled in by later tasks. Until then the not-yet-implemented
|
|
106
|
+
* methods throw a clear, descriptive error.
|
|
107
|
+
*/
|
|
108
|
+
class StorageFacade {
|
|
109
|
+
/** The resolved driver every operation delegates byte persistence to. */
|
|
110
|
+
driver;
|
|
111
|
+
/** The configuration this facade was constructed with. */
|
|
112
|
+
config;
|
|
113
|
+
/**
|
|
114
|
+
* The pre-persistence validation pipeline, present only when
|
|
115
|
+
* `config.validation` is supplied. When defined it is run as the first step
|
|
116
|
+
* of every write so a rejection aborts the write before any bytes reach the
|
|
117
|
+
* driver, leaving no partial object stored (Requirements 9.3, 9.4).
|
|
118
|
+
*/
|
|
119
|
+
validation;
|
|
120
|
+
/**
|
|
121
|
+
* The provider-agnostic multipart upload manager. It delegates to the driver's
|
|
122
|
+
* native `multipart` capability when present and otherwise simulates multipart
|
|
123
|
+
* over the driver primitives, so `createMultipartUpload`/`uploadPart`/
|
|
124
|
+
* `completeMultipartUpload`/`abortMultipartUpload` behave identically across
|
|
125
|
+
* providers (Requirement 6).
|
|
126
|
+
*/
|
|
127
|
+
multipart;
|
|
128
|
+
/**
|
|
129
|
+
* The provider-agnostic resumable upload manager. It delegates to the driver's
|
|
130
|
+
* native `resumable` capability when present and otherwise simulates
|
|
131
|
+
* offset-tracked sessions over the driver primitives, so `startUpload`/
|
|
132
|
+
* `resumeUpload`/`cancelUpload` behave identically across providers
|
|
133
|
+
* (Requirement 7).
|
|
134
|
+
*/
|
|
135
|
+
resumable;
|
|
136
|
+
/**
|
|
137
|
+
* The provider-agnostic signed URL service. It delegates to the driver's
|
|
138
|
+
* native `signedUrl` capability when present and otherwise mints/verifies
|
|
139
|
+
* HMAC-signed URLs over `(key, op, expiry)` using `config.signingSecret`,
|
|
140
|
+
* checking expiry against the injected clock, so `signedUrl` behaves
|
|
141
|
+
* identically across providers (Requirement 8).
|
|
142
|
+
*/
|
|
143
|
+
signedUrls;
|
|
144
|
+
/**
|
|
145
|
+
* The provider-agnostic versioning manager. It delegates to the driver's
|
|
146
|
+
* native `versioning` capability when present and otherwise simulates version
|
|
147
|
+
* snapshots over the driver primitives (reserved `.versions/<key>/<versionId>`
|
|
148
|
+
* copies), so `listVersions`/`restoreVersion`/`deleteVersion` and the
|
|
149
|
+
* overwrite-time snapshot behave identically across providers (Requirement
|
|
150
|
+
* 12). The snapshot step is only taken when `config.versioning === true`.
|
|
151
|
+
*/
|
|
152
|
+
versioning;
|
|
153
|
+
/**
|
|
154
|
+
* The provider-agnostic lifecycle engine. It delegates to the driver's native
|
|
155
|
+
* `lifecycle` capability when present and otherwise simulates rule evaluation
|
|
156
|
+
* over the driver primitives (`list`/`stat`/`get`/`put`/`delete`), measuring
|
|
157
|
+
* object age against the injected clock and applying each rule's action to a
|
|
158
|
+
* qualifying object exactly once, so `applyLifecycle` behaves identically
|
|
159
|
+
* across providers (Requirement 13).
|
|
160
|
+
*/
|
|
161
|
+
lifecycle;
|
|
162
|
+
/**
|
|
163
|
+
* The provider-agnostic access controller. It resolves per-object
|
|
164
|
+
* {@link AccessLevel} decisions through the optional structural `config.auth`
|
|
165
|
+
* bridge, denying disallowed operations with an {@link AuthorizationError}
|
|
166
|
+
* before any persistence or read occurs (Requirement 11). When no auth bridge
|
|
167
|
+
* is configured it is a permissive no-op, so drivers that never configure
|
|
168
|
+
* access control behave exactly as before.
|
|
169
|
+
*/
|
|
170
|
+
access;
|
|
171
|
+
/**
|
|
172
|
+
* The typed Events bridge, present only when `config.bridges?.events` is
|
|
173
|
+
* supplied. When defined, object mutations (`put` → uploaded/updated,
|
|
174
|
+
* `delete` → deleted, `move`/`rename` → moved, `restoreVersion` → restored)
|
|
175
|
+
* and applied lifecycle actions (`applyLifecycle`) publish the corresponding
|
|
176
|
+
* typed `storage.*` event through it (Requirements 13.4, 18.1, 18.2). Every
|
|
177
|
+
* publish is isolated so a failing events layer never breaks the storage
|
|
178
|
+
* operation. When no events bridge is configured this is `undefined` and event
|
|
179
|
+
* publication is a complete no-op.
|
|
180
|
+
*/
|
|
181
|
+
events;
|
|
182
|
+
/**
|
|
183
|
+
* The typed Queue bridge, present only when `config.bridges?.queue` is
|
|
184
|
+
* supplied. When defined, heavy out-of-band work (thumbnail generation, virus
|
|
185
|
+
* scanning, OCR, PDF processing, transcoding, image optimization, archive
|
|
186
|
+
* creation) can be handed off through it (Requirement 17.1). Every dispatch is
|
|
187
|
+
* isolated so a failing queue never breaks the storage operation
|
|
188
|
+
* (Requirement 17.4). When no queue bridge is configured this is `undefined`
|
|
189
|
+
* and job dispatch is a complete no-op — operations proceed unaffected
|
|
190
|
+
* (Requirement 17.3).
|
|
191
|
+
*/
|
|
192
|
+
queue;
|
|
193
|
+
/**
|
|
194
|
+
* The typed Realtime bridge, present only when `config.bridges?.realtime` is
|
|
195
|
+
* supplied. When defined, upload state transitions (`putStream`/`resumeUpload`
|
|
196
|
+
* → started/completed, and failed on error) broadcast the corresponding typed
|
|
197
|
+
* `upload.*` event through it (Requirement 19.1). Every broadcast is isolated
|
|
198
|
+
* so a failing realtime layer never breaks the upload (Requirement 19.3). When
|
|
199
|
+
* no realtime bridge is configured this is `undefined` and broadcasting is a
|
|
200
|
+
* complete no-op — uploads proceed unaffected (Requirement 19.3).
|
|
201
|
+
*/
|
|
202
|
+
realtime;
|
|
203
|
+
/**
|
|
204
|
+
* The observability handle, present only when `config.metrics` or
|
|
205
|
+
* `config.health` is supplied. When defined, its telemetry sink is fed live as
|
|
206
|
+
* operations occur (uploads/downloads/bytes/failed/active/multipart/resumable
|
|
207
|
+
* counters + latency histogram — Requirement 23.2) and its `attach` registers
|
|
208
|
+
* the `storage` health check sourced from {@link probe} (Requirement 23.3).
|
|
209
|
+
* When neither registry is configured this is `undefined` and every telemetry
|
|
210
|
+
* call is a complete no-op. Metrics/health reuse the existing core registries
|
|
211
|
+
* only; no parallel system is introduced (Requirements 23.1, 23.4).
|
|
212
|
+
*/
|
|
213
|
+
observability;
|
|
214
|
+
/**
|
|
215
|
+
* The live, mutable statistics tracked as operations occur. `stats()` returns
|
|
216
|
+
* an immutable snapshot of this state (Requirement 23.2). Kept independent of
|
|
217
|
+
* the optional metrics registry so `stats()` is always available even when no
|
|
218
|
+
* observability is wired.
|
|
219
|
+
*/
|
|
220
|
+
statsState = {
|
|
221
|
+
uploads: 0,
|
|
222
|
+
downloads: 0,
|
|
223
|
+
bytesUploaded: 0,
|
|
224
|
+
bytesDownloaded: 0,
|
|
225
|
+
activeUploads: 0,
|
|
226
|
+
failedUploads: 0,
|
|
227
|
+
storageUsage: 0,
|
|
228
|
+
multipartUploads: 0,
|
|
229
|
+
resumableSessions: 0,
|
|
230
|
+
};
|
|
231
|
+
constructor(driver, config) {
|
|
232
|
+
this.driver = driver;
|
|
233
|
+
this.config = config;
|
|
234
|
+
this.events =
|
|
235
|
+
config.bridges?.events !== undefined
|
|
236
|
+
? bridgeStorageEvents(config.bridges.events)
|
|
237
|
+
: undefined;
|
|
238
|
+
this.queue =
|
|
239
|
+
config.bridges?.queue !== undefined
|
|
240
|
+
? bridgeStorageQueue(config.bridges.queue)
|
|
241
|
+
: undefined;
|
|
242
|
+
this.realtime =
|
|
243
|
+
config.bridges?.realtime !== undefined
|
|
244
|
+
? bridgeStorageRealtime(config.bridges.realtime)
|
|
245
|
+
: undefined;
|
|
246
|
+
this.validation =
|
|
247
|
+
config.validation !== undefined ? new ValidationPipeline(config.validation) : undefined;
|
|
248
|
+
this.multipart = new MultipartManager(driver);
|
|
249
|
+
this.resumable = new ResumableManager(driver);
|
|
250
|
+
this.signedUrls = new SignedUrlService({
|
|
251
|
+
signingSecret: config.signingSecret,
|
|
252
|
+
clock: config.clock,
|
|
253
|
+
driver,
|
|
254
|
+
});
|
|
255
|
+
this.versioning = new VersioningManager(driver);
|
|
256
|
+
this.lifecycle = new LifecycleEngine({ driver, clock: config.clock });
|
|
257
|
+
this.access = new AccessController({ auth: config.auth });
|
|
258
|
+
// Wire observability only when a metrics and/or health registry is supplied.
|
|
259
|
+
// The handle registers its metrics/health against the existing core
|
|
260
|
+
// registries idempotently and its telemetry sink is fed live below; when
|
|
261
|
+
// neither is configured this stays undefined and telemetry is a no-op
|
|
262
|
+
// (Requirements 23.1, 23.3, 23.4).
|
|
263
|
+
this.observability =
|
|
264
|
+
config.metrics !== undefined || config.health !== undefined
|
|
265
|
+
? registerStorageObservability({ metrics: config.metrics, health: config.health })
|
|
266
|
+
: undefined;
|
|
267
|
+
this.observability?.attach(this);
|
|
268
|
+
}
|
|
269
|
+
// ── Stats recording helpers (task 22.1) ─────────────────────────────────────
|
|
270
|
+
/**
|
|
271
|
+
* Record a successful upload of `bytes` taking `latencySeconds`: advances the
|
|
272
|
+
* upload / bytes-uploaded counters and the tracked storage usage, and feeds
|
|
273
|
+
* the live telemetry sink (Requirement 23.2).
|
|
274
|
+
*/
|
|
275
|
+
recordUpload(bytes, latencySeconds) {
|
|
276
|
+
this.statsState.uploads += 1;
|
|
277
|
+
this.statsState.bytesUploaded += bytes;
|
|
278
|
+
this.statsState.storageUsage += bytes;
|
|
279
|
+
this.observability?.telemetry.onUpload?.(bytes, latencySeconds);
|
|
280
|
+
this.observability?.telemetry.onStorageUsage?.(this.statsState.storageUsage);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Record a successful download of `bytes` taking `latencySeconds`: advances
|
|
284
|
+
* the download / bytes-downloaded counters and feeds the telemetry sink.
|
|
285
|
+
*/
|
|
286
|
+
recordDownload(bytes, latencySeconds) {
|
|
287
|
+
this.statsState.downloads += 1;
|
|
288
|
+
this.statsState.bytesDownloaded += bytes;
|
|
289
|
+
this.observability?.telemetry.onDownload?.(bytes, latencySeconds);
|
|
290
|
+
}
|
|
291
|
+
/** Record a failed upload attempt (Requirement 23.2). */
|
|
292
|
+
recordUploadFailure() {
|
|
293
|
+
this.statsState.failedUploads += 1;
|
|
294
|
+
this.observability?.telemetry.onUploadFailed?.();
|
|
295
|
+
}
|
|
296
|
+
/** Mark an in-flight (streamed/resumable) upload as started. */
|
|
297
|
+
recordActiveStart() {
|
|
298
|
+
this.statsState.activeUploads += 1;
|
|
299
|
+
this.observability?.telemetry.onActiveUploadsChange?.(this.statsState.activeUploads);
|
|
300
|
+
}
|
|
301
|
+
/** Mark an in-flight (streamed/resumable) upload as finished. */
|
|
302
|
+
recordActiveEnd() {
|
|
303
|
+
if (this.statsState.activeUploads > 0) {
|
|
304
|
+
this.statsState.activeUploads -= 1;
|
|
305
|
+
}
|
|
306
|
+
this.observability?.telemetry.onActiveUploadsChange?.(this.statsState.activeUploads);
|
|
307
|
+
}
|
|
308
|
+
/** Record a created multipart upload (Requirement 23.2). */
|
|
309
|
+
recordMultipart() {
|
|
310
|
+
this.statsState.multipartUploads += 1;
|
|
311
|
+
this.observability?.telemetry.onMultipartUpload?.();
|
|
312
|
+
}
|
|
313
|
+
/** Record a started resumable upload session (Requirement 23.2). */
|
|
314
|
+
recordResumable() {
|
|
315
|
+
this.statsState.resumableSessions += 1;
|
|
316
|
+
this.observability?.telemetry.onResumableSession?.();
|
|
317
|
+
}
|
|
318
|
+
/** Current epoch-ms reading used to measure operation latency. */
|
|
319
|
+
nowMs() {
|
|
320
|
+
return Date.now();
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Run the access-control check (when an auth bridge is configured) for
|
|
324
|
+
* `operation` on `key` at the given `accessLevel`, throwing an
|
|
325
|
+
* {@link AuthorizationError} on denial so the guarded operation performs no
|
|
326
|
+
* persistence or read (Requirement 11.3). When no auth bridge is configured
|
|
327
|
+
* this is a no-op and every operation is permitted.
|
|
328
|
+
*/
|
|
329
|
+
async authorizeAccess(key, operation, accessLevel, owner, tenant) {
|
|
330
|
+
await this.access.authorize({ key, operation, accessLevel, owner, tenant });
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Run the validation pipeline (when configured) against `input` and throw a
|
|
334
|
+
* {@link ValidationError} on rejection. This is invoked before any call to
|
|
335
|
+
* `driver.put`, so a rejected upload never persists content (Requirement
|
|
336
|
+
* 9.3) and leaves no partial object behind (Requirement 9.4). When no
|
|
337
|
+
* validation is configured this is a no-op.
|
|
338
|
+
*/
|
|
339
|
+
async runValidation(input) {
|
|
340
|
+
if (this.validation === undefined) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const result = await this.validation.validate(input);
|
|
344
|
+
if (!result.ok) {
|
|
345
|
+
throw new ValidationError(result.error ?? "upload rejected by validation", {
|
|
346
|
+
key: input.key,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// ── Object operations (task 5.2) ────────────────────────────────────────────
|
|
351
|
+
/**
|
|
352
|
+
* Persist `content` under `key` and return the resulting metadata
|
|
353
|
+
* (Requirement 4.1). A `string` payload is encoded as UTF-8 bytes before it
|
|
354
|
+
* reaches the driver so all providers store and hash the identical byte
|
|
355
|
+
* sequence; a `Uint8Array` is passed through untouched. Write-time metadata in
|
|
356
|
+
* `options` (content type, owner, tenant, access level, custom fields) is
|
|
357
|
+
* forwarded to the driver, which computes `etag`/`checksum`/`size`/timestamps.
|
|
358
|
+
*/
|
|
359
|
+
async put(key, content, options) {
|
|
360
|
+
const bytes = typeof content === "string" ? encodeUtf8(content) : content;
|
|
361
|
+
const startedMs = this.nowMs();
|
|
362
|
+
try {
|
|
363
|
+
// Access control is the first gate: a denied write throws an
|
|
364
|
+
// AuthorizationError before any validation or persistence, so nothing is
|
|
365
|
+
// written (Requirement 11.3). No-op when no auth bridge is configured.
|
|
366
|
+
await this.authorizeAccess(key, "write", options?.accessLevel ?? DEFAULT_ACCESS_LEVEL, options?.owner, options?.tenant);
|
|
367
|
+
// Validate the fully-known size/contentType/checksum BEFORE any persistence
|
|
368
|
+
// so a rejection aborts the write with no partial object stored (Req 9.3/9.4).
|
|
369
|
+
await this.runValidation({
|
|
370
|
+
key,
|
|
371
|
+
size: bytes.byteLength,
|
|
372
|
+
contentType: options?.contentType,
|
|
373
|
+
checksum: sha256Hex(bytes),
|
|
374
|
+
metadata: options,
|
|
375
|
+
});
|
|
376
|
+
// When versioning is enabled, snapshot the current content (if any) BEFORE
|
|
377
|
+
// the overwrite so the prior Version is retained (Requirement 12.1). The
|
|
378
|
+
// snapshot never throws into this path: a versioning failure returns null
|
|
379
|
+
// and the overwrite proceeds without a Version (Requirement 12.5).
|
|
380
|
+
if (this.config.versioning === true) {
|
|
381
|
+
await this.versioning.snapshot(key);
|
|
382
|
+
}
|
|
383
|
+
// Determine whether this write creates a new object or overwrites an
|
|
384
|
+
// existing one so the correct event (`storage.uploaded` vs
|
|
385
|
+
// `storage.updated`) is published (Requirement 18.1). The existence probe is
|
|
386
|
+
// only performed when an events bridge is configured, so the default path is
|
|
387
|
+
// unchanged and adds no driver call.
|
|
388
|
+
const existedBefore = this.events !== undefined ? await this.driver.exists(key) : false;
|
|
389
|
+
// Surface the complete, typed metadata field set (Requirement 10.1) through
|
|
390
|
+
// the single source of truth so the shape is consistent across drivers.
|
|
391
|
+
const metadata = normalizeMetadata(await this.driver.put(key, bytes, options ?? {}));
|
|
392
|
+
// Publish uploaded/updated after a successful persist. The bridge never
|
|
393
|
+
// throws into this path (Requirement 18.1, 18.2).
|
|
394
|
+
if (this.events !== undefined) {
|
|
395
|
+
if (existedBefore) {
|
|
396
|
+
this.events.updated(metadata);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
this.events.uploaded(metadata);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Record the successful upload for stats/metrics (Requirement 23.2).
|
|
403
|
+
this.recordUpload(bytes.byteLength, (this.nowMs() - startedMs) / 1000);
|
|
404
|
+
return metadata;
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
// Any pre-persistence rejection (auth/validation) or driver failure counts
|
|
408
|
+
// as a failed upload (Requirement 23.2). The error propagates unchanged.
|
|
409
|
+
this.recordUploadFailure();
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Read the object at `key`, converting the driver's discriminated
|
|
415
|
+
* {@link MaybeObject} into the facade {@link GetResult} shape (Requirement
|
|
416
|
+
* 4.2). A present object yields `{ found: true, bytes, metadata }` with the
|
|
417
|
+
* stored bytes returned unchanged; an absent one yields `{ found: false }`
|
|
418
|
+
* without throwing.
|
|
419
|
+
*/
|
|
420
|
+
async get(key) {
|
|
421
|
+
// When access control is enforced, resolve the object's access level from
|
|
422
|
+
// its metadata and authorize the read BEFORE returning any bytes; a denied
|
|
423
|
+
// read throws an AuthorizationError and no content is read (Requirement
|
|
424
|
+
// 11.3). A `public` object is readable without authentication unless the
|
|
425
|
+
// configured bridge blocks it (Requirement 11.4). This lookup is skipped
|
|
426
|
+
// entirely when no auth bridge is configured, leaving the default path
|
|
427
|
+
// unchanged.
|
|
428
|
+
if (this.access.enforced) {
|
|
429
|
+
const metadata = await this.driver.stat(key);
|
|
430
|
+
if (metadata !== null) {
|
|
431
|
+
await this.authorizeAccess(key, "read", metadata.accessLevel, metadata.owner, metadata.tenant);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const startedMs = this.nowMs();
|
|
435
|
+
const result = await this.driver.get(key);
|
|
436
|
+
if (result.found) {
|
|
437
|
+
// Record the successful download for stats/metrics (Requirement 23.2).
|
|
438
|
+
this.recordDownload(result.bytes.byteLength, (this.nowMs() - startedMs) / 1000);
|
|
439
|
+
return { found: true, bytes: result.bytes, metadata: normalizeMetadata(result.metadata) };
|
|
440
|
+
}
|
|
441
|
+
return { found: false };
|
|
442
|
+
}
|
|
443
|
+
/** Report whether an object is stored under `key` (Requirement 4.3). */
|
|
444
|
+
async exists(key) {
|
|
445
|
+
return this.driver.exists(key);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Remove the object at `key` so a subsequent `exists` returns false
|
|
449
|
+
* (Requirement 4.4). Deleting a missing key is a no-op at the driver level.
|
|
450
|
+
*/
|
|
451
|
+
async delete(key) {
|
|
452
|
+
// Authorize the delete against the object's access level (Requirement
|
|
453
|
+
// 11.3). Skipped when no auth bridge is configured; a missing object has no
|
|
454
|
+
// access level to enforce and the driver delete remains a no-op.
|
|
455
|
+
if (this.access.enforced) {
|
|
456
|
+
const metadata = await this.driver.stat(key);
|
|
457
|
+
if (metadata !== null) {
|
|
458
|
+
await this.authorizeAccess(key, "delete", metadata.accessLevel, metadata.owner, metadata.tenant);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
await this.driver.delete(key);
|
|
462
|
+
// Publish `storage.deleted` after the removal (Requirement 18.1). The bridge
|
|
463
|
+
// never throws into this path.
|
|
464
|
+
if (this.events !== undefined) {
|
|
465
|
+
this.events.deleted(key);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Copy the object at `source` to `destination` (Requirement 4.5). The copy is
|
|
470
|
+
* **non-mutating**: the source is read and its content plus metadata are
|
|
471
|
+
* written to the destination, and the source object is never touched. When
|
|
472
|
+
* `source` does not exist, no write occurs and a not-found result
|
|
473
|
+
* (`{ copied: false }`) is returned without throwing (Requirement 4.6).
|
|
474
|
+
*/
|
|
475
|
+
async copy(source, destination) {
|
|
476
|
+
const result = await this.driver.get(source);
|
|
477
|
+
if (!result.found) {
|
|
478
|
+
return { copied: false };
|
|
479
|
+
}
|
|
480
|
+
const metadata = normalizeMetadata(await this.driver.put(destination, result.bytes, toWriteMetadata(result.metadata)));
|
|
481
|
+
return { copied: true, metadata };
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Move the object at `source` to `destination` (Requirements 4.7). The source
|
|
485
|
+
* content is written to the destination and then the source object is
|
|
486
|
+
* removed. When `source` does not exist, no operation is performed and a
|
|
487
|
+
* not-found result (`{ moved: false }`) is returned without throwing.
|
|
488
|
+
*/
|
|
489
|
+
async move(source, destination) {
|
|
490
|
+
const result = await this.driver.get(source);
|
|
491
|
+
if (!result.found) {
|
|
492
|
+
return { moved: false };
|
|
493
|
+
}
|
|
494
|
+
const metadata = normalizeMetadata(await this.driver.put(destination, result.bytes, toWriteMetadata(result.metadata)));
|
|
495
|
+
await this.driver.delete(source);
|
|
496
|
+
// Publish `storage.moved` for the relocated object (Requirement 18.1); this
|
|
497
|
+
// also covers `rename`, which delegates here. The bridge never throws into
|
|
498
|
+
// this path.
|
|
499
|
+
if (this.events !== undefined) {
|
|
500
|
+
this.events.moved(metadata);
|
|
501
|
+
}
|
|
502
|
+
return { moved: true, metadata };
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Rename the object at `key` to `newKey` (Requirement 4.8). This has the same
|
|
506
|
+
* semantics as {@link move}: the content becomes available under `newKey` and
|
|
507
|
+
* the old key is removed; a missing source yields `{ moved: false }` without
|
|
508
|
+
* throwing.
|
|
509
|
+
*/
|
|
510
|
+
async rename(key, newKey) {
|
|
511
|
+
return this.move(key, newKey);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Return the keys (with size/timestamp) of objects whose keys begin with
|
|
515
|
+
* `prefix`, delegating directly to the driver (Requirement 4.9).
|
|
516
|
+
*/
|
|
517
|
+
async list(prefix, options) {
|
|
518
|
+
return this.driver.list(prefix, options);
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Return the metadata for `key` without its content, or `null` if absent
|
|
522
|
+
* (Requirement 4.10).
|
|
523
|
+
*/
|
|
524
|
+
async stat(key) {
|
|
525
|
+
const metadata = await this.driver.stat(key);
|
|
526
|
+
return metadata === null ? null : normalizeMetadata(metadata);
|
|
527
|
+
}
|
|
528
|
+
// ── Streaming (task 7.1) ─────────────────────────────────────────────────────
|
|
529
|
+
/**
|
|
530
|
+
* Persist the content read from a Node {@link NodeReadable} under `key` and
|
|
531
|
+
* return the resulting metadata (Requirement 5.1). The stream is forwarded
|
|
532
|
+
* directly to the driver's streaming primitive along with the write-time
|
|
533
|
+
* metadata in `options` (content type, owner, tenant, access level, custom
|
|
534
|
+
* fields); the driver pipes the content through to storage and never buffers
|
|
535
|
+
* the complete object in memory (Requirement 5.3), which is what lets large
|
|
536
|
+
* files transfer without loading fully into memory.
|
|
537
|
+
*/
|
|
538
|
+
async putStream(key, stream, options) {
|
|
539
|
+
// Access control gates the streamed write before any bytes reach the driver
|
|
540
|
+
// (Requirement 11.3). No-op when no auth bridge is configured.
|
|
541
|
+
await this.authorizeAccess(key, "write", options?.accessLevel ?? DEFAULT_ACCESS_LEVEL, options?.owner, options?.tenant);
|
|
542
|
+
// Broadcast the upload state transitions through the realtime bridge when
|
|
543
|
+
// configured (Requirement 19.1). Every broadcast is isolated so it never
|
|
544
|
+
// breaks the upload path (Requirement 19.3).
|
|
545
|
+
const startedMs = this.nowMs();
|
|
546
|
+
this.recordActiveStart();
|
|
547
|
+
this.realtime?.started(key);
|
|
548
|
+
try {
|
|
549
|
+
// With no validation configured, stream straight through the driver so
|
|
550
|
+
// large files never fully buffer (Requirement 5.3).
|
|
551
|
+
if (this.validation === undefined) {
|
|
552
|
+
const metadata = normalizeMetadata(await this.driver.putStream(key, stream, options ?? {}));
|
|
553
|
+
this.realtime?.completed(key);
|
|
554
|
+
this.recordUpload(metadata.size, (this.nowMs() - startedMs) / 1000);
|
|
555
|
+
return metadata;
|
|
556
|
+
}
|
|
557
|
+
// With validation configured, size and checksum can only be known once the
|
|
558
|
+
// full stream is collected. We buffer the stream, validate the complete
|
|
559
|
+
// input, and only then persist via `driver.put`. Because the driver is not
|
|
560
|
+
// touched until validation passes, a rejection leaves no partial object
|
|
561
|
+
// stored (Requirement 9.4); persistence still happens as a single atomic
|
|
562
|
+
// write on success (Requirement 9.3).
|
|
563
|
+
const bytes = await collectStream(stream);
|
|
564
|
+
await this.runValidation({
|
|
565
|
+
key,
|
|
566
|
+
size: bytes.byteLength,
|
|
567
|
+
contentType: options?.contentType,
|
|
568
|
+
checksum: sha256Hex(bytes),
|
|
569
|
+
metadata: options,
|
|
570
|
+
});
|
|
571
|
+
const metadata = normalizeMetadata(await this.driver.put(key, bytes, options ?? {}));
|
|
572
|
+
this.realtime?.completed(key);
|
|
573
|
+
this.recordUpload(metadata.size, (this.nowMs() - startedMs) / 1000);
|
|
574
|
+
return metadata;
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
this.realtime?.failed(key, error instanceof Error ? error.message : String(error));
|
|
578
|
+
this.recordUploadFailure();
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
finally {
|
|
582
|
+
// The streamed upload is no longer in flight regardless of outcome.
|
|
583
|
+
this.recordActiveEnd();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Return a Node {@link NodeReadable} of the content stored at `key`
|
|
588
|
+
* (Requirement 5.2). The stream is produced by the driver's streaming
|
|
589
|
+
* primitive so bytes are pulled incrementally rather than buffered in full
|
|
590
|
+
* (Requirement 5.3), and piping it to a Node Writable delivers the stored
|
|
591
|
+
* bytes unchanged (Requirement 5.4). When `key` does not exist the driver
|
|
592
|
+
* throws {@link NotFoundError}, which propagates unchanged to the caller
|
|
593
|
+
* (Requirement 5.5).
|
|
594
|
+
*/
|
|
595
|
+
async getStream(key) {
|
|
596
|
+
// Authorize the streamed read against the object's access level before
|
|
597
|
+
// producing any stream (Requirement 11.3). Skipped entirely when no auth
|
|
598
|
+
// bridge is configured so the default path is unchanged.
|
|
599
|
+
if (this.access.enforced) {
|
|
600
|
+
const metadata = await this.driver.stat(key);
|
|
601
|
+
if (metadata !== null) {
|
|
602
|
+
await this.authorizeAccess(key, "read", metadata.accessLevel, metadata.owner, metadata.tenant);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return this.driver.getStream(key);
|
|
606
|
+
}
|
|
607
|
+
// ── Multipart (task 10.1) ────────────────────────────────────────────────────
|
|
608
|
+
/**
|
|
609
|
+
* Begin a multipart upload for `key` and return its upload identifier
|
|
610
|
+
* (Requirement 6.1). Write-time metadata in `options` is captured so the
|
|
611
|
+
* assembled object is written with the intended content type / ownership /
|
|
612
|
+
* access level / custom fields at completion.
|
|
613
|
+
*/
|
|
614
|
+
createMultipartUpload(key, options) {
|
|
615
|
+
// Count the created multipart upload for stats/metrics (Requirement 23.2).
|
|
616
|
+
this.recordMultipart();
|
|
617
|
+
return this.multipart.create(key, options ?? {});
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Persist a single part for `uploadId` and return its {@link StoredPart}
|
|
621
|
+
* descriptor (Requirement 6.2).
|
|
622
|
+
*/
|
|
623
|
+
uploadPart(uploadId, partNumber, content) {
|
|
624
|
+
return this.multipart.uploadPart(uploadId, partNumber, content);
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Assemble the supplied ordered `parts` into the final object and return its
|
|
628
|
+
* metadata; the content equals the concatenation of the parts in order,
|
|
629
|
+
* equivalent to a single `put` of that concatenation (Requirement 6.3).
|
|
630
|
+
*/
|
|
631
|
+
completeMultipartUpload(uploadId, parts) {
|
|
632
|
+
return this.multipart.complete(uploadId, parts);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Discard all uploaded parts for `uploadId` and create no completed object
|
|
636
|
+
* (Requirement 6.4).
|
|
637
|
+
*/
|
|
638
|
+
abortMultipartUpload(uploadId) {
|
|
639
|
+
return this.multipart.abort(uploadId);
|
|
640
|
+
}
|
|
641
|
+
// ── Resumable (task 11.1) ────────────────────────────────────────────────────
|
|
642
|
+
/**
|
|
643
|
+
* Create a resumable upload session for `key` and return its session id
|
|
644
|
+
* (Requirement 7.1). Write-time metadata in `options` is captured so the
|
|
645
|
+
* object created on completion carries the intended content type / ownership /
|
|
646
|
+
* access level / custom fields.
|
|
647
|
+
*/
|
|
648
|
+
startUpload(key, options) {
|
|
649
|
+
// Count the started resumable session for stats/metrics (Requirement 23.2).
|
|
650
|
+
this.recordResumable();
|
|
651
|
+
return this.resumable.start(key, options ?? {});
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Continue the session `sessionId` from its last persisted offset using the
|
|
655
|
+
* full content carried by `stream`, creating the final object on completion
|
|
656
|
+
* and returning its metadata (Requirements 7.2, 7.3). The completed object is
|
|
657
|
+
* byte-identical to an equivalent uninterrupted upload.
|
|
658
|
+
*/
|
|
659
|
+
async resumeUpload(sessionId, stream) {
|
|
660
|
+
// Broadcast the resumed upload's state transitions through the realtime
|
|
661
|
+
// bridge when configured (Requirement 19.1); every broadcast is isolated so
|
|
662
|
+
// it never breaks the upload path (Requirement 19.3).
|
|
663
|
+
const startedMs = this.nowMs();
|
|
664
|
+
this.recordActiveStart();
|
|
665
|
+
this.realtime?.started(sessionId);
|
|
666
|
+
try {
|
|
667
|
+
const metadata = await this.resumable.resume(sessionId, stream);
|
|
668
|
+
this.realtime?.completed(metadata.key);
|
|
669
|
+
this.recordUpload(metadata.size, (this.nowMs() - startedMs) / 1000);
|
|
670
|
+
return metadata;
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
this.realtime?.failed(sessionId, error instanceof Error ? error.message : String(error));
|
|
674
|
+
this.recordUploadFailure();
|
|
675
|
+
throw error;
|
|
676
|
+
}
|
|
677
|
+
finally {
|
|
678
|
+
// The resumed upload is no longer in flight regardless of outcome.
|
|
679
|
+
this.recordActiveEnd();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Discard the session `sessionId` without creating an object (Requirement
|
|
684
|
+
* 7.4), unless it is already completing, in which case the upload is allowed
|
|
685
|
+
* to finish and the object is created (Requirement 7.5).
|
|
686
|
+
*/
|
|
687
|
+
cancelUpload(sessionId) {
|
|
688
|
+
return this.resumable.cancel(sessionId);
|
|
689
|
+
}
|
|
690
|
+
// ── Signed URLs (task 13.1) ──────────────────────────────────────────────────
|
|
691
|
+
/**
|
|
692
|
+
* Mint a URL authorizing exactly the operation `op` on `key` (Requirement
|
|
693
|
+
* 8.1), carrying the accepted options for expiration, request headers, content
|
|
694
|
+
* type, maximum size, and custom metadata (Requirement 8.2). Delegates to the
|
|
695
|
+
* driver's native signed-URL capability when present and otherwise mints an
|
|
696
|
+
* HMAC-signed URL over `(key, op, expiry)` using `config.signingSecret`
|
|
697
|
+
* (throwing a {@link StorageConfigError} when that secret is absent).
|
|
698
|
+
*/
|
|
699
|
+
signedUrl(key, op, options) {
|
|
700
|
+
return this.signedUrls.sign(key, op, options);
|
|
701
|
+
}
|
|
702
|
+
// ── Versioning (task 14.1) ───────────────────────────────────────────────────
|
|
703
|
+
/**
|
|
704
|
+
* Return the {@link VersionInfo} descriptors of the retained Versions for
|
|
705
|
+
* `key` (Requirement 12.2), delegating to the versioning manager (native
|
|
706
|
+
* capability when present, otherwise the reserved-key simulation).
|
|
707
|
+
*/
|
|
708
|
+
listVersions(key) {
|
|
709
|
+
return this.versioning.listVersions(key);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Make the content of the Version identified by `versionId` the current
|
|
713
|
+
* content of `key` and return the resulting metadata (Requirement 12.3).
|
|
714
|
+
*/
|
|
715
|
+
async restoreVersion(key, versionId) {
|
|
716
|
+
const metadata = await this.versioning.restoreVersion(key, versionId);
|
|
717
|
+
// Publish `storage.restored` after the restore succeeds (Requirement 18.1).
|
|
718
|
+
// The bridge never throws into this path.
|
|
719
|
+
if (this.events !== undefined) {
|
|
720
|
+
this.events.restored(metadata);
|
|
721
|
+
}
|
|
722
|
+
return metadata;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Remove exactly the Version identified by `versionId` for `key` while
|
|
726
|
+
* retaining the remaining Versions (Requirement 12.4).
|
|
727
|
+
*/
|
|
728
|
+
deleteVersion(key, versionId) {
|
|
729
|
+
return this.versioning.deleteVersion(key, versionId);
|
|
730
|
+
}
|
|
731
|
+
// ── Lifecycle (task 15.1) ────────────────────────────────────────────────────
|
|
732
|
+
/**
|
|
733
|
+
* Evaluate the lifecycle `rule` and apply its action to every qualifying
|
|
734
|
+
* object, returning one {@link LifecycleOutcome} per actioned object
|
|
735
|
+
* (Requirements 13.1, 13.2). Object age is measured against the configured
|
|
736
|
+
* clock, and each qualifying object is actioned exactly once — a repeated
|
|
737
|
+
* evaluation produces no further action on an already-actioned object.
|
|
738
|
+
* Delegates to the driver's native `lifecycle` capability when present and
|
|
739
|
+
* otherwise simulates the rule over the driver primitives (Requirement 13.3).
|
|
740
|
+
* When an events bridge is configured, each applied action publishes its
|
|
741
|
+
* corresponding typed lifecycle event through the bridge (Requirement 13.4);
|
|
742
|
+
* publication is isolated so a failing events layer never affects the returned
|
|
743
|
+
* outcomes.
|
|
744
|
+
*/
|
|
745
|
+
async applyLifecycle(rule) {
|
|
746
|
+
const outcomes = await this.lifecycle.apply(rule);
|
|
747
|
+
if (this.events !== undefined) {
|
|
748
|
+
for (const outcome of outcomes) {
|
|
749
|
+
this.events.lifecycle(outcome);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return outcomes;
|
|
753
|
+
}
|
|
754
|
+
// ── Image processing (task 19.1) ─────────────────────────────────────────────
|
|
755
|
+
/**
|
|
756
|
+
* The lazily-constructed image processor exposed as `storage.images`. Built on
|
|
757
|
+
* first access and reused thereafter so its transformation cache is shared
|
|
758
|
+
* across calls on the same facade (Requirement 14.3).
|
|
759
|
+
*/
|
|
760
|
+
imageProcessor;
|
|
761
|
+
/**
|
|
762
|
+
* The image processing surface (`transform`) implemented over the driver and
|
|
763
|
+
* the optional structural `config.imageCodec`. It supports the
|
|
764
|
+
* resize/crop/rotate/fit/thumbnail/compress transformations and webp/avif/png/
|
|
765
|
+
* jpeg output formats (Requirements 14.1, 14.2), caches identical
|
|
766
|
+
* transformations (Requirement 14.3), and rejects a non-image source with an
|
|
767
|
+
* {@link UnsupportedImageError} without modifying the source object
|
|
768
|
+
* (Requirement 14.4). Lazily constructed and cached.
|
|
769
|
+
*/
|
|
770
|
+
get images() {
|
|
771
|
+
if (this.imageProcessor === undefined) {
|
|
772
|
+
this.imageProcessor = new StorageImageProcessor(this.driver, this.config.imageCodec);
|
|
773
|
+
}
|
|
774
|
+
return this.imageProcessor;
|
|
775
|
+
}
|
|
776
|
+
// ── Directory API (task 17.1) ────────────────────────────────────────────────
|
|
777
|
+
/**
|
|
778
|
+
* The lazily-constructed Directory API over the driver's flat key space
|
|
779
|
+
* (Requirement 15). Built on first access to `storage.directory` and reused
|
|
780
|
+
* thereafter so directory operations share one instance per facade.
|
|
781
|
+
*/
|
|
782
|
+
directoryApi;
|
|
783
|
+
/**
|
|
784
|
+
* The directory-style API (`mkdir`/`listDirectory`/`removeDirectory`/`walk`)
|
|
785
|
+
* implemented entirely over the driver's flat key space using `/`-delimited
|
|
786
|
+
* prefixes, so it behaves identically across every provider including
|
|
787
|
+
* prefix-only cloud stores (Requirement 15). Lazily constructed and cached.
|
|
788
|
+
*/
|
|
789
|
+
get directory() {
|
|
790
|
+
if (this.directoryApi === undefined) {
|
|
791
|
+
this.directoryApi = new StorageDirectoryApi(this.driver);
|
|
792
|
+
}
|
|
793
|
+
return this.directoryApi;
|
|
794
|
+
}
|
|
795
|
+
// ── Search (task 18.1) ───────────────────────────────────────────────────────
|
|
796
|
+
/**
|
|
797
|
+
* Return the stored objects that satisfy **every** supplied filter
|
|
798
|
+
* (Requirement 16.2), evaluated over the driver's `list` + `stat` primitives.
|
|
799
|
+
* Filtering is conjunctive across prefix, content type, owner, tenant, size
|
|
800
|
+
* range, updated-time range, and custom metadata (Requirement 16.1); when no
|
|
801
|
+
* object matches, an empty result set is returned (Requirement 16.3).
|
|
802
|
+
*/
|
|
803
|
+
search(filters) {
|
|
804
|
+
return searchObjects(this.driver, filters);
|
|
805
|
+
}
|
|
806
|
+
// ── Observability (task 22.1) ────────────────────────────────────────────────
|
|
807
|
+
/**
|
|
808
|
+
* Return an immutable snapshot of the live operation statistics tracked as
|
|
809
|
+
* uploads/downloads/multipart/resumable operations occur: uploads, downloads,
|
|
810
|
+
* bytes uploaded/downloaded, active uploads, failed uploads, storage usage,
|
|
811
|
+
* multipart uploads, and resumable sessions (Requirement 23.2). Always
|
|
812
|
+
* available regardless of whether a metrics registry is configured.
|
|
813
|
+
*/
|
|
814
|
+
stats() {
|
|
815
|
+
return {
|
|
816
|
+
uploads: this.statsState.uploads,
|
|
817
|
+
downloads: this.statsState.downloads,
|
|
818
|
+
bytesUploaded: this.statsState.bytesUploaded,
|
|
819
|
+
bytesDownloaded: this.statsState.bytesDownloaded,
|
|
820
|
+
activeUploads: this.statsState.activeUploads,
|
|
821
|
+
failedUploads: this.statsState.failedUploads,
|
|
822
|
+
storageUsage: this.statsState.storageUsage,
|
|
823
|
+
multipartUploads: this.statsState.multipartUploads,
|
|
824
|
+
resumableSessions: this.statsState.resumableSessions,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Return the driver's best-effort connectivity/quota probe used by the
|
|
829
|
+
* registered health check (Requirement 23.3). Delegates to the driver's
|
|
830
|
+
* optional `probe()` when present; otherwise returns a sensible all-available
|
|
831
|
+
* default (the zero-dependency `memory`/`local` drivers are always reachable,
|
|
832
|
+
* readable, writable, and unbounded).
|
|
833
|
+
*/
|
|
834
|
+
async probe() {
|
|
835
|
+
if (this.driver.probe !== undefined) {
|
|
836
|
+
return this.driver.probe();
|
|
837
|
+
}
|
|
838
|
+
return { connectivity: true, writable: true, readable: true, quotaAvailable: true };
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Release any resources held by the facade. Stops the observability handle's
|
|
842
|
+
* refresh timer (when observability is wired); the driver and bridges hold no
|
|
843
|
+
* timers of their own.
|
|
844
|
+
*/
|
|
845
|
+
async close() {
|
|
846
|
+
this.observability?.close();
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
/** Encode a string payload as UTF-8 bytes for storage. */
|
|
850
|
+
function encodeUtf8(value) {
|
|
851
|
+
return new TextEncoder().encode(value);
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Compute the lowercase sha-256 hex digest of `bytes`, matching the checksum
|
|
855
|
+
* scheme the drivers compute at write time so the value passed to the
|
|
856
|
+
* validation pipeline is identical to the object's stored checksum.
|
|
857
|
+
*/
|
|
858
|
+
function sha256Hex(bytes) {
|
|
859
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Drain a Node {@link NodeReadable} fully into a single {@link Uint8Array}. Used
|
|
863
|
+
* by `putStream` only when a validation pipeline is configured, so size and
|
|
864
|
+
* checksum can be computed and validated before any content is persisted. Each
|
|
865
|
+
* chunk is normalized to a `Buffer` before concatenation.
|
|
866
|
+
*/
|
|
867
|
+
async function collectStream(stream) {
|
|
868
|
+
const chunks = [];
|
|
869
|
+
for await (const chunk of stream) {
|
|
870
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
871
|
+
}
|
|
872
|
+
return new Uint8Array(Buffer.concat(chunks));
|
|
873
|
+
}
|
|
874
|
+
// ── Public entry point ─────────────────────────────────────────────────────
|
|
875
|
+
/**
|
|
876
|
+
* Construct a {@link Storage} instance backed by the driver selected in `config`
|
|
877
|
+
* (Requirements 1.1, 1.2). The returned facade exposes an identical public
|
|
878
|
+
* method set for every provider (Requirement 1.4).
|
|
879
|
+
*
|
|
880
|
+
* Provider selection:
|
|
881
|
+
* - `config.driver`, when supplied, is used directly (cloud providers imported
|
|
882
|
+
* from `@streetjs/storage/*` submodules).
|
|
883
|
+
* - Otherwise `config.provider` selects a built-in zero-dependency driver
|
|
884
|
+
* (`memory` or `local`; `local` requires `config.root`).
|
|
885
|
+
*
|
|
886
|
+
* @throws StorageConfigError when the provider name is unknown and no `driver`
|
|
887
|
+
* is supplied — no {@link Storage} instance is produced (Requirement 1.5). If
|
|
888
|
+
* constructing that error object itself fails unexpectedly, the internal
|
|
889
|
+
* failure surfaces as a thrown error and, likewise, no instance is produced
|
|
890
|
+
* (Requirement 1.6).
|
|
891
|
+
*
|
|
892
|
+
* @typeParam T - Optional per-application custom metadata map.
|
|
893
|
+
*/
|
|
894
|
+
export function createStorage(config) {
|
|
895
|
+
const driver = resolveDriver(config);
|
|
896
|
+
return new StorageFacade(driver, config);
|
|
897
|
+
}
|
|
898
|
+
//# sourceMappingURL=facade.js.map
|