@nestm/storage 0.1.0-alpha.7 → 0.1.0-alpha.8
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 +110 -0
- package/README.md +217 -18
- package/SECURITY.md +34 -5
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +14 -3
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts.map +1 -1
- package/dist/ai-sdk/ai-sdk-workspace-tools.js +54 -23
- package/dist/ai-sdk/ai-sdk-workspace-tools.js.map +1 -1
- package/dist/ai-sdk/index.d.ts +1 -1
- package/dist/ai-sdk/index.d.ts.map +1 -1
- package/dist/ai-sdk/index.js.map +1 -1
- package/dist/files-sdk/files-sdk.driver.d.ts +54 -10
- package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
- package/dist/files-sdk/files-sdk.driver.js +784 -63
- package/dist/files-sdk/files-sdk.driver.js.map +1 -1
- package/dist/files-sdk/fs/index.d.ts +4 -4
- package/dist/files-sdk/fs/index.d.ts.map +1 -1
- package/dist/files-sdk/fs/index.js +242 -9
- package/dist/files-sdk/fs/index.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/provider/index.d.ts +9 -5
- package/dist/files-sdk/provider/index.d.ts.map +1 -1
- package/dist/files-sdk/provider/index.js +53 -15
- package/dist/files-sdk/provider/index.js.map +1 -1
- package/dist/files-sdk/s3/construction-metadata.d.ts +7 -0
- package/dist/files-sdk/s3/construction-metadata.d.ts.map +1 -0
- package/dist/files-sdk/s3/construction-metadata.js +13 -0
- package/dist/files-sdk/s3/construction-metadata.js.map +1 -0
- package/dist/files-sdk/s3/index.d.ts +39 -19
- package/dist/files-sdk/s3/index.d.ts.map +1 -1
- package/dist/files-sdk/s3/index.js +936 -113
- package/dist/files-sdk/s3/index.js.map +1 -1
- package/dist/gateway/storage-gateway.controller.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.controller.js +25 -5
- package/dist/gateway/storage-gateway.controller.js.map +1 -1
- package/dist/storage-etag.d.ts +13 -0
- package/dist/storage-etag.d.ts.map +1 -0
- package/dist/storage-etag.js +31 -0
- package/dist/storage-etag.js.map +1 -0
- package/dist/storage.client.d.ts +3 -1
- package/dist/storage.client.d.ts.map +1 -1
- package/dist/storage.client.js +87 -22
- package/dist/storage.client.js.map +1 -1
- package/dist/storage.driver.d.ts +21 -1
- package/dist/storage.driver.d.ts.map +1 -1
- package/dist/storage.driver.js.map +1 -1
- package/dist/storage.types.d.ts +90 -19
- package/dist/storage.types.d.ts.map +1 -1
- package/dist/storage.types.js.map +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/provider-conformance.d.ts +65 -0
- package/dist/testing/provider-conformance.d.ts.map +1 -0
- package/dist/testing/provider-conformance.js +853 -0
- package/dist/testing/provider-conformance.js.map +1 -0
- package/dist/workspace/index.d.ts +1 -0
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +1 -0
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/storage-workspace.cursor.d.ts +68 -3
- package/dist/workspace/storage-workspace.cursor.d.ts.map +1 -1
- package/dist/workspace/storage-workspace.cursor.js +402 -39
- package/dist/workspace/storage-workspace.cursor.js.map +1 -1
- package/dist/workspace/storage-workspace.d.ts.map +1 -1
- package/dist/workspace/storage-workspace.js +54 -46
- package/dist/workspace/storage-workspace.js.map +1 -1
- package/dist/workspace/storage-workspace.types.d.ts +10 -2
- package/dist/workspace/storage-workspace.types.d.ts.map +1 -1
- package/dist/workspace/storage-workspace.types.js +1 -0
- package/dist/workspace/storage-workspace.types.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,46 +1,312 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
|
-
import { Files, FilesError, } from 'files-sdk';
|
|
2
|
+
import { Files, FilesError, handlers, } from 'files-sdk';
|
|
3
3
|
import { StorageError, StorageErrorCode, isStorageError, } from '../storage.error.js';
|
|
4
|
+
import { isCanonicalStorageEtag, normalizeProviderStorageEtag, } from '../storage-etag.js';
|
|
4
5
|
import { getFilesSdkUploadControl } from '../storage-upload-control.js';
|
|
6
|
+
const FILES_SDK_S3_RESERVED_EXTENSION_KEYS = [
|
|
7
|
+
'conditionalCopyDestination',
|
|
8
|
+
'conditionalCopySource',
|
|
9
|
+
'conditionalCreate',
|
|
10
|
+
'conditionalDelete',
|
|
11
|
+
'conditionalMultipartCompletion',
|
|
12
|
+
'conditionalRead',
|
|
13
|
+
'conditionalReplace',
|
|
14
|
+
'deleteConditional',
|
|
15
|
+
'downloadConditional',
|
|
16
|
+
'physicalKey',
|
|
17
|
+
'promote',
|
|
18
|
+
'signedDownloadPolicy',
|
|
19
|
+
'signedUploadPolicy',
|
|
20
|
+
'uploadConditional',
|
|
21
|
+
];
|
|
22
|
+
const FILES_SDK_S3_AUTHORITY_KEYS = [
|
|
23
|
+
...FILES_SDK_S3_RESERVED_EXTENSION_KEYS,
|
|
24
|
+
'bucket',
|
|
25
|
+
'copy',
|
|
26
|
+
'delete',
|
|
27
|
+
'deleteMany',
|
|
28
|
+
'download',
|
|
29
|
+
'exists',
|
|
30
|
+
'head',
|
|
31
|
+
'list',
|
|
32
|
+
'move',
|
|
33
|
+
'reportsUploadProgress',
|
|
34
|
+
'resumableUpload',
|
|
35
|
+
'signedUploadUrl',
|
|
36
|
+
'signedUrl',
|
|
37
|
+
'supportsCacheControl',
|
|
38
|
+
'supportsDelimiter',
|
|
39
|
+
'supportsMetadata',
|
|
40
|
+
'supportsRange',
|
|
41
|
+
'supportsServerSideCopy',
|
|
42
|
+
'upload',
|
|
43
|
+
'url',
|
|
44
|
+
];
|
|
45
|
+
const s3AdapterProvenance = new WeakMap();
|
|
46
|
+
const undecoratedS3Adapters = new WeakSet();
|
|
47
|
+
const s3AdapterAuthority = new WeakMap();
|
|
48
|
+
const s3MethodAuthority = new WeakMap();
|
|
49
|
+
const s3AuthorityState = new WeakMap();
|
|
50
|
+
const conflictingS3Authority = Symbol('conflictingS3Authority');
|
|
51
|
+
function adapterRawObject(adapter) {
|
|
52
|
+
try {
|
|
53
|
+
const raw = adapter.raw;
|
|
54
|
+
return (typeof raw === 'object' && raw !== null) ||
|
|
55
|
+
typeof raw === 'function'
|
|
56
|
+
? raw
|
|
57
|
+
: undefined;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function s3AdapterAuthorityTokenOf(adapter) {
|
|
64
|
+
let resolved = s3AdapterAuthority.get(adapter);
|
|
65
|
+
const consider = (token) => {
|
|
66
|
+
if (token === undefined)
|
|
67
|
+
return true;
|
|
68
|
+
if (resolved !== undefined && resolved !== token)
|
|
69
|
+
return false;
|
|
70
|
+
resolved = token;
|
|
71
|
+
return true;
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
const candidate = adapter;
|
|
75
|
+
for (const key of FILES_SDK_S3_AUTHORITY_KEYS) {
|
|
76
|
+
if (!(key in candidate))
|
|
77
|
+
continue;
|
|
78
|
+
const value = candidate[key];
|
|
79
|
+
if (typeof value === 'function' &&
|
|
80
|
+
!consider(s3MethodAuthority.get(value))) {
|
|
81
|
+
return conflictingS3Authority;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return resolved;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return conflictingS3Authority;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function bindS3AdapterAuthorityMethods(adapter, token) {
|
|
91
|
+
const candidate = adapter;
|
|
92
|
+
for (const key of FILES_SDK_S3_AUTHORITY_KEYS) {
|
|
93
|
+
if (!(key in candidate))
|
|
94
|
+
continue;
|
|
95
|
+
const value = candidate[key];
|
|
96
|
+
if (typeof value !== 'function')
|
|
97
|
+
continue;
|
|
98
|
+
const existing = s3MethodAuthority.get(value);
|
|
99
|
+
if (existing !== undefined && existing !== token) {
|
|
100
|
+
throw new TypeError('S3 adapter methods have conflicting authority.');
|
|
101
|
+
}
|
|
102
|
+
s3MethodAuthority.set(value, token);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function attachS3AdapterAuthority(adapter, raw) {
|
|
106
|
+
const existing = s3AdapterAuthorityTokenOf(adapter);
|
|
107
|
+
if (existing === conflictingS3Authority) {
|
|
108
|
+
throw new TypeError('S3 adapter authority is inconsistent.');
|
|
109
|
+
}
|
|
110
|
+
const token = existing ?? Object.freeze({});
|
|
111
|
+
const state = s3AuthorityState.get(token);
|
|
112
|
+
if (state !== undefined && state.raw !== raw) {
|
|
113
|
+
throw new TypeError('S3 adapter authority raw client cannot be changed.');
|
|
114
|
+
}
|
|
115
|
+
if (state === undefined)
|
|
116
|
+
s3AuthorityState.set(token, { raw });
|
|
117
|
+
s3AdapterAuthority.set(adapter, token);
|
|
118
|
+
bindS3AdapterAuthorityMethods(adapter, token);
|
|
119
|
+
return token;
|
|
120
|
+
}
|
|
121
|
+
function setS3AdapterProvenance(target, adapter, provenance) {
|
|
122
|
+
const existing = s3AdapterProvenanceOfRaw(target);
|
|
123
|
+
if (existing !== undefined) {
|
|
124
|
+
if (existing.provenance !== provenance ||
|
|
125
|
+
mismatchedS3Authority(adapter, existing.surface) !== undefined) {
|
|
126
|
+
throw new TypeError('S3 adapter provenance cannot be changed.');
|
|
127
|
+
}
|
|
128
|
+
s3AdapterProvenance.set(target, existing);
|
|
129
|
+
return existing;
|
|
130
|
+
}
|
|
131
|
+
const record = Object.freeze({
|
|
132
|
+
provenance,
|
|
133
|
+
surface: s3AuthoritySnapshot(adapter),
|
|
134
|
+
});
|
|
135
|
+
s3AdapterProvenance.set(target, record);
|
|
136
|
+
return record;
|
|
137
|
+
}
|
|
138
|
+
function s3AdapterProvenanceOfRaw(raw) {
|
|
139
|
+
return s3AdapterProvenance.get(raw);
|
|
140
|
+
}
|
|
141
|
+
function s3AuthoritySnapshot(adapter) {
|
|
142
|
+
const candidate = adapter;
|
|
143
|
+
return Object.freeze(Object.fromEntries(FILES_SDK_S3_AUTHORITY_KEYS.map((key) => {
|
|
144
|
+
const present = key in candidate;
|
|
145
|
+
return [
|
|
146
|
+
key,
|
|
147
|
+
Object.freeze({
|
|
148
|
+
present,
|
|
149
|
+
value: present ? candidate[key] : undefined,
|
|
150
|
+
}),
|
|
151
|
+
];
|
|
152
|
+
})));
|
|
153
|
+
}
|
|
154
|
+
function mismatchedS3Authority(adapter, snapshot) {
|
|
155
|
+
const candidate = adapter;
|
|
156
|
+
try {
|
|
157
|
+
return FILES_SDK_S3_AUTHORITY_KEYS.find((key) => {
|
|
158
|
+
const present = key in candidate;
|
|
159
|
+
const expected = snapshot[key];
|
|
160
|
+
return (present !== expected.present ||
|
|
161
|
+
!Object.is(present ? candidate[key] : undefined, expected.value));
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return FILES_SDK_S3_AUTHORITY_KEYS[0];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Rejects adapters already carrying reserved storage S3 extensions. */
|
|
169
|
+
export function assertFilesSdkS3AdapterHasNoReservedExtensions(adapter) {
|
|
170
|
+
const reserved = filesSdkS3ReservedExtensionOf(adapter);
|
|
171
|
+
if (reserved !== undefined) {
|
|
172
|
+
throw new TypeError(`S3 adapter already defines reserved extension "${reserved}".`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function filesSdkS3ReservedExtensionOf(adapter) {
|
|
176
|
+
const candidate = adapter;
|
|
177
|
+
try {
|
|
178
|
+
return FILES_SDK_S3_RESERVED_EXTENSION_KEYS.find((key) => key in candidate);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return FILES_SDK_S3_RESERVED_EXTENSION_KEYS[0];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Records that an S3 adapter was constructed but not provider-decorated. */
|
|
185
|
+
export function markFilesSdkS3AdapterUndecorated(adapter) {
|
|
186
|
+
const raw = adapterRawObject(adapter);
|
|
187
|
+
if (raw === undefined) {
|
|
188
|
+
throw new TypeError('S3 adapter must expose an object raw client.');
|
|
189
|
+
}
|
|
190
|
+
attachS3AdapterAuthority(adapter, raw);
|
|
191
|
+
undecoratedS3Adapters.add(raw);
|
|
192
|
+
}
|
|
193
|
+
/** Records the provider verification state of one decorated S3 adapter. */
|
|
194
|
+
export function markFilesSdkS3AdapterProvenance(adapter, provenance) {
|
|
195
|
+
const raw = adapterRawObject(adapter);
|
|
196
|
+
if (raw === undefined) {
|
|
197
|
+
throw new TypeError('S3 adapter must expose an object raw client.');
|
|
198
|
+
}
|
|
199
|
+
markFilesSdkS3RawClientProvenance(raw, adapter, provenance);
|
|
200
|
+
}
|
|
201
|
+
/** Records final provider verification state on one stable raw S3 client. */
|
|
202
|
+
export function markFilesSdkS3RawClientProvenance(raw, adapter, provenance) {
|
|
203
|
+
const authority = attachS3AdapterAuthority(adapter, raw);
|
|
204
|
+
const record = setS3AdapterProvenance(raw, adapter, provenance);
|
|
205
|
+
s3AuthorityState.set(authority, { raw, record });
|
|
206
|
+
bindS3AdapterAuthorityMethods(adapter, authority);
|
|
207
|
+
}
|
|
208
|
+
function isUndecoratedS3Raw(raw) {
|
|
209
|
+
return undecoratedS3Adapters.has(raw);
|
|
210
|
+
}
|
|
211
|
+
function isStructuralS3Raw(raw) {
|
|
212
|
+
try {
|
|
213
|
+
const candidate = raw;
|
|
214
|
+
return (typeof candidate.send === 'function' &&
|
|
215
|
+
typeof candidate.config === 'object' &&
|
|
216
|
+
candidate.config !== null &&
|
|
217
|
+
candidate.config.serviceId === 'S3');
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
5
223
|
function conditionalCopyAdapterOf(adapter) {
|
|
6
224
|
if (typeof adapter !== 'object' ||
|
|
7
225
|
adapter === null ||
|
|
8
|
-
!('conditionalCopy' in adapter) ||
|
|
9
226
|
!('promote' in adapter)) {
|
|
10
227
|
return undefined;
|
|
11
228
|
}
|
|
12
229
|
const candidate = adapter;
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
230
|
+
const source = candidate.conditionalCopySource;
|
|
231
|
+
const destination = candidate.conditionalCopyDestination;
|
|
232
|
+
if ((source === undefined && destination === undefined) ||
|
|
233
|
+
(source !== undefined &&
|
|
234
|
+
(typeof source.etag !== 'boolean' ||
|
|
235
|
+
typeof source.version !== 'boolean')) ||
|
|
236
|
+
(destination !== undefined &&
|
|
237
|
+
(typeof destination.create !== 'boolean' ||
|
|
238
|
+
typeof destination.replace !== 'boolean' ||
|
|
239
|
+
typeof destination.atomicWithSource !== 'boolean')) ||
|
|
18
240
|
typeof candidate.promote !== 'function') {
|
|
19
241
|
return undefined;
|
|
20
242
|
}
|
|
21
243
|
return candidate;
|
|
22
244
|
}
|
|
23
|
-
function
|
|
245
|
+
function conditionalUploadAdapterOf(adapter) {
|
|
24
246
|
if (typeof adapter !== 'object' ||
|
|
25
247
|
adapter === null ||
|
|
26
|
-
!('
|
|
27
|
-
|
|
248
|
+
!('uploadConditional' in adapter)) {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
const candidate = adapter;
|
|
252
|
+
const create = candidate.conditionalCreate;
|
|
253
|
+
const replace = candidate.conditionalReplace;
|
|
254
|
+
const multipart = candidate.conditionalMultipartCompletion;
|
|
255
|
+
if ((create === undefined && replace === undefined) ||
|
|
256
|
+
(create !== undefined && typeof create.resultEtag !== 'boolean') ||
|
|
257
|
+
(replace !== undefined && typeof replace.resultEtag !== 'boolean') ||
|
|
258
|
+
(multipart !== undefined &&
|
|
259
|
+
(typeof multipart.create !== 'boolean' ||
|
|
260
|
+
typeof multipart.replace !== 'boolean')) ||
|
|
261
|
+
typeof candidate.uploadConditional !== 'function') {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
return candidate;
|
|
265
|
+
}
|
|
266
|
+
function conditionalDeleteAdapterOf(adapter) {
|
|
267
|
+
if (typeof adapter !== 'object' ||
|
|
268
|
+
adapter === null ||
|
|
269
|
+
!('conditionalDelete' in adapter) ||
|
|
28
270
|
!('deleteConditional' in adapter)) {
|
|
29
271
|
return undefined;
|
|
30
272
|
}
|
|
31
273
|
const candidate = adapter;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
typeof capability.create !== 'boolean' ||
|
|
35
|
-
typeof capability.replace !== 'boolean' ||
|
|
36
|
-
typeof capability.delete !== 'boolean' ||
|
|
37
|
-
typeof capability.etag !== 'boolean' ||
|
|
38
|
-
typeof candidate.uploadConditional !== 'function' ||
|
|
274
|
+
if (candidate.conditionalDelete === undefined ||
|
|
275
|
+
typeof candidate.conditionalDelete.etag !== 'boolean' ||
|
|
39
276
|
typeof candidate.deleteConditional !== 'function') {
|
|
40
277
|
return undefined;
|
|
41
278
|
}
|
|
42
279
|
return candidate;
|
|
43
280
|
}
|
|
281
|
+
function conditionalReadAdapterOf(adapter) {
|
|
282
|
+
if (typeof adapter !== 'object' ||
|
|
283
|
+
adapter === null ||
|
|
284
|
+
!('conditionalRead' in adapter) ||
|
|
285
|
+
!('downloadConditional' in adapter)) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
const candidate = adapter;
|
|
289
|
+
if (candidate.conditionalRead === undefined ||
|
|
290
|
+
typeof candidate.conditionalRead.etag !== 'boolean' ||
|
|
291
|
+
typeof candidate.conditionalRead.version !== 'boolean' ||
|
|
292
|
+
typeof candidate.downloadConditional !== 'function') {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
return candidate;
|
|
296
|
+
}
|
|
297
|
+
function physicalKeyAdapterOf(adapter) {
|
|
298
|
+
if (typeof adapter !== 'object' ||
|
|
299
|
+
adapter === null ||
|
|
300
|
+
!('physicalKey' in adapter)) {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
const candidate = adapter;
|
|
304
|
+
const maxBytes = candidate.physicalKey?.maxBytes;
|
|
305
|
+
if (!Number.isSafeInteger(maxBytes) || (maxBytes ?? 0) <= 0) {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
return candidate;
|
|
309
|
+
}
|
|
44
310
|
function signedUploadPolicyAdapterOf(adapter) {
|
|
45
311
|
if (typeof adapter !== 'object' ||
|
|
46
312
|
adapter === null ||
|
|
@@ -114,19 +380,42 @@ function unwrapFilesError(error) {
|
|
|
114
380
|
}
|
|
115
381
|
return current;
|
|
116
382
|
}
|
|
383
|
+
const PUBLIC_PROVIDER_ERROR_MESSAGES = Object.freeze({
|
|
384
|
+
[StorageErrorCode.NOT_FOUND]: 'Storage provider object was not found.',
|
|
385
|
+
[StorageErrorCode.UNAUTHORIZED]: 'Storage provider operation was unauthorized.',
|
|
386
|
+
[StorageErrorCode.CONFLICT]: 'Storage provider operation conflicted with current state.',
|
|
387
|
+
[StorageErrorCode.READ_ONLY]: 'Storage provider is read-only.',
|
|
388
|
+
[StorageErrorCode.INVALID_ARGUMENT]: 'Storage provider rejected an invalid argument.',
|
|
389
|
+
[StorageErrorCode.NOT_SUPPORTED]: 'Storage provider operation is not supported.',
|
|
390
|
+
[StorageErrorCode.ABORTED]: 'Storage provider operation was aborted.',
|
|
391
|
+
[StorageErrorCode.TIMEOUT]: 'Storage provider operation timed out.',
|
|
392
|
+
[StorageErrorCode.LIMIT_EXCEEDED]: 'Storage provider operation exceeded a limit.',
|
|
393
|
+
[StorageErrorCode.PROVIDER]: 'Storage provider operation failed.',
|
|
394
|
+
});
|
|
395
|
+
function sanitizedStorageError(error) {
|
|
396
|
+
return new StorageError(PUBLIC_PROVIDER_ERROR_MESSAGES[error.code], {
|
|
397
|
+
aborted: error.aborted,
|
|
398
|
+
code: error.code,
|
|
399
|
+
permanent: error.permanent,
|
|
400
|
+
timedOut: error.timedOut,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
117
403
|
export function mapFilesSdkError(error) {
|
|
118
|
-
if (isStorageError(error))
|
|
119
|
-
return error;
|
|
120
|
-
}
|
|
404
|
+
if (isStorageError(error))
|
|
405
|
+
return sanitizedStorageError(error);
|
|
121
406
|
if (!isFilesErrorLike(error)) {
|
|
122
|
-
return new StorageError(
|
|
123
|
-
cause: error,
|
|
407
|
+
return new StorageError('Storage provider operation failed.', {
|
|
124
408
|
code: StorageErrorCode.PROVIDER,
|
|
125
409
|
});
|
|
126
410
|
}
|
|
127
411
|
const filesError = unwrapFilesError(error);
|
|
128
|
-
if (
|
|
129
|
-
|
|
412
|
+
if (filesError.code === 'Provider' &&
|
|
413
|
+
!filesError.aborted &&
|
|
414
|
+
!filesError.timedOut &&
|
|
415
|
+
!filesError.permanent &&
|
|
416
|
+
isStorageError(filesError.cause) &&
|
|
417
|
+
filesError.message === filesError.cause.message) {
|
|
418
|
+
return sanitizedStorageError(filesError.cause);
|
|
130
419
|
}
|
|
131
420
|
let code;
|
|
132
421
|
if (filesError.timedOut) {
|
|
@@ -156,9 +445,8 @@ export function mapFilesSdkError(error) {
|
|
|
156
445
|
break;
|
|
157
446
|
}
|
|
158
447
|
}
|
|
159
|
-
return new StorageError(
|
|
448
|
+
return new StorageError(PUBLIC_PROVIDER_ERROR_MESSAGES[code], {
|
|
160
449
|
aborted: filesError.aborted,
|
|
161
|
-
cause: filesError.cause ?? error,
|
|
162
450
|
code,
|
|
163
451
|
permanent: filesError.permanent,
|
|
164
452
|
timedOut: filesError.timedOut,
|
|
@@ -366,10 +654,34 @@ function signedUploadOptions(options) {
|
|
|
366
654
|
...(options.minSize !== undefined && { minSize: options.minSize }),
|
|
367
655
|
};
|
|
368
656
|
}
|
|
369
|
-
function
|
|
657
|
+
function providerEtag(value, key, operation) {
|
|
658
|
+
if (value === undefined) {
|
|
659
|
+
return undefined;
|
|
660
|
+
}
|
|
661
|
+
const etag = normalizeProviderStorageEtag(value);
|
|
662
|
+
if (etag === undefined) {
|
|
663
|
+
throw new StorageError('Storage adapter returned an invalid ETag.', {
|
|
664
|
+
code: StorageErrorCode.PROVIDER,
|
|
665
|
+
key,
|
|
666
|
+
operation,
|
|
667
|
+
permanent: true,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
return etag;
|
|
671
|
+
}
|
|
672
|
+
function invalidConditionalEtag(label, key, operation) {
|
|
673
|
+
return new StorageError(`${label} must be a canonical storage ETag.`, {
|
|
674
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
675
|
+
key,
|
|
676
|
+
operation,
|
|
677
|
+
permanent: true,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
function metadataOf(file, operation) {
|
|
681
|
+
const etag = providerEtag(file.etag, file.key, operation);
|
|
370
682
|
return {
|
|
371
683
|
contentType: file.type,
|
|
372
|
-
...(
|
|
684
|
+
...(etag !== undefined && { etag }),
|
|
373
685
|
key: file.key,
|
|
374
686
|
...(file.lastModified !== undefined && {
|
|
375
687
|
lastModified: new Date(file.lastModified),
|
|
@@ -382,9 +694,10 @@ function metadataOf(file) {
|
|
|
382
694
|
};
|
|
383
695
|
}
|
|
384
696
|
function uploadResultOf(result) {
|
|
697
|
+
const etag = providerEtag(result.etag, result.key, 'upload');
|
|
385
698
|
return {
|
|
386
699
|
contentType: result.contentType,
|
|
387
|
-
...(
|
|
700
|
+
...(etag !== undefined && { etag }),
|
|
388
701
|
key: result.key,
|
|
389
702
|
...(result.lastModified !== undefined && {
|
|
390
703
|
lastModified: new Date(result.lastModified),
|
|
@@ -392,11 +705,170 @@ function uploadResultOf(result) {
|
|
|
392
705
|
size: result.size,
|
|
393
706
|
};
|
|
394
707
|
}
|
|
708
|
+
function normalizeFilesSdkPrefix(prefix) {
|
|
709
|
+
if (prefix === undefined)
|
|
710
|
+
return '';
|
|
711
|
+
if (typeof prefix !== 'string') {
|
|
712
|
+
throw new StorageError('prefix must be a string.', {
|
|
713
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
714
|
+
permanent: true,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
const normalized = prefix.replaceAll(/^\/+|(?<!\/)\/+$/gu, '');
|
|
718
|
+
if (normalized.length === 0 || normalized.includes('\0')) {
|
|
719
|
+
throw new StorageError('prefix must be a non-empty string without null bytes.', {
|
|
720
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
721
|
+
permanent: true,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
if (normalized.split('/').some((segment) => segment === '.' || segment === '..')) {
|
|
725
|
+
throw new StorageError('prefix must not contain . or .. path segments.', {
|
|
726
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
727
|
+
permanent: true,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
return normalized;
|
|
731
|
+
}
|
|
732
|
+
function physicalPluginKey(prefix, key) {
|
|
733
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
734
|
+
throw new StorageError('The final plugin key must be a non-empty string.', {
|
|
735
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
736
|
+
permanent: true,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
if (key.includes('\0')) {
|
|
740
|
+
throw new StorageError('The final plugin key must not contain null bytes.', {
|
|
741
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
742
|
+
key,
|
|
743
|
+
permanent: true,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
const normalized = key.replace(/^\/+/u, '');
|
|
747
|
+
if (normalized.split('/').some((segment) => segment === '.' || segment === '..')) {
|
|
748
|
+
throw new StorageError('The final plugin key must not contain . or .. path segments.', {
|
|
749
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
750
|
+
key,
|
|
751
|
+
permanent: true,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
logicalKey: key,
|
|
756
|
+
physicalKey: prefix.length === 0 ? key : `${prefix}/${normalized}`,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function assertPhysicalPluginBudget(physicalKey, maxBytes, logicalKey) {
|
|
760
|
+
if (maxBytes === undefined ||
|
|
761
|
+
new TextEncoder().encode(physicalKey).byteLength <= maxBytes) {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
throw new StorageError(`The combined storage prefix and key exceed the provider's ${maxBytes}-byte physical-key limit.`, {
|
|
765
|
+
code: StorageErrorCode.LIMIT_EXCEEDED,
|
|
766
|
+
...(logicalKey !== undefined && { key: logicalKey }),
|
|
767
|
+
permanent: true,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
function physicalKeyGuardPlugin(prefix, maxBytes) {
|
|
771
|
+
const assertKey = (key) => {
|
|
772
|
+
const resolved = physicalPluginKey(prefix, key);
|
|
773
|
+
assertPhysicalPluginBudget(resolved.physicalKey, maxBytes, resolved.logicalKey);
|
|
774
|
+
return resolved.logicalKey;
|
|
775
|
+
};
|
|
776
|
+
const assertListOptions = (options) => {
|
|
777
|
+
if (options !== undefined &&
|
|
778
|
+
(typeof options !== 'object' || options === null)) {
|
|
779
|
+
throw new StorageError('The final plugin list options must be an object.', {
|
|
780
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
781
|
+
permanent: true,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
const listPrefix = options
|
|
785
|
+
?.prefix;
|
|
786
|
+
if (listPrefix === undefined || listPrefix === '') {
|
|
787
|
+
if (prefix.length > 0) {
|
|
788
|
+
assertPhysicalPluginBudget(`${prefix}/`, maxBytes);
|
|
789
|
+
}
|
|
790
|
+
return options === undefined
|
|
791
|
+
? undefined
|
|
792
|
+
: (() => {
|
|
793
|
+
const { prefix: _prefix, ...rest } = options;
|
|
794
|
+
return Object.freeze({
|
|
795
|
+
...rest,
|
|
796
|
+
...(listPrefix === '' && { prefix: listPrefix }),
|
|
797
|
+
});
|
|
798
|
+
})();
|
|
799
|
+
}
|
|
800
|
+
if (typeof listPrefix !== 'string') {
|
|
801
|
+
throw new StorageError('The final plugin list prefix must be a string.', {
|
|
802
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
803
|
+
permanent: true,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
const resolved = physicalPluginKey(prefix, listPrefix);
|
|
807
|
+
assertPhysicalPluginBudget(resolved.physicalKey, maxBytes, resolved.logicalKey);
|
|
808
|
+
return Object.freeze({
|
|
809
|
+
...options,
|
|
810
|
+
prefix: resolved.logicalKey,
|
|
811
|
+
});
|
|
812
|
+
};
|
|
813
|
+
return {
|
|
814
|
+
name: '@nestm/storage/physical-key-guard',
|
|
815
|
+
wrap: handlers({
|
|
816
|
+
copy: (operation, next) => {
|
|
817
|
+
const from = assertKey(operation.from);
|
|
818
|
+
const to = assertKey(operation.to);
|
|
819
|
+
return next(Object.freeze({ ...operation, from, to }));
|
|
820
|
+
},
|
|
821
|
+
delete: (operation, next) => {
|
|
822
|
+
const key = assertKey(operation.key);
|
|
823
|
+
return next(Object.freeze({ ...operation, key }));
|
|
824
|
+
},
|
|
825
|
+
download: (operation, next) => {
|
|
826
|
+
const key = assertKey(operation.key);
|
|
827
|
+
return next(Object.freeze({ ...operation, key }));
|
|
828
|
+
},
|
|
829
|
+
exists: (operation, next) => {
|
|
830
|
+
const key = assertKey(operation.key);
|
|
831
|
+
return next(Object.freeze({ ...operation, key }));
|
|
832
|
+
},
|
|
833
|
+
head: (operation, next) => {
|
|
834
|
+
const key = assertKey(operation.key);
|
|
835
|
+
return next(Object.freeze({ ...operation, key }));
|
|
836
|
+
},
|
|
837
|
+
list: (operation, next) => {
|
|
838
|
+
const options = assertListOptions(operation.options);
|
|
839
|
+
return next(Object.freeze({
|
|
840
|
+
...operation,
|
|
841
|
+
...(options !== undefined && { options }),
|
|
842
|
+
}));
|
|
843
|
+
},
|
|
844
|
+
move: (operation, next) => {
|
|
845
|
+
const from = assertKey(operation.from);
|
|
846
|
+
const to = assertKey(operation.to);
|
|
847
|
+
return next(Object.freeze({ ...operation, from, to }));
|
|
848
|
+
},
|
|
849
|
+
signedUploadUrl: (operation, next) => {
|
|
850
|
+
const key = assertKey(operation.key);
|
|
851
|
+
return next(Object.freeze({ ...operation, key }));
|
|
852
|
+
},
|
|
853
|
+
upload: (operation, next) => {
|
|
854
|
+
const key = assertKey(operation.key);
|
|
855
|
+
return next(Object.freeze({ ...operation, key }));
|
|
856
|
+
},
|
|
857
|
+
url: (operation, next) => {
|
|
858
|
+
const key = assertKey(operation.key);
|
|
859
|
+
return next(Object.freeze({ ...operation, key }));
|
|
860
|
+
},
|
|
861
|
+
}),
|
|
862
|
+
};
|
|
863
|
+
}
|
|
395
864
|
export class FilesSdkStorageDriver {
|
|
396
865
|
#files;
|
|
397
866
|
#name;
|
|
398
867
|
#conditionalCopy;
|
|
399
|
-
#
|
|
868
|
+
#conditionalDelete;
|
|
869
|
+
#conditionalRead;
|
|
870
|
+
#conditionalUpload;
|
|
871
|
+
#physicalKey;
|
|
400
872
|
#prefix;
|
|
401
873
|
#readOnly;
|
|
402
874
|
#retries;
|
|
@@ -405,17 +877,81 @@ export class FilesSdkStorageDriver {
|
|
|
405
877
|
#signedUploadPolicy;
|
|
406
878
|
#signedDownloadPolicy;
|
|
407
879
|
constructor(options) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
880
|
+
const raw = adapterRawObject(options.adapter);
|
|
881
|
+
const rawProvenance = raw === undefined ? undefined : s3AdapterProvenanceOfRaw(raw);
|
|
882
|
+
const authorityToken = s3AdapterAuthorityTokenOf(options.adapter);
|
|
883
|
+
const authorityState = typeof authorityToken === 'object'
|
|
884
|
+
? s3AuthorityState.get(authorityToken)
|
|
885
|
+
: undefined;
|
|
886
|
+
const authorityInvalid = authorityToken === conflictingS3Authority ||
|
|
887
|
+
(authorityState !== undefined &&
|
|
888
|
+
(authorityState.record === undefined ||
|
|
889
|
+
raw === undefined ||
|
|
890
|
+
(raw !== authorityState.raw &&
|
|
891
|
+
rawProvenance !== authorityState.record)));
|
|
892
|
+
const s3Provenance = authorityState?.record ?? rawProvenance;
|
|
893
|
+
if (authorityInvalid ||
|
|
894
|
+
(s3Provenance === undefined &&
|
|
895
|
+
raw !== undefined &&
|
|
896
|
+
(isUndecoratedS3Raw(raw) || isStructuralS3Raw(raw)))) {
|
|
897
|
+
throw new StorageError('Raw S3 adapters must be configured with withS3Capabilities() or createS3StorageDriver().', {
|
|
898
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
899
|
+
permanent: true,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
const mismatchedAuthority = s3Provenance === undefined
|
|
903
|
+
? undefined
|
|
904
|
+
: mismatchedS3Authority(options.adapter, s3Provenance.surface);
|
|
905
|
+
if (mismatchedAuthority !== undefined) {
|
|
906
|
+
throw new StorageError(`S3 adapter authority "${mismatchedAuthority}" does not match its verified provider profile.`, {
|
|
907
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
908
|
+
permanent: true,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
const adapterForFiles = s3Provenance === undefined
|
|
912
|
+
? options.adapter
|
|
913
|
+
: Object.freeze({ ...options.adapter });
|
|
914
|
+
const copiedAuthorityMismatch = s3Provenance === undefined
|
|
915
|
+
? undefined
|
|
916
|
+
: mismatchedS3Authority(adapterForFiles, s3Provenance.surface);
|
|
917
|
+
if (copiedAuthorityMismatch !== undefined) {
|
|
918
|
+
throw new StorageError(`S3 adapter authority "${copiedAuthorityMismatch}" changed while it was being bound.`, {
|
|
919
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
920
|
+
permanent: true,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
const readOnly = options.readonly === true || s3Provenance?.provenance === 'unverified';
|
|
924
|
+
this.#physicalKey = physicalKeyAdapterOf(adapterForFiles);
|
|
925
|
+
const guardedPrefix = normalizeFilesSdkPrefix(options.prefix);
|
|
926
|
+
const plugins = [
|
|
927
|
+
...(options.plugins ?? []),
|
|
928
|
+
physicalKeyGuardPlugin(guardedPrefix, this.#physicalKey?.physicalKey.maxBytes),
|
|
929
|
+
];
|
|
930
|
+
this.#files = new Files({
|
|
931
|
+
...options,
|
|
932
|
+
adapter: adapterForFiles,
|
|
933
|
+
plugins,
|
|
934
|
+
readonly: readOnly,
|
|
935
|
+
});
|
|
936
|
+
this.#name = adapterForFiles.name;
|
|
937
|
+
this.#conditionalCopy = conditionalCopyAdapterOf(adapterForFiles);
|
|
938
|
+
this.#conditionalDelete = conditionalDeleteAdapterOf(adapterForFiles);
|
|
939
|
+
this.#conditionalRead = conditionalReadAdapterOf(adapterForFiles);
|
|
940
|
+
this.#conditionalUpload = conditionalUploadAdapterOf(adapterForFiles);
|
|
412
941
|
this.#prefix = this.#files.prefix;
|
|
413
|
-
|
|
942
|
+
if (guardedPrefix !== this.#prefix) {
|
|
943
|
+
throw new StorageError('Storage prefix normalization was inconsistent.', {
|
|
944
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
945
|
+
permanent: true,
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
this.#assertPhysicalKeyBudget(this.#prefix);
|
|
949
|
+
this.#readOnly = readOnly;
|
|
414
950
|
this.#retries = storageRetryOptions(options.retries);
|
|
415
951
|
this.#signal = options.signal;
|
|
416
952
|
this.#timeout = options.timeout;
|
|
417
|
-
this.#signedUploadPolicy = signedUploadPolicyAdapterOf(
|
|
418
|
-
this.#signedDownloadPolicy = signedDownloadPolicyAdapterOf(
|
|
953
|
+
this.#signedUploadPolicy = signedUploadPolicyAdapterOf(adapterForFiles);
|
|
954
|
+
this.#signedDownloadPolicy = signedDownloadPolicyAdapterOf(adapterForFiles);
|
|
419
955
|
}
|
|
420
956
|
get name() {
|
|
421
957
|
return this.#name;
|
|
@@ -427,34 +963,78 @@ export class FilesSdkStorageDriver {
|
|
|
427
963
|
delimiter: capabilities.delimiter,
|
|
428
964
|
metadata: capabilities.metadata,
|
|
429
965
|
rangeRead: capabilities.rangeRead,
|
|
430
|
-
resumableUpload: capabilities.multipart,
|
|
431
|
-
serverSideCopy: capabilities.serverSideCopy,
|
|
432
|
-
...(this.#conditionalCopy !== undefined &&
|
|
433
|
-
|
|
966
|
+
resumableUpload: !this.#readOnly && capabilities.multipart,
|
|
967
|
+
serverSideCopy: !this.#readOnly && capabilities.serverSideCopy,
|
|
968
|
+
...(this.#conditionalCopy !== undefined &&
|
|
969
|
+
!this.#readOnly && {
|
|
970
|
+
...(this.#conditionalCopy.conditionalCopySource !== undefined && {
|
|
971
|
+
conditionalCopySource: {
|
|
972
|
+
...this.#conditionalCopy.conditionalCopySource,
|
|
973
|
+
},
|
|
974
|
+
}),
|
|
975
|
+
...(this.#conditionalCopy.conditionalCopyDestination !==
|
|
976
|
+
undefined && {
|
|
977
|
+
conditionalCopyDestination: {
|
|
978
|
+
...this.#conditionalCopy.conditionalCopyDestination,
|
|
979
|
+
},
|
|
980
|
+
}),
|
|
434
981
|
}),
|
|
435
|
-
...(this.#
|
|
982
|
+
...(this.#conditionalDelete !== undefined &&
|
|
436
983
|
!this.#readOnly && {
|
|
437
|
-
|
|
438
|
-
...this.#
|
|
984
|
+
conditionalDelete: {
|
|
985
|
+
...this.#conditionalDelete.conditionalDelete,
|
|
439
986
|
},
|
|
440
987
|
}),
|
|
988
|
+
...(this.#conditionalRead !== undefined && {
|
|
989
|
+
conditionalRead: { ...this.#conditionalRead.conditionalRead },
|
|
990
|
+
}),
|
|
991
|
+
...(this.#conditionalUpload !== undefined &&
|
|
992
|
+
!this.#readOnly && {
|
|
993
|
+
...(this.#conditionalUpload.conditionalCreate !== undefined && {
|
|
994
|
+
conditionalCreate: {
|
|
995
|
+
...this.#conditionalUpload.conditionalCreate,
|
|
996
|
+
},
|
|
997
|
+
}),
|
|
998
|
+
...(this.#conditionalUpload.conditionalReplace !== undefined && {
|
|
999
|
+
conditionalReplace: {
|
|
1000
|
+
...this.#conditionalUpload.conditionalReplace,
|
|
1001
|
+
},
|
|
1002
|
+
}),
|
|
1003
|
+
...(this.#conditionalUpload.conditionalMultipartCompletion !==
|
|
1004
|
+
undefined && {
|
|
1005
|
+
conditionalMultipartCompletion: {
|
|
1006
|
+
...this.#conditionalUpload.conditionalMultipartCompletion,
|
|
1007
|
+
},
|
|
1008
|
+
}),
|
|
1009
|
+
}),
|
|
1010
|
+
...(this.#physicalKey !== undefined && {
|
|
1011
|
+
physicalKey: { ...this.#physicalKey.physicalKey },
|
|
1012
|
+
}),
|
|
441
1013
|
signedDownload: { ...capabilities.signedUrl },
|
|
442
1014
|
...(this.#signedDownloadPolicy !== undefined && {
|
|
443
1015
|
signedDownloadPolicy: {
|
|
444
1016
|
...this.#signedDownloadPolicy.signedDownloadPolicy,
|
|
445
1017
|
},
|
|
446
1018
|
}),
|
|
447
|
-
signedUpload: 'runtime',
|
|
448
|
-
...(this.#signedUploadPolicy !== undefined &&
|
|
449
|
-
|
|
1019
|
+
signedUpload: this.#readOnly ? false : 'runtime',
|
|
1020
|
+
...(this.#signedUploadPolicy !== undefined &&
|
|
1021
|
+
!this.#readOnly && {
|
|
1022
|
+
signedUploadPolicy: {
|
|
1023
|
+
...this.#signedUploadPolicy.signedUploadPolicy,
|
|
1024
|
+
},
|
|
450
1025
|
}),
|
|
451
|
-
nativeUploadProgress: capabilities.uploadProgress,
|
|
1026
|
+
nativeUploadProgress: !this.#readOnly && capabilities.uploadProgress,
|
|
452
1027
|
};
|
|
453
1028
|
}
|
|
454
1029
|
async upload(key, body, options) {
|
|
1030
|
+
this.#assertLogicalKey(key);
|
|
455
1031
|
return this.#call(async () => uploadResultOf(await this.#files.upload(key, mapBody(body), uploadOptions(options))));
|
|
456
1032
|
}
|
|
457
1033
|
uploadConditional(key, body, options) {
|
|
1034
|
+
if (options.condition.type === 'replace' &&
|
|
1035
|
+
!isCanonicalStorageEtag(options.condition.etag)) {
|
|
1036
|
+
return Promise.reject(invalidConditionalEtag('condition.etag', key, 'upload'));
|
|
1037
|
+
}
|
|
458
1038
|
if (this.#readOnly) {
|
|
459
1039
|
return Promise.reject(new StorageError(`Cannot call uploadConditional() on a read-only storage adapter.`, {
|
|
460
1040
|
code: StorageErrorCode.READ_ONLY,
|
|
@@ -463,8 +1043,18 @@ export class FilesSdkStorageDriver {
|
|
|
463
1043
|
permanent: true,
|
|
464
1044
|
}));
|
|
465
1045
|
}
|
|
466
|
-
const adapter = this.#
|
|
467
|
-
|
|
1046
|
+
const adapter = this.#conditionalUpload;
|
|
1047
|
+
const capability = options.condition.type === 'create'
|
|
1048
|
+
? adapter?.conditionalCreate
|
|
1049
|
+
: adapter?.conditionalReplace;
|
|
1050
|
+
const multipartRequested = options.multipart !== undefined && options.multipart !== false;
|
|
1051
|
+
const multipartSupported = !multipartRequested ||
|
|
1052
|
+
(options.condition.type === 'create'
|
|
1053
|
+
? adapter?.conditionalMultipartCompletion?.create === true
|
|
1054
|
+
: adapter?.conditionalMultipartCompletion?.replace === true);
|
|
1055
|
+
if (adapter === undefined ||
|
|
1056
|
+
capability === undefined ||
|
|
1057
|
+
!multipartSupported) {
|
|
468
1058
|
return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional upload.`, {
|
|
469
1059
|
code: StorageErrorCode.NOT_SUPPORTED,
|
|
470
1060
|
key,
|
|
@@ -484,30 +1074,88 @@ export class FilesSdkStorageDriver {
|
|
|
484
1074
|
permanent: true,
|
|
485
1075
|
});
|
|
486
1076
|
}
|
|
487
|
-
|
|
1077
|
+
const etag = providerEtag(result.etag, key, 'upload');
|
|
1078
|
+
return {
|
|
1079
|
+
...result,
|
|
1080
|
+
...(etag === undefined ? {} : { etag }),
|
|
1081
|
+
key,
|
|
1082
|
+
};
|
|
488
1083
|
});
|
|
489
1084
|
}
|
|
490
1085
|
async download(key, options) {
|
|
1086
|
+
this.#assertLogicalKey(key);
|
|
491
1087
|
return this.#call(async () => {
|
|
492
1088
|
const file = await this.#files.download(key, downloadOptions(options));
|
|
493
1089
|
return {
|
|
494
|
-
...metadataOf(file),
|
|
1090
|
+
...metadataOf(file, 'download'),
|
|
495
1091
|
body: normalizeDownloadStream(file.stream()),
|
|
496
1092
|
};
|
|
497
1093
|
});
|
|
498
1094
|
}
|
|
1095
|
+
downloadConditional(key, options) {
|
|
1096
|
+
if (options.condition.etag !== undefined &&
|
|
1097
|
+
!isCanonicalStorageEtag(options.condition.etag)) {
|
|
1098
|
+
return Promise.reject(invalidConditionalEtag('condition.etag', key, 'download'));
|
|
1099
|
+
}
|
|
1100
|
+
const adapter = this.#conditionalRead;
|
|
1101
|
+
if (adapter === undefined ||
|
|
1102
|
+
(options.condition.etag !== undefined && !adapter.conditionalRead.etag) ||
|
|
1103
|
+
(options.condition.version !== undefined &&
|
|
1104
|
+
!adapter.conditionalRead.version)) {
|
|
1105
|
+
return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional read.`, {
|
|
1106
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
1107
|
+
key,
|
|
1108
|
+
operation: 'download',
|
|
1109
|
+
permanent: true,
|
|
1110
|
+
}));
|
|
1111
|
+
}
|
|
1112
|
+
return this.#call(async () => {
|
|
1113
|
+
const physicalKey = this.#path(key);
|
|
1114
|
+
const object = await adapter.downloadConditional(physicalKey, this.#conditionalOptions(options));
|
|
1115
|
+
if (object.key !== physicalKey) {
|
|
1116
|
+
await object.body.cancel().catch(() => undefined);
|
|
1117
|
+
throw new StorageError('Storage adapter returned an unexpected conditional download key.', {
|
|
1118
|
+
code: StorageErrorCode.PROVIDER,
|
|
1119
|
+
key,
|
|
1120
|
+
operation: 'download',
|
|
1121
|
+
permanent: true,
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
let etag;
|
|
1125
|
+
try {
|
|
1126
|
+
etag = providerEtag(object.etag, key, 'download');
|
|
1127
|
+
}
|
|
1128
|
+
catch (error) {
|
|
1129
|
+
await object.body.cancel(error).catch(() => undefined);
|
|
1130
|
+
throw error;
|
|
1131
|
+
}
|
|
1132
|
+
return {
|
|
1133
|
+
...object,
|
|
1134
|
+
body: normalizeDownloadStream(object.body),
|
|
1135
|
+
...(etag === undefined ? {} : { etag }),
|
|
1136
|
+
key,
|
|
1137
|
+
name: key.split('/').at(-1) ?? key,
|
|
1138
|
+
};
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
499
1141
|
async head(key, options) {
|
|
500
|
-
|
|
1142
|
+
this.#assertLogicalKey(key);
|
|
1143
|
+
return this.#call(async () => metadataOf(await this.#files.head(key, operationOptions(options)), 'head'));
|
|
501
1144
|
}
|
|
502
1145
|
exists(key, options) {
|
|
1146
|
+
this.#assertLogicalKey(key);
|
|
503
1147
|
return this.#call(() => this.#files.exists(key, operationOptions(options)));
|
|
504
1148
|
}
|
|
505
1149
|
delete(key, options) {
|
|
1150
|
+
this.#assertLogicalKey(key);
|
|
506
1151
|
return this.#call(async () => {
|
|
507
1152
|
await this.#files.delete(key, operationOptions(options));
|
|
508
1153
|
});
|
|
509
1154
|
}
|
|
510
1155
|
deleteConditional(key, options) {
|
|
1156
|
+
if (!isCanonicalStorageEtag(options.condition.etag)) {
|
|
1157
|
+
return Promise.reject(invalidConditionalEtag('condition.etag', key, 'delete'));
|
|
1158
|
+
}
|
|
511
1159
|
if (this.#readOnly) {
|
|
512
1160
|
return Promise.reject(new StorageError(`Cannot call deleteConditional() on a read-only storage adapter.`, {
|
|
513
1161
|
code: StorageErrorCode.READ_ONLY,
|
|
@@ -516,8 +1164,8 @@ export class FilesSdkStorageDriver {
|
|
|
516
1164
|
permanent: true,
|
|
517
1165
|
}));
|
|
518
1166
|
}
|
|
519
|
-
const adapter = this.#
|
|
520
|
-
if (adapter === undefined) {
|
|
1167
|
+
const adapter = this.#conditionalDelete;
|
|
1168
|
+
if (adapter === undefined || !adapter.conditionalDelete.etag) {
|
|
521
1169
|
return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional delete.`, {
|
|
522
1170
|
code: StorageErrorCode.NOT_SUPPORTED,
|
|
523
1171
|
key,
|
|
@@ -529,14 +1177,58 @@ export class FilesSdkStorageDriver {
|
|
|
529
1177
|
return this.#call(() => adapter.deleteConditional(this.#path(key), mergedOptions));
|
|
530
1178
|
}
|
|
531
1179
|
copy(sourceKey, destinationKey, options) {
|
|
1180
|
+
this.#assertLogicalKey(sourceKey);
|
|
1181
|
+
this.#assertLogicalKey(destinationKey);
|
|
532
1182
|
return this.#call(() => this.#files.copy(sourceKey, destinationKey, operationOptions(options)));
|
|
533
1183
|
}
|
|
534
1184
|
move(sourceKey, destinationKey, options) {
|
|
1185
|
+
this.#assertLogicalKey(sourceKey);
|
|
1186
|
+
this.#assertLogicalKey(destinationKey);
|
|
535
1187
|
return this.#call(() => this.#files.move(sourceKey, destinationKey, operationOptions(options)));
|
|
536
1188
|
}
|
|
537
1189
|
promote(sourceKey, destinationKey, options) {
|
|
1190
|
+
const destination = options.destination;
|
|
1191
|
+
if (destination !== undefined &&
|
|
1192
|
+
(typeof destination !== 'object' ||
|
|
1193
|
+
destination === null ||
|
|
1194
|
+
(destination.type !== 'create' && destination.type !== 'replace'))) {
|
|
1195
|
+
return Promise.reject(new StorageError('destination.type must be "create" or "replace".', {
|
|
1196
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
1197
|
+
key: destinationKey,
|
|
1198
|
+
operation: 'promote',
|
|
1199
|
+
permanent: true,
|
|
1200
|
+
}));
|
|
1201
|
+
}
|
|
1202
|
+
if (options.sourceEtag !== undefined &&
|
|
1203
|
+
!isCanonicalStorageEtag(options.sourceEtag)) {
|
|
1204
|
+
return Promise.reject(invalidConditionalEtag('sourceEtag', sourceKey, 'promote'));
|
|
1205
|
+
}
|
|
1206
|
+
if (destination?.type === 'replace' &&
|
|
1207
|
+
!isCanonicalStorageEtag(destination.etag)) {
|
|
1208
|
+
return Promise.reject(invalidConditionalEtag('destination.etag', destinationKey, 'promote'));
|
|
1209
|
+
}
|
|
1210
|
+
if (this.#readOnly) {
|
|
1211
|
+
return Promise.reject(new StorageError(`Cannot call promote() on a read-only storage adapter.`, {
|
|
1212
|
+
code: StorageErrorCode.READ_ONLY,
|
|
1213
|
+
key: sourceKey,
|
|
1214
|
+
operation: 'promote',
|
|
1215
|
+
permanent: true,
|
|
1216
|
+
}));
|
|
1217
|
+
}
|
|
538
1218
|
const adapter = this.#conditionalCopy;
|
|
539
|
-
if (adapter === undefined ||
|
|
1219
|
+
if (adapter === undefined ||
|
|
1220
|
+
(options.sourceEtag !== undefined &&
|
|
1221
|
+
adapter.conditionalCopySource?.etag !== true) ||
|
|
1222
|
+
(options.sourceVersion !== undefined &&
|
|
1223
|
+
adapter.conditionalCopySource?.version !== true) ||
|
|
1224
|
+
(options.destination?.type === 'create' &&
|
|
1225
|
+
adapter.conditionalCopyDestination?.create !== true) ||
|
|
1226
|
+
(options.destination?.type === 'replace' &&
|
|
1227
|
+
adapter.conditionalCopyDestination?.replace !== true) ||
|
|
1228
|
+
(options.destination !== undefined &&
|
|
1229
|
+
(options.sourceEtag !== undefined ||
|
|
1230
|
+
options.sourceVersion !== undefined) &&
|
|
1231
|
+
adapter.conditionalCopyDestination?.atomicWithSource !== true)) {
|
|
540
1232
|
return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional promotion.`, {
|
|
541
1233
|
code: StorageErrorCode.NOT_SUPPORTED,
|
|
542
1234
|
key: sourceKey,
|
|
@@ -544,13 +1236,14 @@ export class FilesSdkStorageDriver {
|
|
|
544
1236
|
permanent: true,
|
|
545
1237
|
}));
|
|
546
1238
|
}
|
|
547
|
-
return this.#call(() => adapter.promote(sourceKey, destinationKey, options));
|
|
1239
|
+
return this.#call(() => adapter.promote(this.#path(sourceKey), this.#path(destinationKey), this.#conditionalOptions(options)));
|
|
548
1240
|
}
|
|
549
1241
|
async list(options) {
|
|
1242
|
+
this.#assertListPhysicalPrefix(options?.prefix);
|
|
550
1243
|
return this.#call(async () => {
|
|
551
1244
|
const result = await this.#files.list(listOptions(options));
|
|
552
1245
|
return {
|
|
553
|
-
items: result.items.map(metadataOf),
|
|
1246
|
+
items: result.items.map((file) => metadataOf(file, 'list')),
|
|
554
1247
|
...(result.cursor !== undefined && { cursor: result.cursor }),
|
|
555
1248
|
...(result.prefixes !== undefined && {
|
|
556
1249
|
prefixes: [...result.prefixes],
|
|
@@ -564,7 +1257,7 @@ export class FilesSdkStorageDriver {
|
|
|
564
1257
|
async *#search(pattern, options) {
|
|
565
1258
|
try {
|
|
566
1259
|
for await (const file of this.#files.search(pattern, searchOptions(options))) {
|
|
567
|
-
yield metadataOf(file);
|
|
1260
|
+
yield metadataOf(file, 'search');
|
|
568
1261
|
}
|
|
569
1262
|
}
|
|
570
1263
|
catch (error) {
|
|
@@ -572,9 +1265,11 @@ export class FilesSdkStorageDriver {
|
|
|
572
1265
|
}
|
|
573
1266
|
}
|
|
574
1267
|
signDownload(key, options) {
|
|
1268
|
+
this.#assertLogicalKey(key);
|
|
575
1269
|
return this.#call(() => this.#files.url(key, signedDownloadOptions(options)));
|
|
576
1270
|
}
|
|
577
1271
|
signUpload(key, options) {
|
|
1272
|
+
this.#assertLogicalKey(key);
|
|
578
1273
|
return this.#call(() => this.#files.signedUploadUrl(key, signedUploadOptions(options)));
|
|
579
1274
|
}
|
|
580
1275
|
async #call(operation) {
|
|
@@ -599,9 +1294,6 @@ export class FilesSdkStorageDriver {
|
|
|
599
1294
|
permanent: true,
|
|
600
1295
|
});
|
|
601
1296
|
}
|
|
602
|
-
if (this.#prefix.length === 0) {
|
|
603
|
-
return key;
|
|
604
|
-
}
|
|
605
1297
|
const normalized = key.replace(/^\/+/u, '');
|
|
606
1298
|
if (normalized
|
|
607
1299
|
.split('/')
|
|
@@ -612,7 +1304,36 @@ export class FilesSdkStorageDriver {
|
|
|
612
1304
|
permanent: true,
|
|
613
1305
|
});
|
|
614
1306
|
}
|
|
615
|
-
|
|
1307
|
+
const physicalKey = this.#prefix.length === 0 ? key : `${this.#prefix}/${normalized}`;
|
|
1308
|
+
this.#assertPhysicalKeyBudget(physicalKey, key);
|
|
1309
|
+
return physicalKey;
|
|
1310
|
+
}
|
|
1311
|
+
#assertLogicalKey(key) {
|
|
1312
|
+
this.#path(key);
|
|
1313
|
+
}
|
|
1314
|
+
#assertListPhysicalPrefix(prefix) {
|
|
1315
|
+
if (prefix !== undefined && prefix.length > 0) {
|
|
1316
|
+
this.#assertLogicalKey(prefix);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (this.#prefix.length > 0) {
|
|
1320
|
+
this.#assertPhysicalKeyBudget(`${this.#prefix}/`);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
#physicalKeyExceedsBudget(physicalKey) {
|
|
1324
|
+
const maxBytes = this.#physicalKey?.physicalKey.maxBytes;
|
|
1325
|
+
return (maxBytes !== undefined &&
|
|
1326
|
+
new TextEncoder().encode(physicalKey).byteLength > maxBytes);
|
|
1327
|
+
}
|
|
1328
|
+
#assertPhysicalKeyBudget(physicalKey, logicalKey) {
|
|
1329
|
+
const maxBytes = this.#physicalKey?.physicalKey.maxBytes;
|
|
1330
|
+
if (maxBytes === undefined || !this.#physicalKeyExceedsBudget(physicalKey))
|
|
1331
|
+
return;
|
|
1332
|
+
throw new StorageError(`The combined storage prefix and key exceed the provider's ${maxBytes}-byte physical-key limit.`, {
|
|
1333
|
+
code: StorageErrorCode.LIMIT_EXCEEDED,
|
|
1334
|
+
...(logicalKey !== undefined && { key: logicalKey }),
|
|
1335
|
+
permanent: true,
|
|
1336
|
+
});
|
|
616
1337
|
}
|
|
617
1338
|
#conditionalOptions(options) {
|
|
618
1339
|
return {
|