@nestm/storage 0.1.0-alpha.6 → 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 +128 -0
- package/README.md +398 -19
- package/SECURITY.md +69 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +74 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts.map +1 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.js +414 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.js.map +1 -0
- package/dist/ai-sdk/index.d.ts +2 -0
- package/dist/ai-sdk/index.d.ts.map +1 -0
- package/dist/ai-sdk/index.js +2 -0
- package/dist/ai-sdk/index.js.map +1 -0
- package/dist/files-sdk/files-sdk.driver.d.ts +64 -6
- package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
- package/dist/files-sdk/files-sdk.driver.js +918 -38
- package/dist/files-sdk/files-sdk.driver.js.map +1 -1
- package/dist/files-sdk/fs/index.d.ts +21 -2
- package/dist/files-sdk/fs/index.d.ts.map +1 -1
- package/dist/files-sdk/fs/index.js +833 -1
- 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 +60 -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 +43 -14
- package/dist/files-sdk/s3/index.d.ts.map +1 -1
- package/dist/files-sdk/s3/index.js +1046 -57
- 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 +7 -1
- package/dist/storage.client.d.ts.map +1 -1
- package/dist/storage.client.js +124 -10
- package/dist/storage.client.js.map +1 -1
- package/dist/storage.driver.d.ts +23 -1
- package/dist/storage.driver.d.ts.map +1 -1
- package/dist/storage.driver.js.map +1 -1
- package/dist/storage.service.d.ts.map +1 -1
- package/dist/storage.service.js +28 -3
- package/dist/storage.service.js.map +1 -1
- package/dist/storage.types.d.ts +113 -6
- 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 +5 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +5 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/storage-workspace.cursor.d.ts +71 -0
- package/dist/workspace/storage-workspace.cursor.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.cursor.js +408 -0
- package/dist/workspace/storage-workspace.cursor.js.map +1 -0
- package/dist/workspace/storage-workspace.d.ts +6 -0
- package/dist/workspace/storage-workspace.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.error.d.ts +28 -0
- package/dist/workspace/storage-workspace.error.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.error.js +67 -0
- package/dist/workspace/storage-workspace.error.js.map +1 -0
- package/dist/workspace/storage-workspace.js +755 -0
- package/dist/workspace/storage-workspace.js.map +1 -0
- package/dist/workspace/storage-workspace.path.d.ts +9 -0
- package/dist/workspace/storage-workspace.path.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.path.js +68 -0
- package/dist/workspace/storage-workspace.path.js.map +1 -0
- package/dist/workspace/storage-workspace.types.d.ts +105 -0
- package/dist/workspace/storage-workspace.types.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.types.js +21 -0
- package/dist/workspace/storage-workspace.types.js.map +1 -0
- package/package.json +24 -5
|
@@ -1,25 +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
|
}
|
|
245
|
+
function conditionalUploadAdapterOf(adapter) {
|
|
246
|
+
if (typeof adapter !== 'object' ||
|
|
247
|
+
adapter === null ||
|
|
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) ||
|
|
270
|
+
!('deleteConditional' in adapter)) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
const candidate = adapter;
|
|
274
|
+
if (candidate.conditionalDelete === undefined ||
|
|
275
|
+
typeof candidate.conditionalDelete.etag !== 'boolean' ||
|
|
276
|
+
typeof candidate.deleteConditional !== 'function') {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
return candidate;
|
|
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
|
+
}
|
|
23
310
|
function signedUploadPolicyAdapterOf(adapter) {
|
|
24
311
|
if (typeof adapter !== 'object' ||
|
|
25
312
|
adapter === null ||
|
|
@@ -93,19 +380,42 @@ function unwrapFilesError(error) {
|
|
|
93
380
|
}
|
|
94
381
|
return current;
|
|
95
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
|
+
}
|
|
96
403
|
export function mapFilesSdkError(error) {
|
|
97
|
-
if (isStorageError(error))
|
|
98
|
-
return error;
|
|
99
|
-
}
|
|
404
|
+
if (isStorageError(error))
|
|
405
|
+
return sanitizedStorageError(error);
|
|
100
406
|
if (!isFilesErrorLike(error)) {
|
|
101
|
-
return new StorageError(
|
|
102
|
-
cause: error,
|
|
407
|
+
return new StorageError('Storage provider operation failed.', {
|
|
103
408
|
code: StorageErrorCode.PROVIDER,
|
|
104
409
|
});
|
|
105
410
|
}
|
|
106
411
|
const filesError = unwrapFilesError(error);
|
|
107
|
-
if (
|
|
108
|
-
|
|
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);
|
|
109
419
|
}
|
|
110
420
|
let code;
|
|
111
421
|
if (filesError.timedOut) {
|
|
@@ -135,9 +445,8 @@ export function mapFilesSdkError(error) {
|
|
|
135
445
|
break;
|
|
136
446
|
}
|
|
137
447
|
}
|
|
138
|
-
return new StorageError(
|
|
448
|
+
return new StorageError(PUBLIC_PROVIDER_ERROR_MESSAGES[code], {
|
|
139
449
|
aborted: filesError.aborted,
|
|
140
|
-
cause: filesError.cause ?? error,
|
|
141
450
|
code,
|
|
142
451
|
permanent: filesError.permanent,
|
|
143
452
|
timedOut: filesError.timedOut,
|
|
@@ -157,6 +466,32 @@ function mapRetryOptions(retries) {
|
|
|
157
466
|
}),
|
|
158
467
|
};
|
|
159
468
|
}
|
|
469
|
+
function storageRetryOptions(retries) {
|
|
470
|
+
if (retries === undefined || typeof retries === 'number') {
|
|
471
|
+
return retries;
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
max: retries.max,
|
|
475
|
+
...(retries.backoff !== undefined && {
|
|
476
|
+
backoff: ({ attempt, error }) => retries.backoff?.({
|
|
477
|
+
attempt,
|
|
478
|
+
error: new FilesError(error.code === StorageErrorCode.NOT_FOUND
|
|
479
|
+
? 'NotFound'
|
|
480
|
+
: error.code === StorageErrorCode.UNAUTHORIZED
|
|
481
|
+
? 'Unauthorized'
|
|
482
|
+
: error.code === StorageErrorCode.CONFLICT
|
|
483
|
+
? 'Conflict'
|
|
484
|
+
: error.code === StorageErrorCode.READ_ONLY
|
|
485
|
+
? 'ReadOnly'
|
|
486
|
+
: 'Provider', error.message, error, {
|
|
487
|
+
aborted: error.aborted,
|
|
488
|
+
permanent: error.permanent,
|
|
489
|
+
timedOut: error.timedOut,
|
|
490
|
+
}),
|
|
491
|
+
}) ?? 0,
|
|
492
|
+
}),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
160
495
|
function operationOptions(options) {
|
|
161
496
|
if (options === undefined) {
|
|
162
497
|
return undefined;
|
|
@@ -319,10 +654,34 @@ function signedUploadOptions(options) {
|
|
|
319
654
|
...(options.minSize !== undefined && { minSize: options.minSize }),
|
|
320
655
|
};
|
|
321
656
|
}
|
|
322
|
-
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);
|
|
323
682
|
return {
|
|
324
683
|
contentType: file.type,
|
|
325
|
-
...(
|
|
684
|
+
...(etag !== undefined && { etag }),
|
|
326
685
|
key: file.key,
|
|
327
686
|
...(file.lastModified !== undefined && {
|
|
328
687
|
lastModified: new Date(file.lastModified),
|
|
@@ -335,9 +694,10 @@ function metadataOf(file) {
|
|
|
335
694
|
};
|
|
336
695
|
}
|
|
337
696
|
function uploadResultOf(result) {
|
|
697
|
+
const etag = providerEtag(result.etag, result.key, 'upload');
|
|
338
698
|
return {
|
|
339
699
|
contentType: result.contentType,
|
|
340
|
-
...(
|
|
700
|
+
...(etag !== undefined && { etag }),
|
|
341
701
|
key: result.key,
|
|
342
702
|
...(result.lastModified !== undefined && {
|
|
343
703
|
lastModified: new Date(result.lastModified),
|
|
@@ -345,18 +705,253 @@ function uploadResultOf(result) {
|
|
|
345
705
|
size: result.size,
|
|
346
706
|
};
|
|
347
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
|
+
}
|
|
348
864
|
export class FilesSdkStorageDriver {
|
|
349
865
|
#files;
|
|
350
866
|
#name;
|
|
351
867
|
#conditionalCopy;
|
|
868
|
+
#conditionalDelete;
|
|
869
|
+
#conditionalRead;
|
|
870
|
+
#conditionalUpload;
|
|
871
|
+
#physicalKey;
|
|
872
|
+
#prefix;
|
|
873
|
+
#readOnly;
|
|
874
|
+
#retries;
|
|
875
|
+
#signal;
|
|
876
|
+
#timeout;
|
|
352
877
|
#signedUploadPolicy;
|
|
353
878
|
#signedDownloadPolicy;
|
|
354
879
|
constructor(options) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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);
|
|
941
|
+
this.#prefix = this.#files.prefix;
|
|
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;
|
|
950
|
+
this.#retries = storageRetryOptions(options.retries);
|
|
951
|
+
this.#signal = options.signal;
|
|
952
|
+
this.#timeout = options.timeout;
|
|
953
|
+
this.#signedUploadPolicy = signedUploadPolicyAdapterOf(adapterForFiles);
|
|
954
|
+
this.#signedDownloadPolicy = signedDownloadPolicyAdapterOf(adapterForFiles);
|
|
360
955
|
}
|
|
361
956
|
get name() {
|
|
362
957
|
return this.#name;
|
|
@@ -368,10 +963,52 @@ export class FilesSdkStorageDriver {
|
|
|
368
963
|
delimiter: capabilities.delimiter,
|
|
369
964
|
metadata: capabilities.metadata,
|
|
370
965
|
rangeRead: capabilities.rangeRead,
|
|
371
|
-
resumableUpload: capabilities.multipart,
|
|
372
|
-
serverSideCopy: capabilities.serverSideCopy,
|
|
373
|
-
...(this.#conditionalCopy !== undefined &&
|
|
374
|
-
|
|
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
|
+
}),
|
|
981
|
+
}),
|
|
982
|
+
...(this.#conditionalDelete !== undefined &&
|
|
983
|
+
!this.#readOnly && {
|
|
984
|
+
conditionalDelete: {
|
|
985
|
+
...this.#conditionalDelete.conditionalDelete,
|
|
986
|
+
},
|
|
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 },
|
|
375
1012
|
}),
|
|
376
1013
|
signedDownload: { ...capabilities.signedUrl },
|
|
377
1014
|
...(this.#signedDownloadPolicy !== undefined && {
|
|
@@ -379,45 +1016,219 @@ export class FilesSdkStorageDriver {
|
|
|
379
1016
|
...this.#signedDownloadPolicy.signedDownloadPolicy,
|
|
380
1017
|
},
|
|
381
1018
|
}),
|
|
382
|
-
signedUpload: 'runtime',
|
|
383
|
-
...(this.#signedUploadPolicy !== undefined &&
|
|
384
|
-
|
|
1019
|
+
signedUpload: this.#readOnly ? false : 'runtime',
|
|
1020
|
+
...(this.#signedUploadPolicy !== undefined &&
|
|
1021
|
+
!this.#readOnly && {
|
|
1022
|
+
signedUploadPolicy: {
|
|
1023
|
+
...this.#signedUploadPolicy.signedUploadPolicy,
|
|
1024
|
+
},
|
|
385
1025
|
}),
|
|
386
|
-
nativeUploadProgress: capabilities.uploadProgress,
|
|
1026
|
+
nativeUploadProgress: !this.#readOnly && capabilities.uploadProgress,
|
|
387
1027
|
};
|
|
388
1028
|
}
|
|
389
1029
|
async upload(key, body, options) {
|
|
1030
|
+
this.#assertLogicalKey(key);
|
|
390
1031
|
return this.#call(async () => uploadResultOf(await this.#files.upload(key, mapBody(body), uploadOptions(options))));
|
|
391
1032
|
}
|
|
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
|
+
}
|
|
1038
|
+
if (this.#readOnly) {
|
|
1039
|
+
return Promise.reject(new StorageError(`Cannot call uploadConditional() on a read-only storage adapter.`, {
|
|
1040
|
+
code: StorageErrorCode.READ_ONLY,
|
|
1041
|
+
key,
|
|
1042
|
+
operation: 'upload',
|
|
1043
|
+
permanent: true,
|
|
1044
|
+
}));
|
|
1045
|
+
}
|
|
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) {
|
|
1058
|
+
return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional upload.`, {
|
|
1059
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
1060
|
+
key,
|
|
1061
|
+
operation: 'upload',
|
|
1062
|
+
permanent: true,
|
|
1063
|
+
}));
|
|
1064
|
+
}
|
|
1065
|
+
const mergedOptions = this.#conditionalOptions(options);
|
|
1066
|
+
return this.#call(async () => {
|
|
1067
|
+
const physicalKey = this.#path(key);
|
|
1068
|
+
const result = await adapter.uploadConditional(physicalKey, mapBody(body), mergedOptions);
|
|
1069
|
+
if (result.key !== physicalKey) {
|
|
1070
|
+
throw new StorageError('Storage adapter returned an unexpected conditional upload key.', {
|
|
1071
|
+
code: StorageErrorCode.PROVIDER,
|
|
1072
|
+
key,
|
|
1073
|
+
operation: 'upload',
|
|
1074
|
+
permanent: true,
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
const etag = providerEtag(result.etag, key, 'upload');
|
|
1078
|
+
return {
|
|
1079
|
+
...result,
|
|
1080
|
+
...(etag === undefined ? {} : { etag }),
|
|
1081
|
+
key,
|
|
1082
|
+
};
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
392
1085
|
async download(key, options) {
|
|
1086
|
+
this.#assertLogicalKey(key);
|
|
393
1087
|
return this.#call(async () => {
|
|
394
1088
|
const file = await this.#files.download(key, downloadOptions(options));
|
|
395
1089
|
return {
|
|
396
|
-
...metadataOf(file),
|
|
1090
|
+
...metadataOf(file, 'download'),
|
|
397
1091
|
body: normalizeDownloadStream(file.stream()),
|
|
398
1092
|
};
|
|
399
1093
|
});
|
|
400
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
|
+
}
|
|
401
1141
|
async head(key, options) {
|
|
402
|
-
|
|
1142
|
+
this.#assertLogicalKey(key);
|
|
1143
|
+
return this.#call(async () => metadataOf(await this.#files.head(key, operationOptions(options)), 'head'));
|
|
403
1144
|
}
|
|
404
1145
|
exists(key, options) {
|
|
1146
|
+
this.#assertLogicalKey(key);
|
|
405
1147
|
return this.#call(() => this.#files.exists(key, operationOptions(options)));
|
|
406
1148
|
}
|
|
407
1149
|
delete(key, options) {
|
|
1150
|
+
this.#assertLogicalKey(key);
|
|
408
1151
|
return this.#call(async () => {
|
|
409
1152
|
await this.#files.delete(key, operationOptions(options));
|
|
410
1153
|
});
|
|
411
1154
|
}
|
|
1155
|
+
deleteConditional(key, options) {
|
|
1156
|
+
if (!isCanonicalStorageEtag(options.condition.etag)) {
|
|
1157
|
+
return Promise.reject(invalidConditionalEtag('condition.etag', key, 'delete'));
|
|
1158
|
+
}
|
|
1159
|
+
if (this.#readOnly) {
|
|
1160
|
+
return Promise.reject(new StorageError(`Cannot call deleteConditional() on a read-only storage adapter.`, {
|
|
1161
|
+
code: StorageErrorCode.READ_ONLY,
|
|
1162
|
+
key,
|
|
1163
|
+
operation: 'delete',
|
|
1164
|
+
permanent: true,
|
|
1165
|
+
}));
|
|
1166
|
+
}
|
|
1167
|
+
const adapter = this.#conditionalDelete;
|
|
1168
|
+
if (adapter === undefined || !adapter.conditionalDelete.etag) {
|
|
1169
|
+
return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional delete.`, {
|
|
1170
|
+
code: StorageErrorCode.NOT_SUPPORTED,
|
|
1171
|
+
key,
|
|
1172
|
+
operation: 'delete',
|
|
1173
|
+
permanent: true,
|
|
1174
|
+
}));
|
|
1175
|
+
}
|
|
1176
|
+
const mergedOptions = this.#conditionalOptions(options);
|
|
1177
|
+
return this.#call(() => adapter.deleteConditional(this.#path(key), mergedOptions));
|
|
1178
|
+
}
|
|
412
1179
|
copy(sourceKey, destinationKey, options) {
|
|
1180
|
+
this.#assertLogicalKey(sourceKey);
|
|
1181
|
+
this.#assertLogicalKey(destinationKey);
|
|
413
1182
|
return this.#call(() => this.#files.copy(sourceKey, destinationKey, operationOptions(options)));
|
|
414
1183
|
}
|
|
415
1184
|
move(sourceKey, destinationKey, options) {
|
|
1185
|
+
this.#assertLogicalKey(sourceKey);
|
|
1186
|
+
this.#assertLogicalKey(destinationKey);
|
|
416
1187
|
return this.#call(() => this.#files.move(sourceKey, destinationKey, operationOptions(options)));
|
|
417
1188
|
}
|
|
418
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
|
+
}
|
|
419
1218
|
const adapter = this.#conditionalCopy;
|
|
420
|
-
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)) {
|
|
421
1232
|
return Promise.reject(new StorageError(`Storage adapter "${this.#name}" does not support conditional promotion.`, {
|
|
422
1233
|
code: StorageErrorCode.NOT_SUPPORTED,
|
|
423
1234
|
key: sourceKey,
|
|
@@ -425,13 +1236,14 @@ export class FilesSdkStorageDriver {
|
|
|
425
1236
|
permanent: true,
|
|
426
1237
|
}));
|
|
427
1238
|
}
|
|
428
|
-
return this.#call(() => adapter.promote(sourceKey, destinationKey, options));
|
|
1239
|
+
return this.#call(() => adapter.promote(this.#path(sourceKey), this.#path(destinationKey), this.#conditionalOptions(options)));
|
|
429
1240
|
}
|
|
430
1241
|
async list(options) {
|
|
1242
|
+
this.#assertListPhysicalPrefix(options?.prefix);
|
|
431
1243
|
return this.#call(async () => {
|
|
432
1244
|
const result = await this.#files.list(listOptions(options));
|
|
433
1245
|
return {
|
|
434
|
-
items: result.items.map(metadataOf),
|
|
1246
|
+
items: result.items.map((file) => metadataOf(file, 'list')),
|
|
435
1247
|
...(result.cursor !== undefined && { cursor: result.cursor }),
|
|
436
1248
|
...(result.prefixes !== undefined && {
|
|
437
1249
|
prefixes: [...result.prefixes],
|
|
@@ -445,7 +1257,7 @@ export class FilesSdkStorageDriver {
|
|
|
445
1257
|
async *#search(pattern, options) {
|
|
446
1258
|
try {
|
|
447
1259
|
for await (const file of this.#files.search(pattern, searchOptions(options))) {
|
|
448
|
-
yield metadataOf(file);
|
|
1260
|
+
yield metadataOf(file, 'search');
|
|
449
1261
|
}
|
|
450
1262
|
}
|
|
451
1263
|
catch (error) {
|
|
@@ -453,9 +1265,11 @@ export class FilesSdkStorageDriver {
|
|
|
453
1265
|
}
|
|
454
1266
|
}
|
|
455
1267
|
signDownload(key, options) {
|
|
1268
|
+
this.#assertLogicalKey(key);
|
|
456
1269
|
return this.#call(() => this.#files.url(key, signedDownloadOptions(options)));
|
|
457
1270
|
}
|
|
458
1271
|
signUpload(key, options) {
|
|
1272
|
+
this.#assertLogicalKey(key);
|
|
459
1273
|
return this.#call(() => this.#files.signedUploadUrl(key, signedUploadOptions(options)));
|
|
460
1274
|
}
|
|
461
1275
|
async #call(operation) {
|
|
@@ -466,6 +1280,72 @@ export class FilesSdkStorageDriver {
|
|
|
466
1280
|
throw mapFilesSdkError(error);
|
|
467
1281
|
}
|
|
468
1282
|
}
|
|
1283
|
+
#path(key) {
|
|
1284
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
1285
|
+
throw new StorageError('key must be a non-empty string.', {
|
|
1286
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
1287
|
+
permanent: true,
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
if (key.includes('\0')) {
|
|
1291
|
+
throw new StorageError('key must not contain null bytes.', {
|
|
1292
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
1293
|
+
key,
|
|
1294
|
+
permanent: true,
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
const normalized = key.replace(/^\/+/u, '');
|
|
1298
|
+
if (normalized
|
|
1299
|
+
.split('/')
|
|
1300
|
+
.some((segment) => segment === '.' || segment === '..')) {
|
|
1301
|
+
throw new StorageError('key must not contain . or .. path segments.', {
|
|
1302
|
+
code: StorageErrorCode.INVALID_ARGUMENT,
|
|
1303
|
+
key,
|
|
1304
|
+
permanent: true,
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
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
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
#conditionalOptions(options) {
|
|
1339
|
+
return {
|
|
1340
|
+
...options,
|
|
1341
|
+
...(options.retries === undefined &&
|
|
1342
|
+
this.#retries !== undefined && { retries: this.#retries }),
|
|
1343
|
+
...(options.signal === undefined &&
|
|
1344
|
+
this.#signal !== undefined && { signal: this.#signal }),
|
|
1345
|
+
...(options.timeout === undefined &&
|
|
1346
|
+
this.#timeout !== undefined && { timeout: this.#timeout }),
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
469
1349
|
}
|
|
470
1350
|
export function createFilesSdkDriver(options) {
|
|
471
1351
|
return new FilesSdkStorageDriver(options);
|