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