@nestm/storage 0.1.0-alpha.2 → 0.1.0-alpha.3
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 +15 -0
- package/README.md +111 -13
- package/SECURITY.md +23 -0
- package/dist/files-sdk/files-sdk.driver.d.ts +36 -7
- package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
- package/dist/files-sdk/files-sdk.driver.js +141 -20
- package/dist/files-sdk/files-sdk.driver.js.map +1 -1
- package/dist/files-sdk/index.d.ts +1 -1
- package/dist/files-sdk/index.d.ts.map +1 -1
- package/dist/files-sdk/index.js.map +1 -1
- package/dist/files-sdk/s3/index.d.ts +14 -0
- package/dist/files-sdk/s3/index.d.ts.map +1 -0
- package/dist/files-sdk/s3/index.js +108 -0
- package/dist/files-sdk/s3/index.js.map +1 -0
- package/dist/gateway/index.d.ts +1 -1
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js.map +1 -1
- package/dist/gateway/storage-gateway.controller.d.ts +12 -11
- package/dist/gateway/storage-gateway.controller.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.controller.js +209 -65
- package/dist/gateway/storage-gateway.controller.js.map +1 -1
- package/dist/gateway/storage-gateway.module.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.module.js +60 -1
- package/dist/gateway/storage-gateway.module.js.map +1 -1
- package/dist/gateway/storage-gateway.tokens.d.ts +1 -0
- package/dist/gateway/storage-gateway.tokens.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.tokens.js +1 -0
- package/dist/gateway/storage-gateway.tokens.js.map +1 -1
- package/dist/gateway/storage-gateway.types.d.ts +43 -0
- package/dist/gateway/storage-gateway.types.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.types.js.map +1 -1
- package/dist/storage.client.d.ts +8 -1
- package/dist/storage.client.d.ts.map +1 -1
- package/dist/storage.client.js +40 -0
- package/dist/storage.client.js.map +1 -1
- package/dist/storage.driver.d.ts +6 -1
- package/dist/storage.driver.d.ts.map +1 -1
- package/dist/storage.driver.js.map +1 -1
- package/dist/storage.error.d.ts +3 -0
- package/dist/storage.error.d.ts.map +1 -1
- package/dist/storage.error.js +33 -1
- package/dist/storage.error.js.map +1 -1
- package/dist/storage.types.d.ts +39 -1
- package/dist/storage.types.d.ts.map +1 -1
- package/dist/storage.types.js.map +1 -1
- package/package.json +25 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @nestm/storage
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.3
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b947634: Harden storage integration boundaries with cross-copy-safe `StorageError`
|
|
8
|
+
detection, a package-owned S3 driver factory, conditional staged-object
|
|
9
|
+
promotion, and a mandatory parsed key policy plus signed-transfer limits for
|
|
10
|
+
the optional HTTP gateway.
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- b947634: Map structurally branded `files-sdk` errors, including errors wrapped across
|
|
15
|
+
duplicate package copies, so missing objects retain the `NOT_FOUND` storage
|
|
16
|
+
error code.
|
|
17
|
+
|
|
3
18
|
## 0.1.0-alpha.2
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -25,7 +25,15 @@ consumers do not download NestJS.
|
|
|
25
25
|
## Install
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
pnpm add @nestm/storage@alpha
|
|
28
|
+
pnpm add @nestm/storage@alpha
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The package-owned S3 factory needs no direct `files-sdk` import. Install the
|
|
32
|
+
pinned engine explicitly only when the application imports another adapter
|
|
33
|
+
such as `files-sdk/gcs`:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pnpm add files-sdk@2.2.2
|
|
29
37
|
```
|
|
30
38
|
|
|
31
39
|
Install only the native SDKs required by the chosen provider. For example:
|
|
@@ -82,14 +90,15 @@ runtime or declaration imports. Provider adapters remain available through
|
|
|
82
90
|
|
|
83
91
|
## Configure named stores
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
bridge, and register the
|
|
93
|
+
Use the package-owned S3 factory when applicable. For other providers, create a
|
|
94
|
+
`files-sdk` adapter, wrap it through the explicit bridge, and register the
|
|
95
|
+
resulting driver:
|
|
87
96
|
|
|
88
97
|
```ts
|
|
89
98
|
import { Module } from '@nestjs/common';
|
|
90
|
-
import { s3 } from 'files-sdk/s3';
|
|
91
99
|
import { gcs } from 'files-sdk/gcs';
|
|
92
100
|
import { createFilesSdkDriver } from '@nestm/storage/files-sdk';
|
|
101
|
+
import { createS3StorageDriver } from '@nestm/storage/files-sdk/s3';
|
|
93
102
|
import { StorageModule } from '@nestm/storage';
|
|
94
103
|
|
|
95
104
|
export const StorageKey = {
|
|
@@ -104,11 +113,11 @@ export const StorageKey = {
|
|
|
104
113
|
stores: [
|
|
105
114
|
{
|
|
106
115
|
name: StorageKey.MEDIA,
|
|
107
|
-
driver:
|
|
108
|
-
adapter:
|
|
116
|
+
driver: createS3StorageDriver({
|
|
117
|
+
adapter: {
|
|
109
118
|
bucket: 'media',
|
|
110
119
|
region: 'us-east-1',
|
|
111
|
-
}
|
|
120
|
+
},
|
|
112
121
|
}),
|
|
113
122
|
},
|
|
114
123
|
{
|
|
@@ -194,11 +203,11 @@ StorageModule.forRootAsync({
|
|
|
194
203
|
name: 'media',
|
|
195
204
|
inject: [ConfigService],
|
|
196
205
|
useFactory: (config: ConfigService) =>
|
|
197
|
-
|
|
198
|
-
adapter:
|
|
206
|
+
createS3StorageDriver({
|
|
207
|
+
adapter: {
|
|
199
208
|
bucket: config.getOrThrow('MEDIA_BUCKET'),
|
|
200
209
|
region: config.getOrThrow('AWS_REGION'),
|
|
201
|
-
}
|
|
210
|
+
},
|
|
202
211
|
}),
|
|
203
212
|
},
|
|
204
213
|
],
|
|
@@ -221,6 +230,7 @@ whitespace.
|
|
|
221
230
|
`StorageClient` exposes:
|
|
222
231
|
|
|
223
232
|
- `upload`, `downloadStream`, `head`, `exists`, `delete`, `copy`, and `move`;
|
|
233
|
+
- conditional staged-object `promote` when the driver advertises it;
|
|
224
234
|
- `list`, cursor-aware `listAll`, and lazy `search`;
|
|
225
235
|
- `signDownload` and discriminated PUT/POST `signUpload`;
|
|
226
236
|
- `uploadMany`, `downloadMany`, `headMany`, `existsMany`, and `deleteMany`;
|
|
@@ -250,6 +260,37 @@ Node `Readable` uploads are accepted and converted to Web streams without
|
|
|
250
260
|
buffering. Provider capability gaps fail closed with `StorageError` rather than
|
|
251
261
|
silently discarding a range, metadata, or cache-control request.
|
|
252
262
|
|
|
263
|
+
### Race-free staged-object promotion
|
|
264
|
+
|
|
265
|
+
The S3 bridge advertises ETag- and version-conditional server-side copy. This
|
|
266
|
+
lets an application validate a staged object and copy that exact source to its
|
|
267
|
+
final key instead of re-reading whichever bytes occupy the staging key later:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { StorageError, StorageErrorCode } from '@nestm/storage';
|
|
271
|
+
|
|
272
|
+
const staged = await media.head(stagingKey);
|
|
273
|
+
if (staged.etag === undefined) {
|
|
274
|
+
throw new StorageError('Provider did not return a source ETag.', {
|
|
275
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Validate size, declared MIME, and magic bytes before this call.
|
|
280
|
+
await media.file(stagingKey).promoteTo(finalKey, {
|
|
281
|
+
sourceEtag: staged.etag,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Commit ready metadata first. Promotion deliberately retains the staged
|
|
285
|
+
// object so a failed database commit remains recoverable.
|
|
286
|
+
await media.delete(stagingKey);
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`sourceVersion` can select an immutable S3 version and may be combined with
|
|
290
|
+
`sourceEtag`. A promotion without either identity is rejected. Drivers that do
|
|
291
|
+
not publish `capabilities.conditionalCopy` fail with `NOT_SUPPORTED` rather
|
|
292
|
+
than falling back to an unsafe ordinary copy.
|
|
293
|
+
|
|
253
294
|
### Resumable uploads
|
|
254
295
|
|
|
255
296
|
```ts
|
|
@@ -301,20 +342,42 @@ The gateway lives at `@nestm/storage/gateway` and is never mounted by
|
|
|
301
342
|
`StorageModule`.
|
|
302
343
|
|
|
303
344
|
```ts
|
|
304
|
-
import { Module } from '@nestjs/common';
|
|
345
|
+
import { Injectable, Module } from '@nestjs/common';
|
|
305
346
|
import {
|
|
306
347
|
StorageGatewayModule,
|
|
307
348
|
StorageGatewayOperation,
|
|
349
|
+
type StorageGatewayKeyPolicy,
|
|
308
350
|
} from '@nestm/storage/gateway';
|
|
309
351
|
|
|
352
|
+
@Injectable()
|
|
353
|
+
class TenantStorageKeyPolicy implements StorageGatewayKeyPolicy {
|
|
354
|
+
resolve({ input, request, target }) {
|
|
355
|
+
const tenantId = tenantIdFromAuthenticatedRequest(request);
|
|
356
|
+
const root = `tenants/${base64url(tenantId)}`;
|
|
357
|
+
if (target === 'pattern') {
|
|
358
|
+
// Search is already constrained by the separately resolved prefix.
|
|
359
|
+
return input?.value ?? '*';
|
|
360
|
+
}
|
|
361
|
+
return `${root}/${input?.value ?? ''}`;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@Module({
|
|
366
|
+
providers: [TenantStorageKeyPolicy],
|
|
367
|
+
exports: [TenantStorageKeyPolicy],
|
|
368
|
+
})
|
|
369
|
+
class StoragePolicyModule {}
|
|
370
|
+
|
|
310
371
|
@Module({
|
|
311
372
|
imports: [
|
|
312
373
|
AppStorageModule,
|
|
313
374
|
AuthModule,
|
|
375
|
+
StoragePolicyModule,
|
|
314
376
|
StorageGatewayModule.register({
|
|
315
|
-
imports: [AppStorageModule, AuthModule],
|
|
377
|
+
imports: [AppStorageModule, AuthModule, StoragePolicyModule],
|
|
316
378
|
store: 'media',
|
|
317
379
|
guards: [JwtAuthGuard],
|
|
380
|
+
keyPolicy: TenantStorageKeyPolicy,
|
|
318
381
|
mode: 'hybrid',
|
|
319
382
|
operations: [
|
|
320
383
|
StorageGatewayOperation.DOWNLOAD,
|
|
@@ -325,6 +388,9 @@ import {
|
|
|
325
388
|
StorageGatewayOperation.SIGN_UPLOAD,
|
|
326
389
|
],
|
|
327
390
|
maxUploadBytes: 100 * 1024 * 1024,
|
|
391
|
+
maxSignedUploadBytes: 10 * 1024 * 1024,
|
|
392
|
+
signedUploadContentTypes: ['image/jpeg', 'image/png'],
|
|
393
|
+
maxSignedUrlExpiresIn: 900,
|
|
328
394
|
maxListResults: 1000,
|
|
329
395
|
maxSearchResults: 1000,
|
|
330
396
|
proxyInlineContentTypes: ['image/jpeg', 'image/png'],
|
|
@@ -338,12 +404,39 @@ Registration fails without at least one existing Nest guard. The only bypass is
|
|
|
338
404
|
the explicit `allowUnauthenticated: true` development escape hatch. Operations
|
|
339
405
|
are deny-by-default and must be allowlisted individually.
|
|
340
406
|
|
|
407
|
+
Registration also fails without a `keyPolicy`. Guards answer whether a request
|
|
408
|
+
may reach the gateway; the key policy independently resolves every parsed
|
|
409
|
+
`key`, `prefix`, search `pattern`, `from`, and `to` value to the exact provider
|
|
410
|
+
path. It runs even when a list/search prefix was omitted, so the policy can
|
|
411
|
+
always impose a tenant root. Key-policy providers may be request scoped.
|
|
412
|
+
Returned paths are parsed again and reject absolute paths, backslashes, control
|
|
413
|
+
characters, empty segments, and dot/parent segments.
|
|
414
|
+
|
|
415
|
+
Existing single-tenant applications can temporarily set
|
|
416
|
+
`unsafeAllowUnscopedKeys: true` instead of `keyPolicy`. The name is intentional:
|
|
417
|
+
it preserves caller-controlled provider keys and must not be used on an exposed
|
|
418
|
+
or multi-tenant gateway. It cannot be combined with `keyPolicy`.
|
|
419
|
+
|
|
341
420
|
Proxy downloads default to `Content-Disposition: attachment` and always send
|
|
342
421
|
`X-Content-Type-Options: nosniff`. Add only trusted, non-active MIME types to
|
|
343
422
|
`proxyInlineContentTypes` when browser rendering is required. Search responses
|
|
344
423
|
are capped by `maxSearchResults`, and list pages by `maxListResults` (both
|
|
345
424
|
1,000 by default).
|
|
346
425
|
|
|
426
|
+
Every signed URL is capped by `maxSignedUrlExpiresIn` (3,600 seconds by
|
|
427
|
+
default). Signed uploads always carry a provider-enforced maximum size, capped
|
|
428
|
+
by `maxSignedUploadBytes`, and require an exact lowercase MIME type from
|
|
429
|
+
`signedUploadContentTypes`. The default direct-upload allowlist contains only
|
|
430
|
+
`application/octet-stream`. Gateway callers may request only the literal
|
|
431
|
+
`attachment` or `inline` response disposition; arbitrary response-header text
|
|
432
|
+
and filenames are rejected. A driver must also advertise
|
|
433
|
+
`signedUploadPolicy.contentType` and `signedUploadPolicy.sizeRange`; otherwise
|
|
434
|
+
the gateway refuses to mint the URL. `createS3StorageDriver()` advertises both
|
|
435
|
+
and uses S3 POST policy conditions. Signed downloads similarly require
|
|
436
|
+
`signedDownloadPolicy.expiresIn`. The S3 factory advertises it only when no
|
|
437
|
+
permanent `publicBaseUrl` was configured, preventing a configured TTL from
|
|
438
|
+
silently returning a non-expiring public link.
|
|
439
|
+
|
|
347
440
|
The fixed gateway prefix is `/storage`:
|
|
348
441
|
|
|
349
442
|
| Method | Path | Operation |
|
|
@@ -394,9 +487,14 @@ try {
|
|
|
394
487
|
}
|
|
395
488
|
```
|
|
396
489
|
|
|
490
|
+
`files-sdk` `NotFound` failures retain `StorageErrorCode.NOT_FOUND`, including
|
|
491
|
+
when a provider adapter and this driver resolve separate copies of `files-sdk`.
|
|
492
|
+
`isStorageError()` likewise recognizes branded and exact legacy structural
|
|
493
|
+
errors produced by a duplicated `@nestm/storage` package copy.
|
|
494
|
+
|
|
397
495
|
Capability flags cover range reads, native byte-level upload progress,
|
|
398
496
|
delimiter listing, metadata, cache control, resumable uploads, server-side
|
|
399
|
-
copy, and signed transfers.
|
|
497
|
+
copy, conditional promotion, and signed transfers.
|
|
400
498
|
Provider-specific native clients are intentionally not exposed from the root
|
|
401
499
|
package.
|
|
402
500
|
|
package/SECURITY.md
CHANGED
|
@@ -23,3 +23,26 @@ cross-store pruning, or dependency compromise are especially useful.
|
|
|
23
23
|
For a vulnerability originating in NestJS, `files-sdk`, or a provider SDK,
|
|
24
24
|
follow that project's security policy as well. You may still report it here
|
|
25
25
|
privately when this integration needs a mitigation.
|
|
26
|
+
|
|
27
|
+
## Gateway security boundary
|
|
28
|
+
|
|
29
|
+
Mounting the optional HTTP gateway requires both authentication guards and a
|
|
30
|
+
`StorageGatewayKeyPolicy`. The policy receives parsed paths and must derive the
|
|
31
|
+
provider key space from trusted request context. Do not treat a client prefix
|
|
32
|
+
or object key as a tenant identifier. `unsafeAllowUnscopedKeys` exists only to
|
|
33
|
+
migrate trusted single-tenant deployments and is unsafe on an exposed gateway.
|
|
34
|
+
|
|
35
|
+
Signed URL expiry, signed-upload byte size, and signed-upload MIME are bounded
|
|
36
|
+
by module configuration and cannot be increased by a request. Only `inline`
|
|
37
|
+
and `attachment` content dispositions are accepted. File type still must be
|
|
38
|
+
verified from magic bytes after upload; a signed MIME condition proves only
|
|
39
|
+
what header the uploader supplied. The gateway rejects signed-upload drivers
|
|
40
|
+
that do not advertise provider-enforced content-type and size-range policies.
|
|
41
|
+
It also rejects download adapters that cannot guarantee the requested expiry;
|
|
42
|
+
in particular, the S3 bridge does not treat `publicBaseUrl` links as expiring.
|
|
43
|
+
|
|
44
|
+
For staged uploads on S3, use `promote`/`promoteTo` with the ETag returned by
|
|
45
|
+
the validated `head` or with an immutable provider version. Ordinary `copy`
|
|
46
|
+
does not protect against a staging-key replay between validation and copy.
|
|
47
|
+
Conditional promotion keeps the staging source; delete it only after the
|
|
48
|
+
application's metadata transaction commits.
|
|
@@ -1,24 +1,52 @@
|
|
|
1
1
|
import { type Adapter, type FilesOptions } from 'files-sdk';
|
|
2
|
+
import { StorageError } from '../storage.error.js';
|
|
2
3
|
import type { StorageDriver } from '../storage.driver.js';
|
|
3
|
-
import type { StorageBody, StorageDownloadOptions, StorageListOptions, StorageListResult, StorageObject, StorageObjectMetadata, StorageOperationOptions, StorageSearchOptions, StorageSignedDownloadOptions, StorageSignedUpload, StorageSignedUploadOptions, StorageUploadOptions, StorageUploadResult } from '../storage.types.js';
|
|
4
|
+
import type { StorageBody, StorageDownloadOptions, StorageListOptions, StorageListResult, StorageObject, StorageObjectMetadata, StorageOperationOptions, StorageConditionalCopyCapability, StoragePromotionOptions, StorageSearchOptions, StorageSignedUploadPolicyCapability, StorageSignedDownloadPolicyCapability, StorageSignedDownloadOptions, StorageSignedUpload, StorageSignedUploadOptions, StorageUploadOptions, StorageUploadResult } from '../storage.types.js';
|
|
4
5
|
export type FilesSdkDriverOptions<AdapterType extends Adapter> = FilesOptions<AdapterType>;
|
|
6
|
+
/**
|
|
7
|
+
* Optional adapter extension for providers that can conditionally copy an
|
|
8
|
+
* immutable source identity. Plain files-sdk adapters remain fully supported.
|
|
9
|
+
*/
|
|
10
|
+
export interface FilesSdkConditionalCopyAdapter {
|
|
11
|
+
readonly conditionalCopy: StorageConditionalCopyCapability;
|
|
12
|
+
promote(sourceKey: string, destinationKey: string, options: StoragePromotionOptions): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface FilesSdkSignedUploadPolicyAdapter {
|
|
15
|
+
readonly signedUploadPolicy: StorageSignedUploadPolicyCapability;
|
|
16
|
+
}
|
|
17
|
+
export interface FilesSdkSignedDownloadPolicyAdapter {
|
|
18
|
+
readonly signedDownloadPolicy: StorageSignedDownloadPolicyCapability;
|
|
19
|
+
}
|
|
20
|
+
export declare function mapFilesSdkError(error: unknown): StorageError;
|
|
5
21
|
export declare class FilesSdkStorageDriver<AdapterType extends Adapter = Adapter> implements StorageDriver {
|
|
6
22
|
#private;
|
|
7
23
|
constructor(options: FilesSdkDriverOptions<AdapterType>);
|
|
8
24
|
get name(): string;
|
|
9
25
|
get capabilities(): {
|
|
26
|
+
nativeUploadProgress: boolean;
|
|
27
|
+
signedUploadPolicy?: {
|
|
28
|
+
contentType: boolean;
|
|
29
|
+
sizeRange: boolean;
|
|
30
|
+
};
|
|
31
|
+
signedUpload: "runtime";
|
|
32
|
+
signedDownloadPolicy?: {
|
|
33
|
+
expiresIn: boolean;
|
|
34
|
+
};
|
|
35
|
+
signedDownload: {
|
|
36
|
+
supported: boolean;
|
|
37
|
+
maxExpiresIn?: number;
|
|
38
|
+
};
|
|
39
|
+
conditionalCopy?: {
|
|
40
|
+
supported: boolean;
|
|
41
|
+
etag: boolean;
|
|
42
|
+
version: boolean;
|
|
43
|
+
};
|
|
10
44
|
cacheControl: boolean;
|
|
11
45
|
delimiter: boolean;
|
|
12
46
|
metadata: boolean;
|
|
13
47
|
rangeRead: boolean;
|
|
14
48
|
resumableUpload: boolean;
|
|
15
49
|
serverSideCopy: boolean;
|
|
16
|
-
signedDownload: {
|
|
17
|
-
supported: boolean;
|
|
18
|
-
maxExpiresIn?: number;
|
|
19
|
-
};
|
|
20
|
-
signedUpload: "runtime";
|
|
21
|
-
nativeUploadProgress: boolean;
|
|
22
50
|
};
|
|
23
51
|
upload(key: string, body: StorageBody, options?: StorageUploadOptions): Promise<StorageUploadResult>;
|
|
24
52
|
download(key: string, options?: StorageDownloadOptions): Promise<StorageObject>;
|
|
@@ -27,6 +55,7 @@ export declare class FilesSdkStorageDriver<AdapterType extends Adapter = Adapter
|
|
|
27
55
|
delete(key: string, options?: StorageOperationOptions): Promise<void>;
|
|
28
56
|
copy(sourceKey: string, destinationKey: string, options?: StorageOperationOptions): Promise<void>;
|
|
29
57
|
move(sourceKey: string, destinationKey: string, options?: StorageOperationOptions): Promise<void>;
|
|
58
|
+
promote(sourceKey: string, destinationKey: string, options: StoragePromotionOptions): Promise<void>;
|
|
30
59
|
list(options?: StorageListOptions): Promise<StorageListResult>;
|
|
31
60
|
search(pattern: string | RegExp, options?: StorageSearchOptions): AsyncIterable<StorageObjectMetadata>;
|
|
32
61
|
signDownload(key: string, options?: StorageSignedDownloadOptions): Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files-sdk.driver.d.ts","sourceRoot":"","sources":["../../src/files-sdk/files-sdk.driver.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,OAAO,EAGZ,KAAK,YAAY,EAUlB,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"files-sdk.driver.d.ts","sourceRoot":"","sources":["../../src/files-sdk/files-sdk.driver.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,29 +1,121 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
2
|
import { Files, FilesError, } from 'files-sdk';
|
|
3
|
-
import { StorageError, StorageErrorCode } from '../storage.error.js';
|
|
3
|
+
import { StorageError, StorageErrorCode, isStorageError, } from '../storage.error.js';
|
|
4
4
|
import { getFilesSdkUploadControl } from '../storage-upload-control.js';
|
|
5
|
-
function
|
|
6
|
-
if (
|
|
5
|
+
function conditionalCopyAdapterOf(adapter) {
|
|
6
|
+
if (typeof adapter !== 'object' ||
|
|
7
|
+
adapter === null ||
|
|
8
|
+
!('conditionalCopy' in adapter) ||
|
|
9
|
+
!('promote' in adapter)) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const candidate = adapter;
|
|
13
|
+
const capability = candidate.conditionalCopy;
|
|
14
|
+
if (capability === undefined ||
|
|
15
|
+
typeof capability.supported !== 'boolean' ||
|
|
16
|
+
typeof capability.etag !== 'boolean' ||
|
|
17
|
+
typeof capability.version !== 'boolean' ||
|
|
18
|
+
typeof candidate.promote !== 'function') {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
function signedUploadPolicyAdapterOf(adapter) {
|
|
24
|
+
if (typeof adapter !== 'object' ||
|
|
25
|
+
adapter === null ||
|
|
26
|
+
!('signedUploadPolicy' in adapter)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const candidate = adapter;
|
|
30
|
+
const capability = candidate.signedUploadPolicy;
|
|
31
|
+
if (capability === undefined ||
|
|
32
|
+
typeof capability.contentType !== 'boolean' ||
|
|
33
|
+
typeof capability.sizeRange !== 'boolean') {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return candidate;
|
|
37
|
+
}
|
|
38
|
+
function signedDownloadPolicyAdapterOf(adapter) {
|
|
39
|
+
if (typeof adapter !== 'object' ||
|
|
40
|
+
adapter === null ||
|
|
41
|
+
!('signedDownloadPolicy' in adapter)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const candidate = adapter;
|
|
45
|
+
const capability = candidate.signedDownloadPolicy;
|
|
46
|
+
if (capability === undefined || typeof capability.expiresIn !== 'boolean') {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
51
|
+
function isFilesErrorCode(value) {
|
|
52
|
+
return (value === 'NotFound' ||
|
|
53
|
+
value === 'Unauthorized' ||
|
|
54
|
+
value === 'Conflict' ||
|
|
55
|
+
value === 'ReadOnly' ||
|
|
56
|
+
value === 'Provider');
|
|
57
|
+
}
|
|
58
|
+
function isFilesErrorLike(error) {
|
|
59
|
+
if (error instanceof FilesError) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (!(error instanceof Error)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return (error.name === 'FilesError' &&
|
|
67
|
+
typeof error.message === 'string' &&
|
|
68
|
+
'code' in error &&
|
|
69
|
+
isFilesErrorCode(error.code) &&
|
|
70
|
+
'aborted' in error &&
|
|
71
|
+
typeof error.aborted === 'boolean' &&
|
|
72
|
+
'timedOut' in error &&
|
|
73
|
+
typeof error.timedOut === 'boolean' &&
|
|
74
|
+
'permanent' in error &&
|
|
75
|
+
typeof error.permanent === 'boolean');
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function unwrapFilesError(error) {
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
let current = error;
|
|
84
|
+
while (current.code === 'Provider' &&
|
|
85
|
+
!current.aborted &&
|
|
86
|
+
!current.timedOut &&
|
|
87
|
+
!current.permanent &&
|
|
88
|
+
isFilesErrorLike(current.cause) &&
|
|
89
|
+
current.message === current.cause.message &&
|
|
90
|
+
!seen.has(current.cause)) {
|
|
91
|
+
seen.add(current);
|
|
92
|
+
current = current.cause;
|
|
93
|
+
}
|
|
94
|
+
return current;
|
|
95
|
+
}
|
|
96
|
+
export function mapFilesSdkError(error) {
|
|
97
|
+
if (isStorageError(error)) {
|
|
7
98
|
return error;
|
|
8
99
|
}
|
|
9
|
-
if (!(error
|
|
100
|
+
if (!isFilesErrorLike(error)) {
|
|
10
101
|
return new StorageError(error instanceof Error ? error.message : String(error), {
|
|
11
102
|
cause: error,
|
|
12
103
|
code: StorageErrorCode.PROVIDER,
|
|
13
104
|
});
|
|
14
105
|
}
|
|
15
|
-
|
|
16
|
-
|
|
106
|
+
const filesError = unwrapFilesError(error);
|
|
107
|
+
if (isStorageError(filesError.cause)) {
|
|
108
|
+
return filesError.cause;
|
|
17
109
|
}
|
|
18
110
|
let code;
|
|
19
|
-
if (
|
|
111
|
+
if (filesError.timedOut) {
|
|
20
112
|
code = StorageErrorCode.TIMEOUT;
|
|
21
113
|
}
|
|
22
|
-
else if (
|
|
114
|
+
else if (filesError.aborted) {
|
|
23
115
|
code = StorageErrorCode.ABORTED;
|
|
24
116
|
}
|
|
25
117
|
else {
|
|
26
|
-
switch (
|
|
118
|
+
switch (filesError.code) {
|
|
27
119
|
case 'NotFound':
|
|
28
120
|
code = StorageErrorCode.NOT_FOUND;
|
|
29
121
|
break;
|
|
@@ -37,18 +129,18 @@ function mapFilesError(error) {
|
|
|
37
129
|
code = StorageErrorCode.READ_ONLY;
|
|
38
130
|
break;
|
|
39
131
|
case 'Provider':
|
|
40
|
-
code = /(?:not supported|does not support|unsupported)/iu.test(
|
|
132
|
+
code = /(?:not supported|does not support|unsupported)/iu.test(filesError.message)
|
|
41
133
|
? StorageErrorCode.NOT_SUPPORTED
|
|
42
134
|
: StorageErrorCode.PROVIDER;
|
|
43
135
|
break;
|
|
44
136
|
}
|
|
45
137
|
}
|
|
46
|
-
return new StorageError(
|
|
47
|
-
aborted:
|
|
48
|
-
cause:
|
|
138
|
+
return new StorageError(filesError.message, {
|
|
139
|
+
aborted: filesError.aborted,
|
|
140
|
+
cause: filesError.cause ?? error,
|
|
49
141
|
code,
|
|
50
|
-
permanent:
|
|
51
|
-
timedOut:
|
|
142
|
+
permanent: filesError.permanent,
|
|
143
|
+
timedOut: filesError.timedOut,
|
|
52
144
|
});
|
|
53
145
|
}
|
|
54
146
|
function mapRetryOptions(retries) {
|
|
@@ -60,7 +152,7 @@ function mapRetryOptions(retries) {
|
|
|
60
152
|
...(retries.backoff !== undefined && {
|
|
61
153
|
backoff: ({ attempt, error }) => retries.backoff?.({
|
|
62
154
|
attempt,
|
|
63
|
-
error:
|
|
155
|
+
error: mapFilesSdkError(error),
|
|
64
156
|
}) ?? 0,
|
|
65
157
|
}),
|
|
66
158
|
};
|
|
@@ -114,7 +206,7 @@ function normalizeDownloadStream(source) {
|
|
|
114
206
|
await activeReader.cancel(reason);
|
|
115
207
|
}
|
|
116
208
|
catch (error) {
|
|
117
|
-
throw
|
|
209
|
+
throw mapFilesSdkError(error);
|
|
118
210
|
}
|
|
119
211
|
finally {
|
|
120
212
|
release();
|
|
@@ -137,7 +229,7 @@ function normalizeDownloadStream(source) {
|
|
|
137
229
|
}
|
|
138
230
|
catch (error) {
|
|
139
231
|
release();
|
|
140
|
-
controller.error(
|
|
232
|
+
controller.error(mapFilesSdkError(error));
|
|
141
233
|
}
|
|
142
234
|
},
|
|
143
235
|
});
|
|
@@ -256,9 +348,15 @@ function uploadResultOf(result) {
|
|
|
256
348
|
export class FilesSdkStorageDriver {
|
|
257
349
|
#files;
|
|
258
350
|
#name;
|
|
351
|
+
#conditionalCopy;
|
|
352
|
+
#signedUploadPolicy;
|
|
353
|
+
#signedDownloadPolicy;
|
|
259
354
|
constructor(options) {
|
|
260
355
|
this.#files = new Files(options);
|
|
261
356
|
this.#name = options.adapter.name;
|
|
357
|
+
this.#conditionalCopy = conditionalCopyAdapterOf(options.adapter);
|
|
358
|
+
this.#signedUploadPolicy = signedUploadPolicyAdapterOf(options.adapter);
|
|
359
|
+
this.#signedDownloadPolicy = signedDownloadPolicyAdapterOf(options.adapter);
|
|
262
360
|
}
|
|
263
361
|
get name() {
|
|
264
362
|
return this.#name;
|
|
@@ -272,8 +370,19 @@ export class FilesSdkStorageDriver {
|
|
|
272
370
|
rangeRead: capabilities.rangeRead,
|
|
273
371
|
resumableUpload: capabilities.multipart,
|
|
274
372
|
serverSideCopy: capabilities.serverSideCopy,
|
|
373
|
+
...(this.#conditionalCopy !== undefined && {
|
|
374
|
+
conditionalCopy: { ...this.#conditionalCopy.conditionalCopy },
|
|
375
|
+
}),
|
|
275
376
|
signedDownload: { ...capabilities.signedUrl },
|
|
377
|
+
...(this.#signedDownloadPolicy !== undefined && {
|
|
378
|
+
signedDownloadPolicy: {
|
|
379
|
+
...this.#signedDownloadPolicy.signedDownloadPolicy,
|
|
380
|
+
},
|
|
381
|
+
}),
|
|
276
382
|
signedUpload: 'runtime',
|
|
383
|
+
...(this.#signedUploadPolicy !== undefined && {
|
|
384
|
+
signedUploadPolicy: { ...this.#signedUploadPolicy.signedUploadPolicy },
|
|
385
|
+
}),
|
|
277
386
|
nativeUploadProgress: capabilities.uploadProgress,
|
|
278
387
|
};
|
|
279
388
|
}
|
|
@@ -306,6 +415,18 @@ export class FilesSdkStorageDriver {
|
|
|
306
415
|
move(sourceKey, destinationKey, options) {
|
|
307
416
|
return this.#call(() => this.#files.move(sourceKey, destinationKey, operationOptions(options)));
|
|
308
417
|
}
|
|
418
|
+
promote(sourceKey, destinationKey, options) {
|
|
419
|
+
const adapter = this.#conditionalCopy;
|
|
420
|
+
if (adapter === undefined || !adapter.conditionalCopy.supported) {
|
|
421
|
+
return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional promotion.`, {
|
|
422
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
423
|
+
key: sourceKey,
|
|
424
|
+
operation: 'promote',
|
|
425
|
+
permanent: true,
|
|
426
|
+
}));
|
|
427
|
+
}
|
|
428
|
+
return this.#call(() => adapter.promote(sourceKey, destinationKey, options));
|
|
429
|
+
}
|
|
309
430
|
async list(options) {
|
|
310
431
|
return this.#call(async () => {
|
|
311
432
|
const result = await this.#files.list(listOptions(options));
|
|
@@ -328,7 +449,7 @@ export class FilesSdkStorageDriver {
|
|
|
328
449
|
}
|
|
329
450
|
}
|
|
330
451
|
catch (error) {
|
|
331
|
-
throw
|
|
452
|
+
throw mapFilesSdkError(error);
|
|
332
453
|
}
|
|
333
454
|
}
|
|
334
455
|
signDownload(key, options) {
|
|
@@ -342,7 +463,7 @@ export class FilesSdkStorageDriver {
|
|
|
342
463
|
return await operation();
|
|
343
464
|
}
|
|
344
465
|
catch (error) {
|
|
345
|
-
throw
|
|
466
|
+
throw mapFilesSdkError(error);
|
|
346
467
|
}
|
|
347
468
|
}
|
|
348
469
|
}
|