@warlock.js/core 5.20.0 → 5.22.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/CHANGELOG.md +1130 -1086
- package/esm/benchmark/benchmark.mjs +34 -25
- package/esm/benchmark/benchmark.mjs.map +1 -1
- package/esm/benchmark/profiler.d.mts.map +1 -1
- package/esm/benchmark/profiler.mjs +8 -3
- package/esm/benchmark/profiler.mjs.map +1 -1
- package/esm/bootstrap.d.mts.map +1 -1
- package/esm/bootstrap.mjs +3 -0
- package/esm/bootstrap.mjs.map +1 -1
- package/esm/cache/database-cache-driver.d.mts +2 -2
- package/esm/cache/database-cache-driver.d.mts.map +1 -1
- package/esm/cache/database-cache-driver.mjs +3 -2
- package/esm/cache/database-cache-driver.mjs.map +1 -1
- package/esm/cli/cli-commands.manager.mjs +16 -14
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/add.command.mjs +1 -1
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +3 -2
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs +20 -0
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs +4 -2
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/controller.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/controller.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/migration.generator.mjs +22 -6
- package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/model.generator.mjs +22 -7
- package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs +8 -6
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/repository.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/repository.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/resource.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/resource.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/service.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/service.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/templates/stubs.mjs +7 -7
- package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
- package/esm/cli/commands/generate/utils/writer.mjs +25 -1
- package/esm/cli/commands/generate/utils/writer.mjs.map +1 -1
- package/esm/cli/commands/routes/routes-command.action.mjs +5 -1
- package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -1
- package/esm/cli/commands/routes/with-console-on-stderr.mjs +24 -0
- package/esm/cli/commands/routes/with-console-on-stderr.mjs.map +1 -0
- package/esm/cli/commands/routes.command.mjs +3 -4
- package/esm/cli/commands/routes.command.mjs.map +1 -1
- package/esm/cli/commands/start-production.command.mjs +5 -5
- package/esm/cli/commands/start-production.command.mjs.map +1 -1
- package/esm/cli/commands/storage-put.action.mjs +2 -1
- package/esm/cli/commands/storage-put.action.mjs.map +1 -1
- package/esm/cli/commands/update.command.mjs +1 -0
- package/esm/cli/commands/update.command.mjs.map +1 -1
- package/esm/cli/parse-cli-args.mjs +13 -1
- package/esm/cli/parse-cli-args.mjs.map +1 -1
- package/esm/commands/cli-command.d.mts.map +1 -1
- package/esm/commands/cli-command.mjs +4 -2
- package/esm/commands/cli-command.mjs.map +1 -1
- package/esm/commands/index.mjs +3 -0
- package/esm/config/config-key-from-path.mjs +20 -0
- package/esm/config/config-key-from-path.mjs.map +1 -0
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +2 -3
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/connectors/connectors-manager.d.mts +4 -0
- package/esm/connectors/connectors-manager.d.mts.map +1 -1
- package/esm/connectors/connectors-manager.mjs +30 -2
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/socket-connector.d.mts +29 -0
- package/esm/connectors/socket-connector.d.mts.map +1 -1
- package/esm/connectors/socket-connector.mjs +73 -9
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/connectors/types.d.mts +1 -1
- package/esm/connectors/types.mjs.map +1 -1
- package/esm/database/models/database-log/database-log.d.mts +47 -24
- package/esm/database/models/database-log/database-log.d.mts.map +1 -1
- package/esm/database/models/database-log/database-log.mjs +36 -4
- package/esm/database/models/database-log/database-log.mjs.map +1 -1
- package/esm/database/seed-command-action.mjs +10 -2
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +11 -4
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +26 -6
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs +2 -2
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +3 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +64 -5
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/health-checker/files-healthcare.manager.mjs +31 -9
- package/esm/dev-server/health-checker/files-healthcare.manager.mjs.map +1 -1
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs +11 -7
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs.map +1 -1
- package/esm/dev-server/loader/load-hook.mjs +1 -1
- package/esm/dev-server/loader/register-loader.mjs +1 -1
- package/esm/dev-server/manifest-manager.d.mts.map +1 -1
- package/esm/dev-server/manifest-manager.mjs +12 -6
- package/esm/dev-server/manifest-manager.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs +9 -0
- package/esm/dev-server/parse-imports.mjs.map +1 -1
- package/esm/dev-server/register-dev-shortcuts.mjs +2 -1
- package/esm/dev-server/register-dev-shortcuts.mjs.map +1 -1
- package/esm/dev-server/shortcuts.mjs +2 -2
- package/esm/dev-server/shortcuts.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +18 -0
- package/esm/dev-server/special-file-patterns.mjs.map +1 -0
- package/esm/dev-server/special-files-collector.d.mts.map +1 -1
- package/esm/dev-server/special-files-collector.mjs +2 -5
- package/esm/dev-server/special-files-collector.mjs.map +1 -1
- package/esm/dev-server/start-development-server.mjs +1 -2
- package/esm/dev-server/start-development-server.mjs.map +1 -1
- package/esm/dev-server/supervisor.mjs +23 -3
- package/esm/dev-server/supervisor.mjs.map +1 -1
- package/esm/dev-server/tsconfig-manager.mjs +22 -12
- package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +10 -9
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/encryption/encrypt.d.mts.map +1 -1
- package/esm/encryption/encrypt.mjs +16 -5
- package/esm/encryption/encrypt.mjs.map +1 -1
- package/esm/encryption/hash.d.mts.map +1 -1
- package/esm/encryption/hash.mjs +2 -0
- package/esm/encryption/hash.mjs.map +1 -1
- package/esm/encryption/password.d.mts.map +1 -1
- package/esm/encryption/password.mjs +3 -1
- package/esm/encryption/password.mjs.map +1 -1
- package/esm/generations/features/index.mjs +1 -1
- package/esm/generations/features/mysql.feature.mjs +9 -2
- package/esm/generations/features/mysql.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +2 -1
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs +4 -2
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/http/csrf-origin-policy.d.mts.map +1 -1
- package/esm/http/csrf-origin-policy.mjs +5 -2
- package/esm/http/csrf-origin-policy.mjs.map +1 -1
- package/esm/http/flush-pending-cookies.mjs +52 -0
- package/esm/http/flush-pending-cookies.mjs.map +1 -0
- package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs +4 -2
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +14 -3
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts.map +1 -1
- package/esm/http/middleware/utils/cidr-match.mjs +11 -2
- package/esm/http/middleware/utils/cidr-match.mjs.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
- package/esm/http/plugins.d.mts.map +1 -1
- package/esm/http/plugins.mjs +10 -1
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/request.d.mts +12 -1
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +35 -7
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +16 -0
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +75 -37
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/types.d.mts +24 -0
- package/esm/http/types.d.mts.map +1 -1
- package/esm/http/uploads/uploaded-file.controller.mjs +1 -1
- package/esm/http/uploads-config.mjs +2 -1
- package/esm/http/uploads-config.mjs.map +1 -1
- package/esm/http/uploads-types.d.mts +3 -3
- package/esm/image/image.d.mts +1 -1
- package/esm/image/image.d.mts.map +1 -1
- package/esm/image/image.mjs +25 -15
- package/esm/image/image.mjs.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/index.mjs +5 -3
- package/esm/logger/logger.mjs +6 -1
- package/esm/logger/logger.mjs.map +1 -1
- package/esm/mail/config.d.mts.map +1 -1
- package/esm/mail/config.mjs +31 -21
- package/esm/mail/config.mjs.map +1 -1
- package/esm/mail/react-mail.mjs +6 -3
- package/esm/mail/react-mail.mjs.map +1 -1
- package/esm/mail/send-mail.d.mts.map +1 -1
- package/esm/mail/send-mail.mjs +40 -19
- package/esm/mail/send-mail.mjs.map +1 -1
- package/esm/production/build-contributions.mjs +3 -0
- package/esm/production/build-contributions.mjs.map +1 -1
- package/esm/production/esbuild-plugins.mjs +3 -2
- package/esm/production/esbuild-plugins.mjs.map +1 -1
- package/esm/production/production-builder.mjs +32 -36
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs +12 -5
- package/esm/production/production-supervisor.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.d.mts +3 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.d.mts.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.mjs +17 -5
- package/esm/repositories/adapters/cascade/cascade-adapter.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts +3 -3
- package/esm/repositories/adapters/cascade/cascade-query-builder.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/filter-applicator.mjs +88 -61
- package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/index.d.mts +1 -1
- package/esm/repositories/adapters/cascade/index.mjs +1 -1
- package/esm/repositories/contracts/query-builder.contract.d.mts +3 -3
- package/esm/repositories/contracts/types.d.mts +5 -0
- package/esm/repositories/contracts/types.d.mts.map +1 -1
- package/esm/repositories/index.d.mts +1 -1
- package/esm/repositories/index.mjs +1 -1
- package/esm/repositories/repository.manager.d.mts +65 -3
- package/esm/repositories/repository.manager.d.mts.map +1 -1
- package/esm/repositories/repository.manager.mjs +146 -57
- package/esm/repositories/repository.manager.mjs.map +1 -1
- package/esm/resource/resource-field-builder.d.mts.map +1 -1
- package/esm/resource/resource-field-builder.mjs +1 -0
- package/esm/resource/resource-field-builder.mjs.map +1 -1
- package/esm/resource/resource.d.mts +10 -3
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs +19 -5
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/restful/restful.d.mts +12 -5
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/restful/restful.mjs +47 -16
- package/esm/restful/restful.mjs.map +1 -1
- package/esm/router/build-route-rate-limit.mjs +22 -0
- package/esm/router/build-route-rate-limit.mjs.map +1 -0
- package/esm/router/dev-dispatch.mjs +60 -0
- package/esm/router/dev-dispatch.mjs.map +1 -0
- package/esm/router/route-builder.mjs +4 -1
- package/esm/router/route-builder.mjs.map +1 -1
- package/esm/router/route-registry.d.mts.map +1 -1
- package/esm/router/route-registry.mjs +2 -2
- package/esm/router/route-registry.mjs.map +1 -1
- package/esm/router/router.d.mts +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +57 -39
- package/esm/router/router.mjs.map +1 -1
- package/esm/socket/types.d.mts +9 -2
- package/esm/socket/types.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.d.mts +17 -0
- package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.mjs +53 -17
- package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
- package/esm/storage/drivers/local-driver.d.mts +7 -5
- package/esm/storage/drivers/local-driver.d.mts.map +1 -1
- package/esm/storage/drivers/local-driver.mjs +36 -31
- package/esm/storage/drivers/local-driver.mjs.map +1 -1
- package/esm/storage/index.d.mts +2 -1
- package/esm/storage/index.mjs +2 -1
- package/esm/storage/scoped-storage.d.mts +13 -1
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs +18 -8
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts +0 -5
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +7 -16
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/types.d.mts +22 -4
- package/esm/storage/types.d.mts.map +1 -1
- package/esm/storage/utils/classify-ip-address.d.mts +13 -0
- package/esm/storage/utils/classify-ip-address.d.mts.map +1 -0
- package/esm/storage/utils/classify-ip-address.mjs +98 -0
- package/esm/storage/utils/classify-ip-address.mjs.map +1 -0
- package/esm/storage/utils/safe-fetch.d.mts +3 -12
- package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs +3 -48
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/updater/update-warlock-packages.mjs +11 -1
- package/esm/updater/update-warlock-packages.mjs.map +1 -1
- package/esm/use-cases/use-case-broadcast.mjs +7 -1
- package/esm/use-cases/use-case-broadcast.mjs.map +1 -1
- package/esm/use-cases/use-cases-registry.mjs +21 -2
- package/esm/use-cases/use-cases-registry.mjs.map +1 -1
- package/esm/utils/database-log.mjs +0 -3
- package/esm/utils/database-log.mjs.map +1 -1
- package/esm/utils/queue.d.mts +6 -4
- package/esm/utils/queue.d.mts.map +1 -1
- package/esm/utils/queue.mjs +20 -9
- package/esm/utils/queue.mjs.map +1 -1
- package/esm/validation/database/exists-except-current-id.mjs +6 -2
- package/esm/validation/database/exists-except-current-id.mjs.map +1 -1
- package/esm/validation/database/unique-except-current-id.mjs +6 -2
- package/esm/validation/database/unique-except-current-id.mjs.map +1 -1
- package/esm/validation/init.mjs +6 -2
- package/esm/validation/init.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +3 -2
- package/esm/validation/validateAll.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +14 -1
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.mjs +3 -1
- package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
- package/esm/warlock-config/warn-deprecated-config.mjs +25 -0
- package/esm/warlock-config/warn-deprecated-config.mjs.map +1 -0
- package/package.json +16 -16
- package/esm/_virtual/_rolldown/runtime.mjs +0 -7
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StorageCapabilityError } from "../utils/storage-capability-error.mjs";
|
|
1
2
|
import { storageDriverContext } from "../context/storage-driver-context.mjs";
|
|
2
3
|
import { getMimeType } from "../utils/mime.mjs";
|
|
3
4
|
import crypto from "crypto";
|
|
@@ -101,16 +102,37 @@ function loadS3Sync() {
|
|
|
101
102
|
* - `url()`: Returns the public URL for a file (provider-specific format)
|
|
102
103
|
*/
|
|
103
104
|
var CloudDriver = class {
|
|
105
|
+
/**
|
|
106
|
+
* How object visibility is applied: per-object ACLs (default), bucket
|
|
107
|
+
* policy (no per-object ACL is sent), or unsupported ("none").
|
|
108
|
+
* Buckets with ACLs disabled (S3 BucketOwnerEnforced, R2) need
|
|
109
|
+
* "policy" or "none".
|
|
110
|
+
*/
|
|
111
|
+
get visibilityMode() {
|
|
112
|
+
return this.options.visibilityMode ?? "acl";
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the ACL to send on a put for the requested visibility.
|
|
116
|
+
*
|
|
117
|
+
* @throws {StorageCapabilityError} When public is requested but the driver has no visibility support
|
|
118
|
+
*/
|
|
119
|
+
resolveAcl(visibility) {
|
|
120
|
+
if (visibility !== "public") return void 0;
|
|
121
|
+
if (this.visibilityMode === "none") throw new StorageCapabilityError("public visibility", this.name);
|
|
122
|
+
return this.visibilityMode === "acl" ? "public-read" : void 0;
|
|
123
|
+
}
|
|
104
124
|
constructor(options) {
|
|
105
125
|
this.options = options;
|
|
106
126
|
if (isModuleExists === null) loadS3Sync();
|
|
107
127
|
if (!isModuleExists) throw new Error(S3_INSTALL_INSTRUCTIONS);
|
|
128
|
+
const { bucket: _bucket, accessKeyId, secretAccessKey, endpoint: _endpoint, prefix: _prefix, urlPrefix: _urlPrefix, retry: _retry, accountId: _accountId, publicDomain: _publicDomain, driver: _driver, visibilityMode: _visibilityMode, ...clientOptions } = this.options;
|
|
108
129
|
this.client = new S3Client.S3Client({
|
|
130
|
+
...clientOptions,
|
|
109
131
|
region: this.options.region,
|
|
110
|
-
credentials: {
|
|
111
|
-
accessKeyId
|
|
112
|
-
secretAccessKey
|
|
113
|
-
},
|
|
132
|
+
...accessKeyId && secretAccessKey && { credentials: {
|
|
133
|
+
accessKeyId,
|
|
134
|
+
secretAccessKey
|
|
135
|
+
} },
|
|
114
136
|
...this.getEndpoint() && { endpoint: this.getEndpoint() }
|
|
115
137
|
});
|
|
116
138
|
this.retryConfig = {
|
|
@@ -164,16 +186,17 @@ var CloudDriver = class {
|
|
|
164
186
|
async withRetry(operation, operationName = "operation") {
|
|
165
187
|
const { maxRetries, initialDelayMs, backoffMultiplier, maxDelayMs } = this.retryConfig;
|
|
166
188
|
let lastError;
|
|
167
|
-
|
|
189
|
+
const attempts = maxRetries + 1;
|
|
190
|
+
for (let attempt = 0; attempt < attempts; attempt++) try {
|
|
168
191
|
return await operation();
|
|
169
192
|
} catch (error) {
|
|
170
193
|
lastError = error;
|
|
171
|
-
if (attempt ===
|
|
194
|
+
if (attempt === attempts - 1) break;
|
|
172
195
|
if (!this.isRetryableError(error)) throw error;
|
|
173
196
|
const delayMs = Math.min(initialDelayMs * Math.pow(backoffMultiplier, attempt), maxDelayMs);
|
|
174
197
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
175
198
|
}
|
|
176
|
-
throw
|
|
199
|
+
throw lastError;
|
|
177
200
|
}
|
|
178
201
|
/**
|
|
179
202
|
* Check if an error is retryable
|
|
@@ -220,7 +243,7 @@ var CloudDriver = class {
|
|
|
220
243
|
CacheControl: options?.cacheControl,
|
|
221
244
|
ContentDisposition: options?.contentDisposition,
|
|
222
245
|
Metadata: options?.metadata,
|
|
223
|
-
ACL: options?.visibility
|
|
246
|
+
ACL: this.resolveAcl(options?.visibility)
|
|
224
247
|
});
|
|
225
248
|
const result = await this.client.send(command);
|
|
226
249
|
return {
|
|
@@ -260,7 +283,7 @@ var CloudDriver = class {
|
|
|
260
283
|
CacheControl: options?.cacheControl,
|
|
261
284
|
ContentDisposition: options?.contentDisposition,
|
|
262
285
|
Metadata: options?.metadata,
|
|
263
|
-
ACL: options?.visibility
|
|
286
|
+
ACL: this.resolveAcl(options?.visibility),
|
|
264
287
|
IfNoneMatch: "*"
|
|
265
288
|
});
|
|
266
289
|
let result;
|
|
@@ -290,7 +313,7 @@ var CloudDriver = class {
|
|
|
290
313
|
* Uses S3 multipart upload for efficient streaming
|
|
291
314
|
*/
|
|
292
315
|
async putStream(stream, location, options) {
|
|
293
|
-
|
|
316
|
+
{
|
|
294
317
|
const { Upload } = S3Storage;
|
|
295
318
|
location = this.applyPrefix(location);
|
|
296
319
|
const mimeType = options?.mimeType || this.guessMimeType(location);
|
|
@@ -304,7 +327,7 @@ var CloudDriver = class {
|
|
|
304
327
|
CacheControl: options?.cacheControl,
|
|
305
328
|
ContentDisposition: options?.contentDisposition,
|
|
306
329
|
Metadata: options?.metadata,
|
|
307
|
-
ACL: options?.visibility
|
|
330
|
+
ACL: this.resolveAcl(options?.visibility)
|
|
308
331
|
}
|
|
309
332
|
}).done();
|
|
310
333
|
const info = await this.metadata(location);
|
|
@@ -320,7 +343,7 @@ var CloudDriver = class {
|
|
|
320
343
|
etag: result.ETag,
|
|
321
344
|
versionId: result.VersionId
|
|
322
345
|
};
|
|
323
|
-
}
|
|
346
|
+
}
|
|
324
347
|
}
|
|
325
348
|
/**
|
|
326
349
|
* Get file contents as Buffer
|
|
@@ -443,8 +466,10 @@ var CloudDriver = class {
|
|
|
443
466
|
});
|
|
444
467
|
await this.client.send(command);
|
|
445
468
|
return true;
|
|
446
|
-
} catch {
|
|
447
|
-
|
|
469
|
+
} catch (error) {
|
|
470
|
+
const { name, $metadata } = error ?? {};
|
|
471
|
+
if (name === "NotFound" || name === "NoSuchKey" || $metadata?.httpStatusCode === 404) return false;
|
|
472
|
+
throw error;
|
|
448
473
|
}
|
|
449
474
|
}
|
|
450
475
|
/**
|
|
@@ -517,17 +542,26 @@ var CloudDriver = class {
|
|
|
517
542
|
return (await this.metadata(location)).size;
|
|
518
543
|
}
|
|
519
544
|
/**
|
|
545
|
+
* Build a URL-encoded CopySource ("bucket/key") as the SDK requires
|
|
546
|
+
*/
|
|
547
|
+
copySource(key) {
|
|
548
|
+
const encodedKey = key.split("/").map(encodeURIComponent).join("/");
|
|
549
|
+
return `${this.options.bucket}/${encodedKey}`;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
520
552
|
* Copy file to a new location
|
|
521
553
|
*/
|
|
522
554
|
async copy(from, to) {
|
|
523
555
|
return this.withRetry(async () => {
|
|
524
556
|
const { CopyObjectCommand, HeadObjectCommand } = S3Client;
|
|
557
|
+
const visibility = await this.getVisibility(from).catch(() => "private");
|
|
525
558
|
from = this.applyPrefix(from);
|
|
526
559
|
to = this.applyPrefix(to);
|
|
527
560
|
const command = new CopyObjectCommand({
|
|
528
561
|
Bucket: this.options.bucket,
|
|
529
|
-
CopySource:
|
|
530
|
-
Key: to
|
|
562
|
+
CopySource: this.copySource(from),
|
|
563
|
+
Key: to,
|
|
564
|
+
...this.visibilityMode === "acl" && visibility === "public" && { ACL: "public-read" }
|
|
531
565
|
});
|
|
532
566
|
const result = await this.client.send(command);
|
|
533
567
|
const headCommand = new HeadObjectCommand({
|
|
@@ -564,6 +598,7 @@ var CloudDriver = class {
|
|
|
564
598
|
return this.withRetry(async () => {
|
|
565
599
|
const { ListObjectsV2Command } = S3Client;
|
|
566
600
|
directory = this.applyPrefix(directory);
|
|
601
|
+
if (directory && directory !== "/" && !directory.endsWith("/")) directory = `${directory}/`;
|
|
567
602
|
const command = new ListObjectsV2Command({
|
|
568
603
|
Bucket: this.options.bucket,
|
|
569
604
|
Prefix: directory,
|
|
@@ -618,7 +653,7 @@ var CloudDriver = class {
|
|
|
618
653
|
location = this.applyPrefix(location);
|
|
619
654
|
const command = new CopyObjectCommand({
|
|
620
655
|
Bucket: this.options.bucket,
|
|
621
|
-
CopySource:
|
|
656
|
+
CopySource: this.copySource(location),
|
|
622
657
|
Key: location,
|
|
623
658
|
StorageClass: storageClass,
|
|
624
659
|
MetadataDirective: "COPY"
|
|
@@ -630,6 +665,7 @@ var CloudDriver = class {
|
|
|
630
665
|
* Set file visibility (public or private)
|
|
631
666
|
*/
|
|
632
667
|
async setVisibility(location, visibility) {
|
|
668
|
+
if (this.visibilityMode !== "acl") throw new StorageCapabilityError("setVisibility", this.name);
|
|
633
669
|
return this.withRetry(async () => {
|
|
634
670
|
const { PutObjectAclCommand } = S3Client;
|
|
635
671
|
location = this.applyPrefix(location);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloud-driver.mjs","names":[],"sources":["../../../../../../../../core/src/storage/drivers/cloud-driver.ts"],"sourcesContent":["import crypto from \"crypto\";\nimport { createRequire } from \"module\";\nimport type { Readable } from \"stream\";\nimport { storageDriverContext } from \"../context/storage-driver-context\";\nimport type {\n CloudStorageDriverContract,\n CloudStorageDriverOptions,\n CloudStorageFileData,\n DeleteManyResult,\n FileVisibility,\n ListOptions,\n PresignedOptions,\n PresignedUploadOptions,\n PutOptions,\n StorageDriverType,\n StorageFileInfo,\n} from \"../types\";\nimport { getMimeType } from \"../utils/mime\";\n\n// ============================================================\n// Lazy-loaded S3 SDK Types\n// ============================================================\n\n/**\n * Cached S3 SDK modules (loaded once, reused)\n */\nlet S3Client: typeof import(\"@aws-sdk/client-s3\");\nlet S3Storage: typeof import(\"@aws-sdk/lib-storage\");\nlet S3Presigner: typeof import(\"@aws-sdk/s3-request-presigner\");\n\nlet isModuleExists: boolean | null = null;\n\n/**\n * Installation instructions for S3 SDK packages\n */\nconst S3_INSTALL_INSTRUCTIONS = `\nCloud storage requires the AWS S3 SDK packages.\nInstall them with:\n\n warlock add s3\n\n(raw packages: @aws-sdk/client-s3, @aws-sdk/lib-storage, @aws-sdk/s3-request-presigner)\n`.trim();\n\n/**\n * Atomic initialization promise to handle concurrent calls to loadS3\n */\nlet initializationPromise: Promise<void> | null = null;\n\n/**\n * Load S3 modules lazily\n *\n * @example\n * await loadS3();\n * if (isModuleExists) {\n * // Safe to use S3Client, S3Storage, S3Presigner\n * }\n */\nexport async function loadS3(): Promise<void> {\n // If already successfully loaded, no need to do anything\n if (isModuleExists === true) return;\n\n // If currently loading, return the existing promise so the caller waits\n if (initializationPromise) return initializationPromise;\n\n initializationPromise = (async () => {\n try {\n // Use Promise.all to load modules in parallel for efficiency\n const [client, storage, presigner] = await Promise.all([\n import(\"@aws-sdk/client-s3\"),\n import(\"@aws-sdk/lib-storage\"),\n import(\"@aws-sdk/s3-request-presigner\"),\n ]);\n\n S3Client = client;\n S3Storage = storage;\n S3Presigner = presigner;\n\n isModuleExists = true;\n } catch {\n // Mark as failed if any import fails (usually means packages aren't installed)\n isModuleExists = false;\n }\n })();\n\n return initializationPromise;\n}\n\nloadS3();\n\n/**\n * Synchronous fallback loader.\n *\n * `loadS3()` above is async and fire-and-forget at module load, so on the\n * very first *synchronous* `CloudDriver` construction the dynamic `import()`\n * microtask has not yet resolved — `isModuleExists` is still `null` even when\n * the SDK is genuinely installed. The constructor's install-check would then\n * throw the misleading \"SDK not installed\" error.\n *\n * This probes the modules synchronously via `require` (the AWS SDK ships\n * CJS-compatible builds) so a present SDK resolves the install-check\n * immediately, without waiting on the async path. Returns `true` when all\n * three modules loaded, `false` otherwise. Only invoked while\n * `isModuleExists` is still indeterminate.\n */\nfunction loadS3Sync(): boolean {\n if (isModuleExists !== null) {\n return isModuleExists;\n }\n\n try {\n // `createRequire` gives an ESM-safe synchronous `require` regardless of\n // whether this module runs as CJS or ESM at runtime.\n const req = createRequire(import.meta.url);\n S3Client = req(\"@aws-sdk/client-s3\");\n S3Storage = req(\"@aws-sdk/lib-storage\");\n S3Presigner = req(\"@aws-sdk/s3-request-presigner\");\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n\n return isModuleExists;\n}\n\n// ============================================================\n// CloudDriver Base Class\n// ============================================================\n\n/**\n * Base abstract class for all S3-compatible cloud storage drivers\n *\n * This class contains all shared logic for S3-compatible storage services\n * including AWS S3, Cloudflare R2, DigitalOcean Spaces, and others.\n *\n * **Important:** S3 SDK packages are lazy-loaded on first use.\n * Users must install them separately:\n * ```\n * npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner\n * ```\n *\n * Subclasses must implement:\n * - `name`: Driver identifier (e.g., \"s3\", \"r2\", \"spaces\")\n * - `url()`: Returns the public URL for a file (provider-specific format)\n */\nexport abstract class CloudDriver<\n TOptions extends Partial<CloudStorageDriverOptions> = CloudStorageDriverOptions,\n> implements CloudStorageDriverContract {\n /**\n * S3-compatible client (lazy-loaded)\n */\n protected client!: InstanceType<typeof import(\"@aws-sdk/client-s3\").S3Client>;\n\n /**\n * Retry configuration\n */\n protected retryConfig: {\n maxRetries: number;\n initialDelayMs: number;\n maxDelayMs: number;\n backoffMultiplier: number;\n };\n\n public constructor(public options: TOptions) {\n // The async loadS3() may not have resolved yet on the first synchronous\n // construction. When the flag is still indeterminate, probe synchronously\n // so a genuinely-installed SDK never triggers the misleading throw below.\n if (isModuleExists === null) {\n loadS3Sync();\n }\n\n if (!isModuleExists) {\n throw new Error(S3_INSTALL_INSTRUCTIONS);\n }\n\n this.client = new S3Client.S3Client({\n region: this.options.region!,\n credentials: {\n accessKeyId: this.options.accessKeyId!,\n secretAccessKey: this.options.secretAccessKey!,\n },\n ...(this.getEndpoint() && { endpoint: this.getEndpoint() }),\n });\n\n // Initialize retry configuration\n this.retryConfig = {\n maxRetries: this.options.retry?.maxRetries ?? 3,\n initialDelayMs: this.options.retry?.initialDelayMs ?? 1000,\n maxDelayMs: this.options.retry?.maxDelayMs ?? 10000,\n backoffMultiplier: this.options.retry?.backoffMultiplier ?? 2,\n };\n }\n\n /**\n * Driver name identifier\n */\n public abstract readonly name: StorageDriverType;\n\n /**\n * Get public URL for file\n * Must be implemented by subclasses with provider-specific format\n */\n public abstract url(location: string): string;\n\n /**\n * Get endpoint URL\n * Can be overridden by subclasses for provider-specific endpoints\n */\n protected getEndpoint(): string | undefined {\n return this.options.endpoint;\n }\n\n // ============================================================\n // Helper Methods\n // ============================================================\n\n /**\n * Apply prefix to location path\n *\n * Priority: context prefix > driver options prefix > no prefix\n * This allows multi-tenant scenarios where context overrides driver config.\n *\n * @param location - Original location path\n * @returns Location with prefix applied if one exists\n */\n public applyPrefix(location: string): string {\n // Check context prefix first (highest priority)\n const contextPrefix = storageDriverContext.getPrefix();\n const prefix = contextPrefix || this.options.prefix;\n\n if (!prefix) {\n return location;\n }\n\n const cleanPrefix = prefix.replace(/\\/+$/, \"\");\n const cleanLocation = location.replace(/^\\/+/, \"\");\n\n // Avoid double-prefixing\n if (cleanLocation.startsWith(cleanPrefix + \"/\") || cleanLocation === cleanPrefix) {\n return cleanLocation;\n }\n\n return `${cleanPrefix}/${cleanLocation}`;\n }\n\n /**\n * Normalize storage path (remove double slashes, sanitize)\n * @internal\n */\n protected normalizePath(path: string): string {\n return path\n .replace(/\\/+/g, \"/\") // Remove multiple slashes\n .replace(/^\\//, \"\") // Remove leading slash\n .trim();\n }\n\n /**\n * Execute an operation with retry logic\n *\n * Retries on transient errors with exponential backoff.\n *\n * @param operation - Async operation to execute\n * @param operationName - Name for logging\n * @returns Result of the operation\n * @internal\n */\n protected async withRetry<T>(\n operation: () => Promise<T>,\n operationName: string = \"operation\",\n ): Promise<T> {\n const { maxRetries, initialDelayMs, backoffMultiplier, maxDelayMs } = this.retryConfig;\n\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n return await operation();\n } catch (error) {\n lastError = error as Error;\n\n // Don't retry on the last attempt\n if (attempt === maxRetries - 1) {\n break;\n }\n\n // Check if error is retryable\n if (!this.isRetryableError(error)) {\n throw error; // Non-retryable error, fail immediately\n }\n\n // Calculate delay with exponential backoff\n const delayMs = Math.min(initialDelayMs * Math.pow(backoffMultiplier, attempt), maxDelayMs);\n\n // Wait before retrying\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n }\n\n throw new Error(`${operationName} failed after ${maxRetries} attempts: ${lastError?.message}`);\n }\n\n /**\n * Check if an error is retryable\n *\n * Retries on:\n * - Network errors\n * - 5xx server errors\n * - Rate limiting (429)\n * - Timeout errors\n *\n * Does NOT retry on:\n * - 4xx client errors (except 429)\n * - Authentication errors\n * - Not found errors\n *\n * @param error - Error to check\n * @returns true if error is retryable\n * @internal\n */\n protected isRetryableError(error: any): boolean {\n // Network errors\n if (error.code === \"ECONNRESET\" || error.code === \"ETIMEDOUT\" || error.code === \"ENOTFOUND\") {\n return true;\n }\n\n // S3 SDK errors\n if (error.name === \"NetworkingError\" || error.name === \"TimeoutError\") {\n return true;\n }\n\n // HTTP status codes\n const statusCode = error.$metadata?.httpStatusCode || error.statusCode;\n if (statusCode) {\n // 5xx server errors are retryable\n if (statusCode >= 500 && statusCode < 600) {\n return true;\n }\n\n // 429 Too Many Requests is retryable\n if (statusCode === 429) {\n return true;\n }\n }\n\n return false;\n }\n\n // ============================================================\n // Core File Operations\n // ============================================================\n\n /**\n * Put file to cloud storage\n */\n public async put(\n file: Buffer,\n location: string,\n options?: PutOptions,\n ): Promise<CloudStorageFileData> {\n return this.withRetry(async () => {\n const { PutObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const hash = this.calculateHash(file);\n const mimeType = options?.mimeType || this.guessMimeType(location);\n\n const command = new PutObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n Body: file,\n ContentType: mimeType,\n CacheControl: options?.cacheControl,\n ContentDisposition: options?.contentDisposition,\n Metadata: options?.metadata,\n ACL: options?.visibility === \"public\" ? \"public-read\" : undefined,\n });\n\n const result = await this.client.send(command);\n\n return {\n path: location,\n url: this.url(location),\n size: file.length,\n hash,\n mimeType,\n driver: this.name,\n bucket: this.options.bucket!,\n region: this.options.region!,\n etag: result.ETag,\n versionId: result.VersionId,\n };\n }, \"put\");\n }\n\n /**\n * Atomically put a file only if the key does not exist (S3 conditional\n * write, `If-None-Match: *`).\n *\n * HTTP 412 (PreconditionFailed) and 409 (ConditionalRequestConflict, a\n * concurrent conditional write in flight) mean \"already exists\" and return\n * `null`. Every other error throws.\n */\n public async putIfAbsent(\n file: Buffer | string,\n location: string,\n options?: PutOptions,\n ): Promise<CloudStorageFileData | null> {\n const body = typeof file === \"string\" ? Buffer.from(file) : file;\n\n return this.withRetry(async () => {\n const { PutObjectCommand } = S3Client;\n\n location = this.applyPrefix(location);\n\n const hash = this.calculateHash(body);\n const mimeType = options?.mimeType || this.guessMimeType(location);\n\n const command = new PutObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n Body: body,\n ContentType: mimeType,\n CacheControl: options?.cacheControl,\n ContentDisposition: options?.contentDisposition,\n Metadata: options?.metadata,\n ACL: options?.visibility === \"public\" ? \"public-read\" : undefined,\n IfNoneMatch: \"*\",\n });\n\n let result;\n\n try {\n result = await this.client.send(command);\n } catch (error: any) {\n const status = error?.$metadata?.httpStatusCode;\n\n if (\n status === 412 ||\n status === 409 ||\n error?.name === \"PreconditionFailed\" ||\n error?.name === \"ConditionalRequestConflict\"\n ) {\n return null;\n }\n\n throw error;\n }\n\n return {\n path: location,\n url: this.url(location),\n size: body.length,\n hash,\n mimeType,\n driver: this.name,\n bucket: this.options.bucket!,\n region: this.options.region!,\n etag: result.ETag,\n versionId: result.VersionId,\n };\n }, \"putIfAbsent\");\n }\n\n /**\n * Put file from a readable stream (for large files)\n * Uses S3 multipart upload for efficient streaming\n */\n public async putStream(\n stream: Readable,\n location: string,\n options?: PutOptions,\n ): Promise<CloudStorageFileData> {\n return this.withRetry(async () => {\n const { Upload } = S3Storage;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const mimeType = options?.mimeType || this.guessMimeType(location);\n\n const upload = new Upload({\n client: this.client,\n params: {\n Bucket: this.options.bucket,\n Key: location,\n Body: stream,\n ContentType: mimeType,\n CacheControl: options?.cacheControl,\n ContentDisposition: options?.contentDisposition,\n Metadata: options?.metadata,\n ACL: options?.visibility === \"public\" ? \"public-read\" : undefined,\n },\n });\n\n const result = await upload.done();\n\n // Get file info for size and hash\n const info = await this.metadata(location);\n\n return {\n path: location,\n url: this.url(location),\n size: info.size,\n hash: info.etag?.replace(/\"/g, \"\") || \"\",\n mimeType,\n driver: this.name,\n bucket: this.options.bucket!,\n region: this.options.region!,\n etag: result.ETag,\n versionId: result.VersionId,\n };\n }, \"putStream\");\n }\n\n /**\n * Get file contents as Buffer\n */\n public async get(location: string): Promise<Buffer> {\n return this.withRetry(async () => {\n const { GetObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new GetObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n const result = await this.client.send(command);\n\n if (!result.Body) {\n throw new Error(`File not found: ${location}`);\n }\n\n return Buffer.from(await result.Body.transformToByteArray());\n }, \"get\");\n }\n\n /**\n * Get file as a readable stream (for large files)\n */\n public async getStream(location: string): Promise<Readable> {\n return this.withRetry(async () => {\n const { GetObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new GetObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n const result = await this.client.send(command);\n\n if (!result.Body) {\n throw new Error(`File not found: ${location}`);\n }\n\n return result.Body as Readable;\n }, \"getStream\");\n }\n\n /**\n * Delete a file\n */\n public async delete(location: string): Promise<boolean> {\n return this.withRetry(async () => {\n const { DeleteObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new DeleteObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n await this.client.send(command);\n\n return true;\n }, \"delete\");\n }\n\n /**\n * Delete multiple files at once (uses batch delete for efficiency)\n */\n public async deleteMany(locations: string[]): Promise<DeleteManyResult[]> {\n if (locations.length === 0) {\n return [];\n }\n\n return this.withRetry(async () => {\n const { DeleteObjectsCommand } = S3Client;\n\n // Apply storage prefix to all locations\n const prefixedLocations = locations.map((loc) => this.applyPrefix(loc));\n\n const command = new DeleteObjectsCommand({\n Bucket: this.options.bucket,\n Delete: {\n Objects: prefixedLocations.map((Key) => ({ Key })),\n Quiet: false,\n },\n });\n\n const result = await this.client.send(command);\n const results: DeleteManyResult[] = [];\n\n // Process successful deletes\n for (const deleted of result.Deleted || []) {\n if (deleted.Key) {\n results.push({ location: deleted.Key, deleted: true });\n }\n }\n\n // Process errors\n for (const error of result.Errors || []) {\n if (error.Key) {\n results.push({\n location: error.Key,\n deleted: false,\n error: error.Message || \"Unknown error\",\n });\n }\n }\n\n return results;\n }, \"deleteMany\");\n }\n\n /**\n * Delete directory (recursively deletes all objects with matching prefix)\n *\n * S3/R2 doesn't have true directories - only key prefixes.\n * This method lists all objects with the prefix and deletes them in batches.\n *\n * @param directoryPath - Directory prefix to delete\n * @returns true when all objects are deleted\n */\n public async deleteDirectory(directoryPath: string): Promise<boolean> {\n const { ListObjectsV2Command, DeleteObjectsCommand } = S3Client;\n\n // Apply storage prefix\n directoryPath = this.applyPrefix(directoryPath);\n\n // Ensure directory path ends with / for proper prefix matching\n const prefix = directoryPath.endsWith(\"/\") ? directoryPath : `${directoryPath}/`;\n\n // Drive the loop with the ListObjectsV2 IsTruncated / ContinuationToken\n // contract rather than guessing termination from page size. Each page\n // carries its own ContinuationToken so we never re-list page one.\n let continuationToken: string | undefined;\n\n do {\n const listResult = await this.withRetry(\n () =>\n this.client.send(\n new ListObjectsV2Command({\n Bucket: this.options.bucket,\n Prefix: prefix,\n ContinuationToken: continuationToken,\n }),\n ),\n \"deleteDirectory:list\",\n );\n\n const keys = (listResult.Contents || [])\n .map((object) => object.Key)\n .filter((key): key is string => Boolean(key));\n\n if (keys.length > 0) {\n // Delete the raw keys directly — they already carry the full prefix\n // from the listing, so we bypass deleteMany's applyPrefix step.\n await this.withRetry(\n () =>\n this.client.send(\n new DeleteObjectsCommand({\n Bucket: this.options.bucket,\n Delete: {\n Objects: keys.map((Key) => ({ Key })),\n Quiet: true,\n },\n }),\n ),\n \"deleteDirectory:delete\",\n );\n }\n\n continuationToken = listResult.IsTruncated ? listResult.NextContinuationToken : undefined;\n } while (continuationToken);\n\n return true;\n }\n\n /**\n * Check if file exists\n */\n public async exists(location: string): Promise<boolean> {\n // No retry for exists - it should be fast and failure means non-existent\n try {\n const { HeadObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new HeadObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n await this.client.send(command);\n return true;\n } catch {\n return false;\n }\n }\n\n // ============================================================\n // URL Operations\n // ============================================================\n\n /**\n * Get a temporary presigned URL (alias for getPresignedUrl)\n */\n public async temporaryUrl(location: string, expiresIn = 3600): Promise<string> {\n return this.getPresignedUrl(location, { expiresIn });\n }\n\n /**\n * Get presigned URL for downloading\n */\n public async getPresignedUrl(location: string, options?: PresignedOptions): Promise<string> {\n return this.withRetry(async () => {\n const { GetObjectCommand } = S3Client;\n const { getSignedUrl } = S3Presigner;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new GetObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n return getSignedUrl(this.client, command, {\n expiresIn: options?.expiresIn || 3600,\n });\n }, \"getPresignedUrl\");\n }\n\n /**\n * Get presigned URL for uploading\n */\n public async getPresignedUploadUrl(\n location: string,\n options?: PresignedUploadOptions,\n ): Promise<string> {\n return this.withRetry(async () => {\n const { PutObjectCommand } = S3Client;\n const { getSignedUrl } = S3Presigner;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new PutObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n ContentType: options?.contentType,\n Metadata: options?.metadata,\n });\n\n return getSignedUrl(this.client, command, {\n expiresIn: options?.expiresIn || 3600,\n });\n }, \"getPresignedUploadUrl\");\n }\n\n // ============================================================\n // Metadata Operations\n // ============================================================\n\n /**\n * Get file info/metadata without downloading\n */\n public async metadata(location: string): Promise<StorageFileInfo> {\n return this.withRetry(async () => {\n const { HeadObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new HeadObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n const result = await this.client.send(command);\n const name = location.split(\"/\").pop() || \"\";\n\n return {\n path: location,\n name,\n size: result.ContentLength || 0,\n isDirectory: false,\n lastModified: result.LastModified,\n mimeType: result.ContentType || this.guessMimeType(location),\n etag: result.ETag,\n storageClass: result.StorageClass,\n };\n }, \"metadata\");\n }\n\n /**\n * Get file size in bytes (shortcut for metadata().size)\n */\n public async size(location: string): Promise<number> {\n const info = await this.metadata(location);\n return info.size;\n }\n\n // ============================================================\n // File Operations\n // ============================================================\n\n /**\n * Copy file to a new location\n */\n public async copy(from: string, to: string): Promise<CloudStorageFileData> {\n return this.withRetry(async () => {\n const { CopyObjectCommand, HeadObjectCommand } = S3Client;\n\n // Apply storage prefix to both paths\n from = this.applyPrefix(from);\n to = this.applyPrefix(to);\n\n const command = new CopyObjectCommand({\n Bucket: this.options.bucket,\n CopySource: `${this.options.bucket}/${from}`,\n Key: to,\n });\n\n const result = await this.client.send(command);\n\n // Get file metadata\n const headCommand = new HeadObjectCommand({\n Bucket: this.options.bucket,\n Key: to,\n });\n const headResult = await this.client.send(headCommand);\n\n return {\n path: to,\n url: this.url(to),\n size: headResult.ContentLength || 0,\n hash: headResult.ETag?.replace(/\"/g, \"\") || \"\",\n mimeType: headResult.ContentType || this.guessMimeType(to),\n driver: this.name,\n bucket: this.options.bucket!,\n region: this.options.region!,\n etag: result.CopyObjectResult?.ETag,\n versionId: result.VersionId,\n };\n }, \"copy\");\n }\n\n /**\n * Move file to a new location\n */\n public async move(from: string, to: string): Promise<CloudStorageFileData> {\n const file = await this.copy(from, to);\n await this.delete(from);\n return file;\n }\n\n /**\n * List files in a directory\n */\n public async list(directory: string, options?: ListOptions): Promise<StorageFileInfo[]> {\n return this.withRetry(async () => {\n const { ListObjectsV2Command } = S3Client;\n\n // Apply storage prefix\n directory = this.applyPrefix(directory);\n\n const command = new ListObjectsV2Command({\n Bucket: this.options.bucket,\n Prefix: directory,\n MaxKeys: options?.limit,\n ContinuationToken: options?.cursor,\n Delimiter: options?.recursive ? undefined : \"/\",\n });\n\n const result = await this.client.send(command);\n const files: StorageFileInfo[] = [];\n\n // Add files\n for (const object of result.Contents || []) {\n if (!object.Key) continue;\n\n files.push({\n path: object.Key,\n name: object.Key.split(\"/\").pop() || \"\",\n size: object.Size || 0,\n isDirectory: false,\n lastModified: object.LastModified,\n etag: object.ETag,\n storageClass: object.StorageClass,\n });\n }\n\n // Add directories\n for (const prefix of result.CommonPrefixes || []) {\n if (!prefix.Prefix) continue;\n\n files.push({\n path: prefix.Prefix,\n name: prefix.Prefix.split(\"/\").filter(Boolean).pop() || \"\",\n size: 0,\n isDirectory: true,\n });\n }\n\n return files;\n }, \"list\");\n }\n\n // ============================================================\n // Cloud-Specific Operations\n // ============================================================\n\n /**\n * Get bucket name\n */\n public getBucket(): string {\n return this.options.bucket!;\n }\n\n /**\n * Get region\n */\n public getRegion(): string {\n return this.options.region!;\n }\n\n /**\n * Set storage class (e.g., STANDARD, GLACIER, etc.)\n */\n public async setStorageClass(location: string, storageClass: string): Promise<void> {\n return this.withRetry(async () => {\n const { CopyObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new CopyObjectCommand({\n Bucket: this.options.bucket,\n CopySource: `${this.options.bucket}/${location}`,\n Key: location,\n StorageClass: storageClass as any,\n MetadataDirective: \"COPY\",\n });\n\n await this.client.send(command);\n }, \"setStorageClass\");\n }\n\n /**\n * Set file visibility (public or private)\n */\n public async setVisibility(location: string, visibility: FileVisibility): Promise<void> {\n return this.withRetry(async () => {\n const { PutObjectAclCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new PutObjectAclCommand({\n Bucket: this.options.bucket,\n Key: location,\n ACL: visibility === \"public\" ? \"public-read\" : \"private\",\n });\n\n await this.client.send(command);\n }, \"setVisibility\");\n }\n\n /**\n * Get file visibility\n */\n public async getVisibility(location: string): Promise<FileVisibility> {\n return this.withRetry(async () => {\n const { GetObjectAclCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new GetObjectAclCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n const result = await this.client.send(command);\n\n // Check if any grant allows public read\n const hasPublicRead = result.Grants?.some(\n (grant) =>\n grant.Grantee?.URI === \"http://acs.amazonaws.com/groups/global/AllUsers\" &&\n grant.Permission === \"READ\",\n );\n\n return hasPublicRead ? \"public\" : \"private\";\n }, \"getVisibility\");\n }\n\n // ============================================================\n // Utilities\n // ============================================================\n\n /**\n * Calculate SHA-256 hash\n */\n protected calculateHash(buffer: Buffer): string {\n return crypto.createHash(\"sha256\").update(new Uint8Array(buffer)).digest(\"hex\");\n }\n\n /**\n * Guess MIME type from file extension\n */\n protected guessMimeType(location: string): string {\n return getMimeType(location);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cloud-driver.mjs","names":[],"sources":["../../../../../../../../core/src/storage/drivers/cloud-driver.ts"],"sourcesContent":["import crypto from \"crypto\";\nimport { createRequire } from \"module\";\nimport type { Readable } from \"stream\";\nimport { storageDriverContext } from \"../context/storage-driver-context\";\nimport type {\n CloudStorageDriverContract,\n CloudStorageDriverOptions,\n CloudStorageFileData,\n DeleteManyResult,\n FileVisibility,\n ListOptions,\n PresignedOptions,\n PresignedUploadOptions,\n PutOptions,\n StorageDriverType,\n StorageFileInfo,\n} from \"../types\";\nimport { getMimeType } from \"../utils/mime\";\nimport { StorageCapabilityError } from \"../utils/storage-capability-error\";\n\n// ============================================================\n// Lazy-loaded S3 SDK Types\n// ============================================================\n\n/**\n * Cached S3 SDK modules (loaded once, reused)\n */\nlet S3Client: typeof import(\"@aws-sdk/client-s3\");\nlet S3Storage: typeof import(\"@aws-sdk/lib-storage\");\nlet S3Presigner: typeof import(\"@aws-sdk/s3-request-presigner\");\n\nlet isModuleExists: boolean | null = null;\n\n/**\n * Installation instructions for S3 SDK packages\n */\nconst S3_INSTALL_INSTRUCTIONS = `\nCloud storage requires the AWS S3 SDK packages.\nInstall them with:\n\n warlock add s3\n\n(raw packages: @aws-sdk/client-s3, @aws-sdk/lib-storage, @aws-sdk/s3-request-presigner)\n`.trim();\n\n/**\n * Atomic initialization promise to handle concurrent calls to loadS3\n */\nlet initializationPromise: Promise<void> | null = null;\n\n/**\n * Load S3 modules lazily\n *\n * @example\n * await loadS3();\n * if (isModuleExists) {\n * // Safe to use S3Client, S3Storage, S3Presigner\n * }\n */\nexport async function loadS3(): Promise<void> {\n // If already successfully loaded, no need to do anything\n if (isModuleExists === true) return;\n\n // If currently loading, return the existing promise so the caller waits\n if (initializationPromise) return initializationPromise;\n\n initializationPromise = (async () => {\n try {\n // Use Promise.all to load modules in parallel for efficiency\n const [client, storage, presigner] = await Promise.all([\n import(\"@aws-sdk/client-s3\"),\n import(\"@aws-sdk/lib-storage\"),\n import(\"@aws-sdk/s3-request-presigner\"),\n ]);\n\n S3Client = client;\n S3Storage = storage;\n S3Presigner = presigner;\n\n isModuleExists = true;\n } catch {\n // Mark as failed if any import fails (usually means packages aren't installed)\n isModuleExists = false;\n }\n })();\n\n return initializationPromise;\n}\n\nloadS3();\n\n/**\n * Synchronous fallback loader.\n *\n * `loadS3()` above is async and fire-and-forget at module load, so on the\n * very first *synchronous* `CloudDriver` construction the dynamic `import()`\n * microtask has not yet resolved — `isModuleExists` is still `null` even when\n * the SDK is genuinely installed. The constructor's install-check would then\n * throw the misleading \"SDK not installed\" error.\n *\n * This probes the modules synchronously via `require` (the AWS SDK ships\n * CJS-compatible builds) so a present SDK resolves the install-check\n * immediately, without waiting on the async path. Returns `true` when all\n * three modules loaded, `false` otherwise. Only invoked while\n * `isModuleExists` is still indeterminate.\n */\nfunction loadS3Sync(): boolean {\n if (isModuleExists !== null) {\n return isModuleExists;\n }\n\n try {\n // `createRequire` gives an ESM-safe synchronous `require` regardless of\n // whether this module runs as CJS or ESM at runtime.\n const req = createRequire(import.meta.url);\n S3Client = req(\"@aws-sdk/client-s3\");\n S3Storage = req(\"@aws-sdk/lib-storage\");\n S3Presigner = req(\"@aws-sdk/s3-request-presigner\");\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n\n return isModuleExists;\n}\n\n// ============================================================\n// CloudDriver Base Class\n// ============================================================\n\n/**\n * Base abstract class for all S3-compatible cloud storage drivers\n *\n * This class contains all shared logic for S3-compatible storage services\n * including AWS S3, Cloudflare R2, DigitalOcean Spaces, and others.\n *\n * **Important:** S3 SDK packages are lazy-loaded on first use.\n * Users must install them separately:\n * ```\n * npm install @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner\n * ```\n *\n * Subclasses must implement:\n * - `name`: Driver identifier (e.g., \"s3\", \"r2\", \"spaces\")\n * - `url()`: Returns the public URL for a file (provider-specific format)\n */\nexport abstract class CloudDriver<\n TOptions extends Partial<CloudStorageDriverOptions> = CloudStorageDriverOptions,\n> implements CloudStorageDriverContract {\n /**\n * S3-compatible client (lazy-loaded)\n */\n protected client!: InstanceType<typeof import(\"@aws-sdk/client-s3\").S3Client>;\n\n /**\n * Retry configuration\n */\n protected retryConfig: {\n maxRetries: number;\n initialDelayMs: number;\n maxDelayMs: number;\n backoffMultiplier: number;\n };\n\n /**\n * How object visibility is applied: per-object ACLs (default), bucket\n * policy (no per-object ACL is sent), or unsupported (\"none\").\n * Buckets with ACLs disabled (S3 BucketOwnerEnforced, R2) need\n * \"policy\" or \"none\".\n */\n protected get visibilityMode(): \"acl\" | \"policy\" | \"none\" {\n return (this.options as { visibilityMode?: \"acl\" | \"policy\" | \"none\" }).visibilityMode ?? \"acl\";\n }\n\n /**\n * Resolve the ACL to send on a put for the requested visibility.\n *\n * @throws {StorageCapabilityError} When public is requested but the driver has no visibility support\n */\n protected resolveAcl(visibility?: FileVisibility): \"public-read\" | undefined {\n if (visibility !== \"public\") return undefined;\n\n if (this.visibilityMode === \"none\") {\n throw new StorageCapabilityError(\"public visibility\", this.name);\n }\n\n return this.visibilityMode === \"acl\" ? \"public-read\" : undefined;\n }\n\n public constructor(public options: TOptions) {\n // The async loadS3() may not have resolved yet on the first synchronous\n // construction. When the flag is still indeterminate, probe synchronously\n // so a genuinely-installed SDK never triggers the misleading throw below.\n if (isModuleExists === null) {\n loadS3Sync();\n }\n\n if (!isModuleExists) {\n throw new Error(S3_INSTALL_INSTRUCTIONS);\n }\n\n // Any remaining option is an S3ClientConfig option (forcePathStyle,\n // requestHandler, maxAttempts, ...) and is passed through to the SDK.\n const {\n bucket: _bucket,\n accessKeyId,\n secretAccessKey,\n endpoint: _endpoint,\n prefix: _prefix,\n urlPrefix: _urlPrefix,\n retry: _retry,\n accountId: _accountId,\n publicDomain: _publicDomain,\n driver: _driver,\n visibilityMode: _visibilityMode,\n ...clientOptions\n } = this.options as TOptions & Record<string, unknown>;\n\n this.client = new S3Client.S3Client({\n ...clientOptions,\n region: this.options.region!,\n // Without static keys the SDK default provider chain applies\n // (IAM role, ECS task role, IRSA, SSO profile).\n ...(accessKeyId && secretAccessKey && { credentials: { accessKeyId, secretAccessKey } }),\n ...(this.getEndpoint() && { endpoint: this.getEndpoint() }),\n } as import(\"@aws-sdk/client-s3\").S3ClientConfig);\n\n // Initialize retry configuration\n this.retryConfig = {\n maxRetries: this.options.retry?.maxRetries ?? 3,\n initialDelayMs: this.options.retry?.initialDelayMs ?? 1000,\n maxDelayMs: this.options.retry?.maxDelayMs ?? 10000,\n backoffMultiplier: this.options.retry?.backoffMultiplier ?? 2,\n };\n }\n\n /**\n * Driver name identifier\n */\n public abstract readonly name: StorageDriverType;\n\n /**\n * Get public URL for file\n * Must be implemented by subclasses with provider-specific format\n */\n public abstract url(location: string): string;\n\n /**\n * Get endpoint URL\n * Can be overridden by subclasses for provider-specific endpoints\n */\n protected getEndpoint(): string | undefined {\n return this.options.endpoint;\n }\n\n // ============================================================\n // Helper Methods\n // ============================================================\n\n /**\n * Apply prefix to location path\n *\n * Priority: context prefix > driver options prefix > no prefix\n * This allows multi-tenant scenarios where context overrides driver config.\n *\n * @param location - Original location path\n * @returns Location with prefix applied if one exists\n */\n public applyPrefix(location: string): string {\n // Check context prefix first (highest priority)\n const contextPrefix = storageDriverContext.getPrefix();\n const prefix = contextPrefix || this.options.prefix;\n\n if (!prefix) {\n return location;\n }\n\n const cleanPrefix = prefix.replace(/\\/+$/, \"\");\n const cleanLocation = location.replace(/^\\/+/, \"\");\n\n // Avoid double-prefixing\n if (cleanLocation.startsWith(cleanPrefix + \"/\") || cleanLocation === cleanPrefix) {\n return cleanLocation;\n }\n\n return `${cleanPrefix}/${cleanLocation}`;\n }\n\n /**\n * Normalize storage path (remove double slashes, sanitize)\n * @internal\n */\n protected normalizePath(path: string): string {\n return path\n .replace(/\\/+/g, \"/\") // Remove multiple slashes\n .replace(/^\\//, \"\") // Remove leading slash\n .trim();\n }\n\n /**\n * Execute an operation with retry logic\n *\n * Retries on transient errors with exponential backoff.\n *\n * @param operation - Async operation to execute\n * @param operationName - Name for logging\n * @returns Result of the operation\n * @internal\n */\n protected async withRetry<T>(\n operation: () => Promise<T>,\n operationName: string = \"operation\",\n ): Promise<T> {\n const { maxRetries, initialDelayMs, backoffMultiplier, maxDelayMs } = this.retryConfig;\n\n let lastError: unknown;\n\n // maxRetries counts retries, not attempts: 0 means a single attempt\n const attempts = maxRetries + 1;\n\n for (let attempt = 0; attempt < attempts; attempt++) {\n try {\n return await operation();\n } catch (error) {\n lastError = error;\n\n // Don't retry on the last attempt\n if (attempt === attempts - 1) {\n break;\n }\n\n // Check if error is retryable\n if (!this.isRetryableError(error)) {\n throw error; // Non-retryable error, fail immediately\n }\n\n // Calculate delay with exponential backoff\n const delayMs = Math.min(initialDelayMs * Math.pow(backoffMultiplier, attempt), maxDelayMs);\n\n // Wait before retrying\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n }\n\n // Rethrow the original error so name/$metadata survive\n throw lastError;\n }\n\n /**\n * Check if an error is retryable\n *\n * Retries on:\n * - Network errors\n * - 5xx server errors\n * - Rate limiting (429)\n * - Timeout errors\n *\n * Does NOT retry on:\n * - 4xx client errors (except 429)\n * - Authentication errors\n * - Not found errors\n *\n * @param error - Error to check\n * @returns true if error is retryable\n * @internal\n */\n protected isRetryableError(error: any): boolean {\n // Network errors\n if (error.code === \"ECONNRESET\" || error.code === \"ETIMEDOUT\" || error.code === \"ENOTFOUND\") {\n return true;\n }\n\n // S3 SDK errors\n if (error.name === \"NetworkingError\" || error.name === \"TimeoutError\") {\n return true;\n }\n\n // HTTP status codes\n const statusCode = error.$metadata?.httpStatusCode || error.statusCode;\n if (statusCode) {\n // 5xx server errors are retryable\n if (statusCode >= 500 && statusCode < 600) {\n return true;\n }\n\n // 429 Too Many Requests is retryable\n if (statusCode === 429) {\n return true;\n }\n }\n\n return false;\n }\n\n // ============================================================\n // Core File Operations\n // ============================================================\n\n /**\n * Put file to cloud storage\n */\n public async put(\n file: Buffer,\n location: string,\n options?: PutOptions,\n ): Promise<CloudStorageFileData> {\n return this.withRetry(async () => {\n const { PutObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const hash = this.calculateHash(file);\n const mimeType = options?.mimeType || this.guessMimeType(location);\n\n const command = new PutObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n Body: file,\n ContentType: mimeType,\n CacheControl: options?.cacheControl,\n ContentDisposition: options?.contentDisposition,\n Metadata: options?.metadata,\n ACL: this.resolveAcl(options?.visibility),\n });\n\n const result = await this.client.send(command);\n\n return {\n path: location,\n url: this.url(location),\n size: file.length,\n hash,\n mimeType,\n driver: this.name,\n bucket: this.options.bucket!,\n region: this.options.region!,\n etag: result.ETag,\n versionId: result.VersionId,\n };\n }, \"put\");\n }\n\n /**\n * Atomically put a file only if the key does not exist (S3 conditional\n * write, `If-None-Match: *`).\n *\n * HTTP 412 (PreconditionFailed) and 409 (ConditionalRequestConflict, a\n * concurrent conditional write in flight) mean \"already exists\" and return\n * `null`. Every other error throws.\n */\n public async putIfAbsent(\n file: Buffer | string,\n location: string,\n options?: PutOptions,\n ): Promise<CloudStorageFileData | null> {\n const body = typeof file === \"string\" ? Buffer.from(file) : file;\n\n return this.withRetry(async () => {\n const { PutObjectCommand } = S3Client;\n\n location = this.applyPrefix(location);\n\n const hash = this.calculateHash(body);\n const mimeType = options?.mimeType || this.guessMimeType(location);\n\n const command = new PutObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n Body: body,\n ContentType: mimeType,\n CacheControl: options?.cacheControl,\n ContentDisposition: options?.contentDisposition,\n Metadata: options?.metadata,\n ACL: this.resolveAcl(options?.visibility),\n IfNoneMatch: \"*\",\n });\n\n let result;\n\n try {\n result = await this.client.send(command);\n } catch (error: any) {\n const status = error?.$metadata?.httpStatusCode;\n\n if (\n status === 412 ||\n status === 409 ||\n error?.name === \"PreconditionFailed\" ||\n error?.name === \"ConditionalRequestConflict\"\n ) {\n return null;\n }\n\n throw error;\n }\n\n return {\n path: location,\n url: this.url(location),\n size: body.length,\n hash,\n mimeType,\n driver: this.name,\n bucket: this.options.bucket!,\n region: this.options.region!,\n etag: result.ETag,\n versionId: result.VersionId,\n };\n }, \"putIfAbsent\");\n }\n\n /**\n * Put file from a readable stream (for large files)\n * Uses S3 multipart upload for efficient streaming\n */\n public async putStream(\n stream: Readable,\n location: string,\n options?: PutOptions,\n ): Promise<CloudStorageFileData> {\n // Not wrapped in withRetry: a retry would replay an already-consumed\n // stream, and lib-storage already retries individual parts.\n {\n const { Upload } = S3Storage;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const mimeType = options?.mimeType || this.guessMimeType(location);\n\n const upload = new Upload({\n client: this.client,\n params: {\n Bucket: this.options.bucket,\n Key: location,\n Body: stream,\n ContentType: mimeType,\n CacheControl: options?.cacheControl,\n ContentDisposition: options?.contentDisposition,\n Metadata: options?.metadata,\n ACL: this.resolveAcl(options?.visibility),\n },\n });\n\n const result = await upload.done();\n\n // Get file info for size and hash\n const info = await this.metadata(location);\n\n return {\n path: location,\n url: this.url(location),\n size: info.size,\n hash: info.etag?.replace(/\"/g, \"\") || \"\",\n mimeType,\n driver: this.name,\n bucket: this.options.bucket!,\n region: this.options.region!,\n etag: result.ETag,\n versionId: result.VersionId,\n };\n }\n }\n\n /**\n * Get file contents as Buffer\n */\n public async get(location: string): Promise<Buffer> {\n return this.withRetry(async () => {\n const { GetObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new GetObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n const result = await this.client.send(command);\n\n if (!result.Body) {\n throw new Error(`File not found: ${location}`);\n }\n\n return Buffer.from(await result.Body.transformToByteArray());\n }, \"get\");\n }\n\n /**\n * Get file as a readable stream (for large files)\n */\n public async getStream(location: string): Promise<Readable> {\n return this.withRetry(async () => {\n const { GetObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new GetObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n const result = await this.client.send(command);\n\n if (!result.Body) {\n throw new Error(`File not found: ${location}`);\n }\n\n return result.Body as Readable;\n }, \"getStream\");\n }\n\n /**\n * Delete a file\n */\n public async delete(location: string): Promise<boolean> {\n return this.withRetry(async () => {\n const { DeleteObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new DeleteObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n await this.client.send(command);\n\n return true;\n }, \"delete\");\n }\n\n /**\n * Delete multiple files at once (uses batch delete for efficiency)\n */\n public async deleteMany(locations: string[]): Promise<DeleteManyResult[]> {\n if (locations.length === 0) {\n return [];\n }\n\n return this.withRetry(async () => {\n const { DeleteObjectsCommand } = S3Client;\n\n // Apply storage prefix to all locations\n const prefixedLocations = locations.map((loc) => this.applyPrefix(loc));\n\n const command = new DeleteObjectsCommand({\n Bucket: this.options.bucket,\n Delete: {\n Objects: prefixedLocations.map((Key) => ({ Key })),\n Quiet: false,\n },\n });\n\n const result = await this.client.send(command);\n const results: DeleteManyResult[] = [];\n\n // Process successful deletes\n for (const deleted of result.Deleted || []) {\n if (deleted.Key) {\n results.push({ location: deleted.Key, deleted: true });\n }\n }\n\n // Process errors\n for (const error of result.Errors || []) {\n if (error.Key) {\n results.push({\n location: error.Key,\n deleted: false,\n error: error.Message || \"Unknown error\",\n });\n }\n }\n\n return results;\n }, \"deleteMany\");\n }\n\n /**\n * Delete directory (recursively deletes all objects with matching prefix)\n *\n * S3/R2 doesn't have true directories - only key prefixes.\n * This method lists all objects with the prefix and deletes them in batches.\n *\n * @param directoryPath - Directory prefix to delete\n * @returns true when all objects are deleted\n */\n public async deleteDirectory(directoryPath: string): Promise<boolean> {\n const { ListObjectsV2Command, DeleteObjectsCommand } = S3Client;\n\n // Apply storage prefix\n directoryPath = this.applyPrefix(directoryPath);\n\n // Ensure directory path ends with / for proper prefix matching\n const prefix = directoryPath.endsWith(\"/\") ? directoryPath : `${directoryPath}/`;\n\n // Drive the loop with the ListObjectsV2 IsTruncated / ContinuationToken\n // contract rather than guessing termination from page size. Each page\n // carries its own ContinuationToken so we never re-list page one.\n let continuationToken: string | undefined;\n\n do {\n const listResult = await this.withRetry(\n () =>\n this.client.send(\n new ListObjectsV2Command({\n Bucket: this.options.bucket,\n Prefix: prefix,\n ContinuationToken: continuationToken,\n }),\n ),\n \"deleteDirectory:list\",\n );\n\n const keys = (listResult.Contents || [])\n .map((object) => object.Key)\n .filter((key): key is string => Boolean(key));\n\n if (keys.length > 0) {\n // Delete the raw keys directly — they already carry the full prefix\n // from the listing, so we bypass deleteMany's applyPrefix step.\n await this.withRetry(\n () =>\n this.client.send(\n new DeleteObjectsCommand({\n Bucket: this.options.bucket,\n Delete: {\n Objects: keys.map((Key) => ({ Key })),\n Quiet: true,\n },\n }),\n ),\n \"deleteDirectory:delete\",\n );\n }\n\n continuationToken = listResult.IsTruncated ? listResult.NextContinuationToken : undefined;\n } while (continuationToken);\n\n return true;\n }\n\n /**\n * Check if file exists\n */\n public async exists(location: string): Promise<boolean> {\n // No retry for exists - it should be fast and failure means non-existent\n try {\n const { HeadObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new HeadObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n await this.client.send(command);\n return true;\n } catch (error) {\n // Only a definite \"not found\" means absent; credential/network/5xx\n // errors must surface, otherwise `if (!exists) put()` overwrites.\n const { name, $metadata } = (error ?? {}) as {\n name?: string;\n $metadata?: { httpStatusCode?: number };\n };\n\n if (name === \"NotFound\" || name === \"NoSuchKey\" || $metadata?.httpStatusCode === 404) {\n return false;\n }\n\n throw error;\n }\n }\n\n // ============================================================\n // URL Operations\n // ============================================================\n\n /**\n * Get a temporary presigned URL (alias for getPresignedUrl)\n */\n public async temporaryUrl(location: string, expiresIn = 3600): Promise<string> {\n return this.getPresignedUrl(location, { expiresIn });\n }\n\n /**\n * Get presigned URL for downloading\n */\n public async getPresignedUrl(location: string, options?: PresignedOptions): Promise<string> {\n return this.withRetry(async () => {\n const { GetObjectCommand } = S3Client;\n const { getSignedUrl } = S3Presigner;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new GetObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n return getSignedUrl(this.client, command, {\n expiresIn: options?.expiresIn || 3600,\n });\n }, \"getPresignedUrl\");\n }\n\n /**\n * Get presigned URL for uploading\n */\n public async getPresignedUploadUrl(\n location: string,\n options?: PresignedUploadOptions,\n ): Promise<string> {\n return this.withRetry(async () => {\n const { PutObjectCommand } = S3Client;\n const { getSignedUrl } = S3Presigner;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new PutObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n ContentType: options?.contentType,\n Metadata: options?.metadata,\n });\n\n return getSignedUrl(this.client, command, {\n expiresIn: options?.expiresIn || 3600,\n });\n }, \"getPresignedUploadUrl\");\n }\n\n // ============================================================\n // Metadata Operations\n // ============================================================\n\n /**\n * Get file info/metadata without downloading\n */\n public async metadata(location: string): Promise<StorageFileInfo> {\n return this.withRetry(async () => {\n const { HeadObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new HeadObjectCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n const result = await this.client.send(command);\n const name = location.split(\"/\").pop() || \"\";\n\n return {\n path: location,\n name,\n size: result.ContentLength || 0,\n isDirectory: false,\n lastModified: result.LastModified,\n mimeType: result.ContentType || this.guessMimeType(location),\n etag: result.ETag,\n storageClass: result.StorageClass,\n };\n }, \"metadata\");\n }\n\n /**\n * Get file size in bytes (shortcut for metadata().size)\n */\n public async size(location: string): Promise<number> {\n const info = await this.metadata(location);\n return info.size;\n }\n\n // ============================================================\n // File Operations\n // ============================================================\n\n /**\n * Build a URL-encoded CopySource (\"bucket/key\") as the SDK requires\n */\n protected copySource(key: string): string {\n const encodedKey = key.split(\"/\").map(encodeURIComponent).join(\"/\");\n\n return `${this.options.bucket}/${encodedKey}`;\n }\n\n /**\n * Copy file to a new location\n */\n public async copy(from: string, to: string): Promise<CloudStorageFileData> {\n return this.withRetry(async () => {\n const { CopyObjectCommand, HeadObjectCommand } = S3Client;\n\n // CopyObject drops the ACL, so a public source would silently become private\n const visibility = await this.getVisibility(from).catch(() => \"private\" as const);\n\n // Apply storage prefix to both paths\n from = this.applyPrefix(from);\n to = this.applyPrefix(to);\n\n const command = new CopyObjectCommand({\n Bucket: this.options.bucket,\n CopySource: this.copySource(from),\n Key: to,\n ...(this.visibilityMode === \"acl\" && visibility === \"public\" && { ACL: \"public-read\" as const }),\n });\n\n const result = await this.client.send(command);\n\n // Get file metadata\n const headCommand = new HeadObjectCommand({\n Bucket: this.options.bucket,\n Key: to,\n });\n const headResult = await this.client.send(headCommand);\n\n return {\n path: to,\n url: this.url(to),\n size: headResult.ContentLength || 0,\n hash: headResult.ETag?.replace(/\"/g, \"\") || \"\",\n mimeType: headResult.ContentType || this.guessMimeType(to),\n driver: this.name,\n bucket: this.options.bucket!,\n region: this.options.region!,\n etag: result.CopyObjectResult?.ETag,\n versionId: result.VersionId,\n };\n }, \"copy\");\n }\n\n /**\n * Move file to a new location\n */\n public async move(from: string, to: string): Promise<CloudStorageFileData> {\n const file = await this.copy(from, to);\n await this.delete(from);\n return file;\n }\n\n /**\n * List files in a directory\n */\n public async list(directory: string, options?: ListOptions): Promise<StorageFileInfo[]> {\n return this.withRetry(async () => {\n const { ListObjectsV2Command } = S3Client;\n\n // Apply storage prefix\n directory = this.applyPrefix(directory);\n\n // A directory prefix must end in \"/\" so \"images\" doesn't match \"images-old/…\"\n if (directory && directory !== \"/\" && !directory.endsWith(\"/\")) {\n directory = `${directory}/`;\n }\n\n const command = new ListObjectsV2Command({\n Bucket: this.options.bucket,\n Prefix: directory,\n MaxKeys: options?.limit,\n ContinuationToken: options?.cursor,\n Delimiter: options?.recursive ? undefined : \"/\",\n });\n\n const result = await this.client.send(command);\n const files: StorageFileInfo[] = [];\n\n // Add files\n for (const object of result.Contents || []) {\n if (!object.Key) continue;\n\n files.push({\n path: object.Key,\n name: object.Key.split(\"/\").pop() || \"\",\n size: object.Size || 0,\n isDirectory: false,\n lastModified: object.LastModified,\n etag: object.ETag,\n storageClass: object.StorageClass,\n });\n }\n\n // Add directories\n for (const prefix of result.CommonPrefixes || []) {\n if (!prefix.Prefix) continue;\n\n files.push({\n path: prefix.Prefix,\n name: prefix.Prefix.split(\"/\").filter(Boolean).pop() || \"\",\n size: 0,\n isDirectory: true,\n });\n }\n\n return files;\n }, \"list\");\n }\n\n // ============================================================\n // Cloud-Specific Operations\n // ============================================================\n\n /**\n * Get bucket name\n */\n public getBucket(): string {\n return this.options.bucket!;\n }\n\n /**\n * Get region\n */\n public getRegion(): string {\n return this.options.region!;\n }\n\n /**\n * Set storage class (e.g., STANDARD, GLACIER, etc.)\n */\n public async setStorageClass(location: string, storageClass: string): Promise<void> {\n return this.withRetry(async () => {\n const { CopyObjectCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new CopyObjectCommand({\n Bucket: this.options.bucket,\n CopySource: this.copySource(location),\n Key: location,\n StorageClass: storageClass as any,\n MetadataDirective: \"COPY\",\n });\n\n await this.client.send(command);\n }, \"setStorageClass\");\n }\n\n /**\n * Set file visibility (public or private)\n */\n public async setVisibility(location: string, visibility: FileVisibility): Promise<void> {\n if (this.visibilityMode !== \"acl\") {\n throw new StorageCapabilityError(\"setVisibility\", this.name);\n }\n\n return this.withRetry(async () => {\n const { PutObjectAclCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new PutObjectAclCommand({\n Bucket: this.options.bucket,\n Key: location,\n ACL: visibility === \"public\" ? \"public-read\" : \"private\",\n });\n\n await this.client.send(command);\n }, \"setVisibility\");\n }\n\n /**\n * Get file visibility\n */\n public async getVisibility(location: string): Promise<FileVisibility> {\n return this.withRetry(async () => {\n const { GetObjectAclCommand } = S3Client;\n\n // Apply storage prefix\n location = this.applyPrefix(location);\n\n const command = new GetObjectAclCommand({\n Bucket: this.options.bucket,\n Key: location,\n });\n\n const result = await this.client.send(command);\n\n // Check if any grant allows public read\n const hasPublicRead = result.Grants?.some(\n (grant) =>\n grant.Grantee?.URI === \"http://acs.amazonaws.com/groups/global/AllUsers\" &&\n grant.Permission === \"READ\",\n );\n\n return hasPublicRead ? \"public\" : \"private\";\n }, \"getVisibility\");\n }\n\n // ============================================================\n // Utilities\n // ============================================================\n\n /**\n * Calculate SHA-256 hash\n */\n protected calculateHash(buffer: Buffer): string {\n return crypto.createHash(\"sha256\").update(new Uint8Array(buffer)).digest(\"hex\");\n }\n\n /**\n * Guess MIME type from file extension\n */\n protected guessMimeType(location: string): string {\n return getMimeType(location);\n }\n}\n"],"mappings":"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"}
|
|
@@ -34,10 +34,6 @@ declare class LocalDriver implements StorageDriverContract {
|
|
|
34
34
|
* Secret key for signing temporary URLs
|
|
35
35
|
*/
|
|
36
36
|
protected signatureKey?: string;
|
|
37
|
-
/**
|
|
38
|
-
* Cached Storage File Metadata
|
|
39
|
-
*/
|
|
40
|
-
protected _metadata: Map<string, StorageFileInfo>;
|
|
41
37
|
constructor(options?: LocalStorageDriverOptions);
|
|
42
38
|
/**
|
|
43
39
|
* Apply prefix to location path
|
|
@@ -89,7 +85,9 @@ declare class LocalDriver implements StorageDriverContract {
|
|
|
89
85
|
* contained within the storage root — the raw, caller-supplied argument is
|
|
90
86
|
* never handed to the filesystem (preventing `../` traversal deletes).
|
|
91
87
|
*/
|
|
92
|
-
deleteDirectory(directoryPath: string
|
|
88
|
+
deleteDirectory(directoryPath: string, options?: {
|
|
89
|
+
allowRoot?: boolean;
|
|
90
|
+
}): Promise<boolean>;
|
|
93
91
|
/**
|
|
94
92
|
* Check if file exists
|
|
95
93
|
*/
|
|
@@ -164,6 +162,10 @@ declare class LocalDriver implements StorageDriverContract {
|
|
|
164
162
|
* Calculate SHA-256 hash
|
|
165
163
|
*/
|
|
166
164
|
protected calculateHash(buffer: Buffer): string;
|
|
165
|
+
/**
|
|
166
|
+
* Calculate SHA-256 hash of a file by streaming it (constant memory)
|
|
167
|
+
*/
|
|
168
|
+
protected hashFile(absolutePath: string): Promise<string>;
|
|
167
169
|
/**
|
|
168
170
|
* Guess MIME type from file extension
|
|
169
171
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-driver.d.mts","names":[],"sources":["../../../../../../../../core/src/storage/drivers/local-driver.ts"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"local-driver.d.mts","names":[],"sources":["../../../../../../../../core/src/storage/drivers/local-driver.ts"],"mappings":";;;;;;;;AA2CA;;;;;;cAAa,WAAA,YAAuB,qBAAA;EA0BR,OAAA,EAAS,yBAAA;EAqDvB;;;EAAA,SA3EI,IAAA,EAAM,iBAAA;EA+GV;;;EAAA,UA1GF,IAAA;EA+JE;;;EAAA,UA1JF,SAAA;EA8L0B;;;EAAA,UAzL1B,kBAAA;EAmO4C;;;EAAA,UA9N5C,YAAA;cAEgB,OAAA,GAAS,yBAAA;EAyUyB;;;;;;;;;EAlTrD,WAAA,CAAY,QAAA;EAydkD;;;EA9bxD,GAAA,CACX,IAAA,EAAM,MAAA,YAAkB,YAAA,EACxB,QAAA,UACA,OAAA,GAAU,UAAA,GACT,OAAA,CAAQ,eAAA;EAmhB6D;;;;;;;;EAnf3D,WAAA,CACX,IAAA,EAAM,MAAA,WACN,QAAA,UACA,OAAA,GAAU,UAAA,GACT,OAAA,CAAQ,eAAA;EA1FwB;;;EA2ItB,SAAA,CACX,MAAA,EAAQ,QAAA,EACR,QAAA,UACA,OAAA,GAAU,UAAA,GACT,OAAA,CAAQ,eAAA;EA3JD;;;EA8LG,GAAA,CAAI,QAAA,WAAmB,OAAA,CAAQ,MAAA;EAlLT;;;EA+LtB,SAAA,CAAU,QAAA,WAAmB,OAAA,CAAQ,QAAA;EA7IrC;;;EA0JA,MAAA,CAAO,QAAA,WAAmB,OAAA;EAxJrC;;;EAwKW,UAAA,CAAW,SAAA,aAAsB,OAAA,CAAQ,gBAAA;EAtK3C;;;;;;;EAgME,eAAA,CAAgB,aAAA,UAAuB,OAAA;IAAY,SAAA;EAAA,IAAqB,OAAA;EA1G3E;;;EA4HG,MAAA,CAAO,QAAA,WAAmB,OAAA;EA1HrC;;;EAsIK,GAAA,CAAI,QAAA;EAlGM;;;;;;;EA6GJ,YAAA,CAAa,QAAA,UAAkB,SAAA,YAAmB,OAAA;EAnF3C;;;;;;EAqGb,oBAAA,CAAqB,QAAA,UAAkB,SAAA;EA3DjB;;;;;;EAmFhB,sBAAA,CAAuB,KAAA,WAAgB,OAAA,CAAQ,wBAAA;EArDrD;;;EAiIM,QAAA,CAAS,QAAA,WAAmB,OAAA,CAAQ,eAAA;EAtHL;;;EA6I/B,IAAA,CAAK,QAAA,WAAmB,OAAA;EA3HS;;;EAuIjC,IAAA,CAAK,IAAA,UAAc,EAAA,WAAa,OAAA,CAAQ,eAAA;EA/GO;;;EA2I/C,IAAA,CAAK,IAAA,UAAc,EAAA,WAAa,OAAA,CAAQ,eAAA;EA/DJ;;;EA2FpC,IAAA,CAAK,SAAA,UAAmB,OAAA,GAAU,WAAA,GAAc,OAAA,CAAQ,eAAA;EAxDxD;;;EAwGN,IAAA,CAAK,QAAA;EAxGyC;;;EA+G9C,OAAA;EAnFsC;;;;;;;EAAA,UAkGnC,eAAA,CAAgB,QAAA;EAtBnB;;;EAAA,UAyCS,QAAA,CAAS,IAAA,EAAM,MAAA,YAAkB,YAAA,GAAe,OAAA,CAAQ,MAAA;EAnB9C;;;EAAA,UAmChB,aAAA,CAAc,MAAA,EAAQ,MAAA;EAhBP;;;EAAA,UAuBT,QAAA,CAAS,YAAA,WAAuB,OAAA;EAPhB;;;EAAA,UAoBtB,aAAA,CAAc,QAAA;AAAA"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { storagePath } from "../../utils/paths.mjs";
|
|
2
2
|
import { url } from "../../utils/urls.mjs";
|
|
3
|
+
import { StorageError } from "../utils/storage-error.mjs";
|
|
3
4
|
import { storageDriverContext } from "../context/storage-driver-context.mjs";
|
|
4
5
|
import { getMimeType } from "../utils/mime.mjs";
|
|
5
6
|
import { resolveWithinRoot } from "../utils/contain-path.mjs";
|
|
@@ -9,6 +10,7 @@ import { directoryExistsAsync, ensureDirectoryAsync, fileExistsAsync, removeDire
|
|
|
9
10
|
import { createReadStream, createWriteStream, promises } from "fs";
|
|
10
11
|
import { copyFile, readFile, readdir, rename, stat, unlink, writeFile } from "fs/promises";
|
|
11
12
|
import crypto from "crypto";
|
|
13
|
+
import { Transform } from "stream";
|
|
12
14
|
import { pipeline } from "stream/promises";
|
|
13
15
|
|
|
14
16
|
//#region ../core/src/storage/drivers/local-driver.ts
|
|
@@ -26,7 +28,6 @@ var LocalDriver = class {
|
|
|
26
28
|
this.options = options;
|
|
27
29
|
this.name = "local";
|
|
28
30
|
this.urlPrefix = "";
|
|
29
|
-
this._metadata = /* @__PURE__ */ new Map();
|
|
30
31
|
this.root = resolve(options.root ?? storagePath());
|
|
31
32
|
this.urlPrefix = options.urlPrefix ?? "";
|
|
32
33
|
this.temporaryUrlPrefix = options.temporaryUrlPrefix ?? "/temp-files";
|
|
@@ -58,11 +59,10 @@ var LocalDriver = class {
|
|
|
58
59
|
const fileBuffer = await this.toBuffer(file);
|
|
59
60
|
const hash = this.calculateHash(fileBuffer);
|
|
60
61
|
await writeFile(absolutePath, new Uint8Array(fileBuffer));
|
|
61
|
-
this._metadata.delete(location);
|
|
62
62
|
const stats = await stat(absolutePath);
|
|
63
63
|
const mimeType = options?.mimeType || this.guessMimeType(location);
|
|
64
64
|
return {
|
|
65
|
-
path: location,
|
|
65
|
+
path: this.applyPrefix(location),
|
|
66
66
|
url: this.url(location),
|
|
67
67
|
size: stats.size,
|
|
68
68
|
hash,
|
|
@@ -96,11 +96,10 @@ var LocalDriver = class {
|
|
|
96
96
|
} finally {
|
|
97
97
|
await unlink(tempPath).catch(() => void 0);
|
|
98
98
|
}
|
|
99
|
-
this._metadata.delete(location);
|
|
100
99
|
const stats = await stat(absolutePath);
|
|
101
100
|
const mimeType = options?.mimeType || this.guessMimeType(location);
|
|
102
101
|
return {
|
|
103
|
-
path: location,
|
|
102
|
+
path: this.applyPrefix(location),
|
|
104
103
|
url: this.url(location),
|
|
105
104
|
size: stats.size,
|
|
106
105
|
hash,
|
|
@@ -114,14 +113,16 @@ var LocalDriver = class {
|
|
|
114
113
|
async putStream(stream, location, options) {
|
|
115
114
|
const absolutePath = this.getAbsolutePath(location);
|
|
116
115
|
await ensureDirectoryAsync(dirname(absolutePath));
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
const hasher = crypto.createHash("sha256");
|
|
117
|
+
await pipeline(stream, new Transform({ transform(chunk, _encoding, callback) {
|
|
118
|
+
hasher.update(chunk);
|
|
119
|
+
callback(null, chunk);
|
|
120
|
+
} }), createWriteStream(absolutePath));
|
|
121
|
+
const hash = hasher.digest("hex");
|
|
121
122
|
const stats = await stat(absolutePath);
|
|
122
123
|
const mimeType = options?.mimeType || this.guessMimeType(location);
|
|
123
124
|
return {
|
|
124
|
-
path: location,
|
|
125
|
+
path: this.applyPrefix(location),
|
|
125
126
|
url: this.url(location),
|
|
126
127
|
size: stats.size,
|
|
127
128
|
hash,
|
|
@@ -152,7 +153,6 @@ var LocalDriver = class {
|
|
|
152
153
|
const absolutePath = this.getAbsolutePath(location);
|
|
153
154
|
if (!await fileExistsAsync(absolutePath)) return false;
|
|
154
155
|
await unlinkAsync(absolutePath);
|
|
155
|
-
this._metadata.delete(location);
|
|
156
156
|
return true;
|
|
157
157
|
}
|
|
158
158
|
/**
|
|
@@ -182,8 +182,10 @@ var LocalDriver = class {
|
|
|
182
182
|
* contained within the storage root — the raw, caller-supplied argument is
|
|
183
183
|
* never handed to the filesystem (preventing `../` traversal deletes).
|
|
184
184
|
*/
|
|
185
|
-
async deleteDirectory(directoryPath) {
|
|
186
|
-
|
|
185
|
+
async deleteDirectory(directoryPath, options) {
|
|
186
|
+
const absolutePath = this.getAbsolutePath(directoryPath);
|
|
187
|
+
if (!options?.allowRoot && resolve(absolutePath) === this.root) throw new StorageError(`Refusing to delete the storage root ("${directoryPath}"). Pass { allowRoot: true } to confirm.`, { context: { directoryPath } });
|
|
188
|
+
await removeDirectoryAsync(absolutePath);
|
|
187
189
|
return true;
|
|
188
190
|
}
|
|
189
191
|
/**
|
|
@@ -197,7 +199,7 @@ var LocalDriver = class {
|
|
|
197
199
|
* Get public URL for file
|
|
198
200
|
*/
|
|
199
201
|
url(location) {
|
|
200
|
-
return url(this.urlPrefix + "/" + ltrim(location, "/"));
|
|
202
|
+
return url(this.urlPrefix + "/" + ltrim(this.applyPrefix(location), "/"));
|
|
201
203
|
}
|
|
202
204
|
/**
|
|
203
205
|
* Get a temporary signed URL that expires
|
|
@@ -289,20 +291,17 @@ var LocalDriver = class {
|
|
|
289
291
|
* Get file info/metadata without downloading
|
|
290
292
|
*/
|
|
291
293
|
async metadata(location) {
|
|
292
|
-
if (this._metadata.has(location)) return this._metadata.get(location);
|
|
293
294
|
const absolutePath = this.getAbsolutePath(location);
|
|
294
295
|
if (!await fileExistsAsync(absolutePath)) throw new Error(`File not found: ${absolutePath}`);
|
|
295
296
|
const stats = await stat(absolutePath);
|
|
296
|
-
|
|
297
|
-
this._metadata.set(location, {
|
|
297
|
+
return {
|
|
298
298
|
path: location,
|
|
299
|
-
name,
|
|
299
|
+
name: location.split("/").pop() || "",
|
|
300
300
|
size: stats.size,
|
|
301
301
|
isDirectory: stats.isDirectory(),
|
|
302
302
|
lastModified: stats.mtime,
|
|
303
303
|
mimeType: this.guessMimeType(location)
|
|
304
|
-
}
|
|
305
|
-
return this._metadata.get(location);
|
|
304
|
+
};
|
|
306
305
|
}
|
|
307
306
|
/**
|
|
308
307
|
* Get file size in bytes (shortcut for metadata().size)
|
|
@@ -319,12 +318,10 @@ var LocalDriver = class {
|
|
|
319
318
|
if (!await fileExistsAsync(fromPath)) throw new Error(`Source file not found: ${from}`);
|
|
320
319
|
await ensureDirectoryAsync(dirname(toPath));
|
|
321
320
|
await copyFile(fromPath, toPath);
|
|
322
|
-
this.
|
|
323
|
-
const fileBuffer = await readFile(toPath);
|
|
324
|
-
const hash = this.calculateHash(fileBuffer);
|
|
321
|
+
const hash = await this.hashFile(toPath);
|
|
325
322
|
const stats = await stat(toPath);
|
|
326
323
|
return {
|
|
327
|
-
path: to,
|
|
324
|
+
path: this.applyPrefix(to),
|
|
328
325
|
url: this.url(to),
|
|
329
326
|
size: stats.size,
|
|
330
327
|
hash,
|
|
@@ -341,13 +338,10 @@ var LocalDriver = class {
|
|
|
341
338
|
if (!await fileExistsAsync(fromPath)) throw new Error(`Source file not found: ${from}`);
|
|
342
339
|
await ensureDirectoryAsync(dirname(toPath));
|
|
343
340
|
await rename(fromPath, toPath);
|
|
344
|
-
this.
|
|
345
|
-
this._metadata.delete(to);
|
|
346
|
-
const fileBuffer = await readFile(toPath);
|
|
347
|
-
const hash = this.calculateHash(fileBuffer);
|
|
341
|
+
const hash = await this.hashFile(toPath);
|
|
348
342
|
const stats = await stat(toPath);
|
|
349
343
|
return {
|
|
350
|
-
path: to,
|
|
344
|
+
path: this.applyPrefix(to),
|
|
351
345
|
url: this.url(to),
|
|
352
346
|
size: stats.size,
|
|
353
347
|
hash,
|
|
@@ -403,14 +397,17 @@ var LocalDriver = class {
|
|
|
403
397
|
*/
|
|
404
398
|
getAbsolutePath(location) {
|
|
405
399
|
const prefixedLocation = this.applyPrefix(location);
|
|
406
|
-
|
|
400
|
+
const resolved = resolveWithinRoot(this.root, prefixedLocation);
|
|
401
|
+
const prefix = storageDriverContext.getPrefix() || this.options.prefix;
|
|
402
|
+
if (prefix) resolveWithinRoot(resolveWithinRoot(this.root, prefix.replace(/^[\\/]+|[\\/]+$/g, "")), resolved);
|
|
403
|
+
return resolved;
|
|
407
404
|
}
|
|
408
405
|
/**
|
|
409
406
|
* Convert various input types to Buffer
|
|
410
407
|
*/
|
|
411
408
|
async toBuffer(file) {
|
|
412
409
|
if (Buffer.isBuffer(file)) return file;
|
|
413
|
-
if (typeof file === "string") return
|
|
410
|
+
if (typeof file === "string") return Buffer.from(file);
|
|
414
411
|
return file.buffer();
|
|
415
412
|
}
|
|
416
413
|
/**
|
|
@@ -420,6 +417,14 @@ var LocalDriver = class {
|
|
|
420
417
|
return crypto.createHash("sha256").update(new Uint8Array(buffer)).digest("hex");
|
|
421
418
|
}
|
|
422
419
|
/**
|
|
420
|
+
* Calculate SHA-256 hash of a file by streaming it (constant memory)
|
|
421
|
+
*/
|
|
422
|
+
async hashFile(absolutePath) {
|
|
423
|
+
const hasher = crypto.createHash("sha256");
|
|
424
|
+
for await (const chunk of createReadStream(absolutePath)) hasher.update(chunk);
|
|
425
|
+
return hasher.digest("hex");
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
423
428
|
* Guess MIME type from file extension
|
|
424
429
|
*/
|
|
425
430
|
guessMimeType(location) {
|