@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,6 +1,7 @@
|
|
|
1
1
|
import { StorageClient } from '../storage.client.js';
|
|
2
|
+
import { isCanonicalStorageEtag } from '../storage-etag.js';
|
|
2
3
|
import { StorageErrorCode } from '../storage.error.js';
|
|
3
|
-
import {
|
|
4
|
+
import { STORAGE_WORKSPACE_MAX_CURSOR_BYTES, decodeStorageWorkspaceCursor, issueStorageWorkspaceCursor, resolveStorageWorkspaceCursorConfiguration, } from './storage-workspace.cursor.js';
|
|
4
5
|
import { sanitizeWorkspaceError, workspaceError, } from './storage-workspace.error.js';
|
|
5
6
|
import { assertWorkspacePath, containsControlCharacter, isPathInside, joinWorkspacePath, workspaceBasename, } from './storage-workspace.path.js';
|
|
6
7
|
import { DEFAULT_STORAGE_WORKSPACE_LIMITS, STORAGE_WORKSPACE_PERMISSIONS, } from './storage-workspace.types.js';
|
|
@@ -29,18 +30,42 @@ function positiveSafeInteger(value, label) {
|
|
|
29
30
|
}
|
|
30
31
|
return value;
|
|
31
32
|
}
|
|
32
|
-
function assertEtag(etag,
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
containsControlCharacter(etag) ||
|
|
36
|
-
encoder.encode(etag).byteLength > maxBytes) {
|
|
37
|
-
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, `${operation} requires a valid non-empty etag.`, { permanent: true });
|
|
33
|
+
function assertEtag(etag, operation) {
|
|
34
|
+
if (!isCanonicalStorageEtag(etag)) {
|
|
35
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, `${operation} requires a canonical bare etag of at most 1,024 bytes.`, { permanent: true });
|
|
38
36
|
}
|
|
39
37
|
}
|
|
38
|
+
function writeModeOf(options) {
|
|
39
|
+
const candidate = options;
|
|
40
|
+
const mode = candidate?.mode;
|
|
41
|
+
if (mode === 'create' || mode === 'overwrite' || mode === 'replace') {
|
|
42
|
+
if (mode !== 'replace' && candidate !== undefined && 'etag' in candidate) {
|
|
43
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Workspace write options may include an etag only in replace mode.', { permanent: true });
|
|
44
|
+
}
|
|
45
|
+
return mode;
|
|
46
|
+
}
|
|
47
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Workspace write mode must be create, overwrite, or replace.', { permanent: true });
|
|
48
|
+
}
|
|
49
|
+
function hasExplicitMode(options, expected, operation) {
|
|
50
|
+
if (typeof options !== 'object' || options === null) {
|
|
51
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, `${operation} options must be an object.`, { permanent: true });
|
|
52
|
+
}
|
|
53
|
+
if (!('mode' in options)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (options.mode !== expected) {
|
|
57
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, `${operation} mode must be ${expected}.`, { permanent: true });
|
|
58
|
+
}
|
|
59
|
+
if ('etag' in options) {
|
|
60
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, `${operation} ${expected} options must not include an etag.`, { permanent: true });
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
40
64
|
function resolveLimits(requested, parent) {
|
|
41
65
|
const baseline = parent ?? DEFAULT_STORAGE_WORKSPACE_LIMITS;
|
|
42
66
|
const next = {
|
|
43
67
|
cursorTtlMs: requested?.cursorTtlMs ?? baseline.cursorTtlMs,
|
|
68
|
+
maxCursorBytes: requested?.maxCursorBytes ?? baseline.maxCursorBytes,
|
|
44
69
|
maxPageSize: requested?.maxPageSize ?? baseline.maxPageSize,
|
|
45
70
|
maxPathBytes: requested?.maxPathBytes ?? baseline.maxPathBytes,
|
|
46
71
|
maxReadBytes: requested?.maxReadBytes ?? baseline.maxReadBytes,
|
|
@@ -55,6 +80,9 @@ function resolveLimits(requested, parent) {
|
|
|
55
80
|
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, `Child workspace limit "${name}" cannot exceed its parent limit.`, { permanent: true });
|
|
56
81
|
}
|
|
57
82
|
}
|
|
83
|
+
if (next.maxCursorBytes > STORAGE_WORKSPACE_MAX_CURSOR_BYTES) {
|
|
84
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, `limits.maxCursorBytes cannot exceed ${STORAGE_WORKSPACE_MAX_CURSOR_BYTES}.`, { permanent: true });
|
|
85
|
+
}
|
|
58
86
|
return Object.freeze(next);
|
|
59
87
|
}
|
|
60
88
|
function resolvePermissions(requested, parent) {
|
|
@@ -77,8 +105,7 @@ function logicalFile(metadata, path) {
|
|
|
77
105
|
metadata.size < 0 ||
|
|
78
106
|
typeof metadata.contentType !== 'string' ||
|
|
79
107
|
metadata.contentType.length === 0 ||
|
|
80
|
-
(metadata.etag !== undefined &&
|
|
81
|
-
(typeof metadata.etag !== 'string' || metadata.etag.length === 0)) ||
|
|
108
|
+
(metadata.etag !== undefined && !isCanonicalStorageEtag(metadata.etag)) ||
|
|
82
109
|
(metadata.lastModified !== undefined &&
|
|
83
110
|
!Number.isFinite(new Date(metadata.lastModified).getTime()))) {
|
|
84
111
|
throw workspaceError(StorageErrorCode.PROVIDER, 'Storage provider returned malformed file metadata.', { permanent: true });
|
|
@@ -224,7 +251,7 @@ async function collectBoundedText(stream, announcedSize, maxBytes, path) {
|
|
|
224
251
|
async function collectBoundedBytes(stream, announcedSize, maxBytes, path) {
|
|
225
252
|
if (announcedSize > maxBytes) {
|
|
226
253
|
await stream.cancel().catch(() => undefined);
|
|
227
|
-
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `Workspace file "${path}" exceeds the ${maxBytes}-byte
|
|
254
|
+
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `Workspace file "${path}" exceeds the ${maxBytes}-byte limit.`, { path, permanent: true });
|
|
228
255
|
}
|
|
229
256
|
const reader = stream.getReader();
|
|
230
257
|
const chunks = [];
|
|
@@ -241,7 +268,7 @@ async function collectBoundedBytes(stream, announcedSize, maxBytes, path) {
|
|
|
241
268
|
size += result.value.byteLength;
|
|
242
269
|
if (size > maxBytes) {
|
|
243
270
|
await reader.cancel().catch(() => undefined);
|
|
244
|
-
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `Workspace file "${path}" exceeded the ${maxBytes}-byte
|
|
271
|
+
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `Workspace file "${path}" exceeded the ${maxBytes}-byte limit.`, { path, permanent: true });
|
|
245
272
|
}
|
|
246
273
|
chunks.push(result.value);
|
|
247
274
|
}
|
|
@@ -315,10 +342,31 @@ class StorageWorkspaceImplementation {
|
|
|
315
342
|
});
|
|
316
343
|
}
|
|
317
344
|
}
|
|
345
|
+
async readBytes(path, options) {
|
|
346
|
+
this.#require('read');
|
|
347
|
+
const logicalPath = this.#filePath(path);
|
|
348
|
+
const maxBytes = options?.maxBytes ?? this.#limits.maxReadBytes;
|
|
349
|
+
positiveSafeInteger(maxBytes, 'maxBytes');
|
|
350
|
+
if (maxBytes > this.#limits.maxReadBytes) {
|
|
351
|
+
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `maxBytes cannot exceed the ${this.#limits.maxReadBytes}-byte workspace read limit.`, { path: logicalPath, permanent: true });
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const object = await this.#state.client.downloadStream(this.#scope(logicalPath), operationOptions(options));
|
|
355
|
+
this.#assertResultPath(object.key, logicalPath);
|
|
356
|
+
const bytes = await collectBoundedBytes(object.body, object.size, maxBytes, logicalPath);
|
|
357
|
+
return { ...logicalFile(object, logicalPath), bytes };
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
throw sanitizeWorkspaceError(error, {
|
|
361
|
+
operation: 'read',
|
|
362
|
+
path: logicalPath,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
318
366
|
async list(options = {}) {
|
|
319
367
|
this.#require('list');
|
|
320
|
-
const continuation = options.cursor
|
|
321
|
-
? this.#state.
|
|
368
|
+
const continuation = options.cursor !== undefined
|
|
369
|
+
? await decodeStorageWorkspaceCursor(this.#state.cursor, this.#cursorBinding(), 'list', options.cursor)
|
|
322
370
|
: undefined;
|
|
323
371
|
if (continuation !== undefined &&
|
|
324
372
|
((options.directory !== undefined &&
|
|
@@ -331,7 +379,6 @@ class StorageWorkspaceImplementation {
|
|
|
331
379
|
const directory = this.#directoryPath(continuation?.directory ?? options.directory ?? '');
|
|
332
380
|
const limit = this.#pageLimit(continuation?.limit ?? options.limit, this.#limits.maxPageSize);
|
|
333
381
|
const recursive = continuation?.recursive ?? options.recursive === true;
|
|
334
|
-
const binding = this.#binding('list');
|
|
335
382
|
const scopedDirectory = this.#scopeDirectory(directory);
|
|
336
383
|
try {
|
|
337
384
|
const page = await this.#state.client.list({
|
|
@@ -383,12 +430,13 @@ class StorageWorkspaceImplementation {
|
|
|
383
430
|
}
|
|
384
431
|
return {
|
|
385
432
|
...(page.cursor !== undefined && {
|
|
386
|
-
cursor: this.#state.
|
|
433
|
+
cursor: await issueStorageWorkspaceCursor(this.#state.cursor, this.#cursorBinding(), {
|
|
387
434
|
backendCursor: page.cursor,
|
|
388
435
|
directory,
|
|
389
436
|
limit,
|
|
437
|
+
operation: 'list',
|
|
390
438
|
recursive,
|
|
391
|
-
}
|
|
439
|
+
}),
|
|
392
440
|
}),
|
|
393
441
|
entries,
|
|
394
442
|
};
|
|
@@ -402,8 +450,8 @@ class StorageWorkspaceImplementation {
|
|
|
402
450
|
}
|
|
403
451
|
async search(query, options = {}) {
|
|
404
452
|
this.#require('search');
|
|
405
|
-
const continuation = options.cursor
|
|
406
|
-
? this.#state.
|
|
453
|
+
const continuation = options.cursor !== undefined
|
|
454
|
+
? await decodeStorageWorkspaceCursor(this.#state.cursor, this.#cursorBinding(), 'search', options.cursor)
|
|
407
455
|
: undefined;
|
|
408
456
|
if (continuation !== undefined &&
|
|
409
457
|
((query.length > 0 && query !== continuation.query) ||
|
|
@@ -432,18 +480,11 @@ class StorageWorkspaceImplementation {
|
|
|
432
480
|
}
|
|
433
481
|
const caseInsensitive = continuation?.caseInsensitive ?? options.caseInsensitive === true;
|
|
434
482
|
const limit = this.#pageLimit(continuation?.limit ?? options.limit, this.#limits.maxSearchResults);
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
caseInsensitive,
|
|
439
|
-
directory,
|
|
440
|
-
limit,
|
|
441
|
-
match,
|
|
442
|
-
query,
|
|
443
|
-
scanned: 0,
|
|
483
|
+
const prior = {
|
|
484
|
+
backendCursor: continuation?.backendCursor,
|
|
485
|
+
scanned: continuation?.scanned ?? 0,
|
|
444
486
|
};
|
|
445
487
|
return this.#searchPage(query, {
|
|
446
|
-
binding,
|
|
447
488
|
caseInsensitive,
|
|
448
489
|
directory,
|
|
449
490
|
limit,
|
|
@@ -453,7 +494,8 @@ class StorageWorkspaceImplementation {
|
|
|
453
494
|
});
|
|
454
495
|
}
|
|
455
496
|
async writeFile(path, body, options) {
|
|
456
|
-
|
|
497
|
+
const mode = writeModeOf(options);
|
|
498
|
+
this.#require(mode === 'overwrite' ? 'write' : mode);
|
|
457
499
|
const logicalPath = this.#filePath(path);
|
|
458
500
|
if (typeof body !== 'string' && !(body instanceof Uint8Array)) {
|
|
459
501
|
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Workspace writes accept only strings or Uint8Array bodies.', { path: logicalPath, permanent: true });
|
|
@@ -464,9 +506,6 @@ class StorageWorkspaceImplementation {
|
|
|
464
506
|
if (size > this.#limits.maxWriteBytes) {
|
|
465
507
|
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `Workspace write exceeds the ${this.#limits.maxWriteBytes}-byte limit.`, { path: logicalPath, permanent: true });
|
|
466
508
|
}
|
|
467
|
-
if (options.mode === 'replace') {
|
|
468
|
-
assertEtag(options.etag, this.#limits.maxPathBytes, 'Replace');
|
|
469
|
-
}
|
|
470
509
|
try {
|
|
471
510
|
const common = {
|
|
472
511
|
...(options.contentType !== undefined && {
|
|
@@ -475,18 +514,27 @@ class StorageWorkspaceImplementation {
|
|
|
475
514
|
...(options.metadata !== undefined && { metadata: options.metadata }),
|
|
476
515
|
...operationOptions(options),
|
|
477
516
|
};
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
517
|
+
let result;
|
|
518
|
+
if (mode === 'overwrite') {
|
|
519
|
+
result = await this.#state.client.upload(this.#scope(logicalPath), body, common);
|
|
520
|
+
}
|
|
521
|
+
else if (mode === 'create') {
|
|
522
|
+
result = await this.#state.client.uploadConditional(this.#scope(logicalPath), body, { ...common, condition: { type: 'create' } });
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
const etag = options.etag;
|
|
526
|
+
assertEtag(etag, 'Replace');
|
|
527
|
+
result = await this.#state.client.uploadConditional(this.#scope(logicalPath), body, {
|
|
481
528
|
...common,
|
|
482
|
-
condition: { etag
|
|
529
|
+
condition: { etag, type: 'replace' },
|
|
483
530
|
});
|
|
531
|
+
}
|
|
484
532
|
this.#assertResultPath(result.key, logicalPath);
|
|
485
533
|
return logicalFile(result, logicalPath);
|
|
486
534
|
}
|
|
487
535
|
catch (error) {
|
|
488
536
|
throw sanitizeWorkspaceError(error, {
|
|
489
|
-
operation:
|
|
537
|
+
operation: mode,
|
|
490
538
|
path: logicalPath,
|
|
491
539
|
});
|
|
492
540
|
}
|
|
@@ -494,34 +542,45 @@ class StorageWorkspaceImplementation {
|
|
|
494
542
|
async copyFile(source, destination, options) {
|
|
495
543
|
this.#require('copy');
|
|
496
544
|
this.#require('read');
|
|
497
|
-
|
|
545
|
+
const overwrite = hasExplicitMode(options, 'overwrite', 'Copy');
|
|
546
|
+
this.#require(overwrite ? 'write' : 'create');
|
|
498
547
|
const sourcePath = this.#filePath(source, 'source path');
|
|
499
548
|
const destinationPath = this.#filePath(destination, 'destination path');
|
|
500
549
|
if (sourcePath === destinationPath) {
|
|
501
550
|
throw workspaceError(StorageErrorCode.CONFLICT, 'Copy destination must differ from its source.', { path: destinationPath, permanent: true });
|
|
502
551
|
}
|
|
503
|
-
if (
|
|
504
|
-
|
|
552
|
+
if (overwrite) {
|
|
553
|
+
return this.#copyOverwrite(sourcePath, destinationPath, options);
|
|
554
|
+
}
|
|
555
|
+
const conditional = options;
|
|
556
|
+
assertEtag(conditional.etag, 'Copy');
|
|
557
|
+
const capabilities = this.#state.client.capabilities;
|
|
558
|
+
if (capabilities.conditionalCreate?.resultEtag !== true ||
|
|
559
|
+
capabilities.conditionalRead?.etag !== true) {
|
|
560
|
+
throw workspaceError(StorageErrorCode.NOT_SUPPORTED, 'Safe copy requires exact-ETag reads and create-only uploads that return an ETag.', { operation: 'copy', path: destinationPath, permanent: true });
|
|
505
561
|
}
|
|
506
|
-
return this.#copyCreate(sourcePath, destinationPath,
|
|
562
|
+
return this.#copyCreate(sourcePath, destinationPath, conditional);
|
|
507
563
|
}
|
|
508
564
|
async moveFile(source, destination, options) {
|
|
509
565
|
this.#require('move');
|
|
510
566
|
this.#require('read');
|
|
511
567
|
this.#require('create');
|
|
512
568
|
this.#require('delete');
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
conditional.delete !== true ||
|
|
516
|
-
conditional.etag !== true) {
|
|
517
|
-
throw workspaceError(StorageErrorCode.NOT_SUPPORTED, 'Safe move requires create-only upload, conditional delete, and upload ETag support.', { operation: 'move', permanent: true });
|
|
569
|
+
if (typeof options !== 'object' || options === null || 'mode' in options) {
|
|
570
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Workspace move supports only an ETag-conditional mutation.', { permanent: true });
|
|
518
571
|
}
|
|
519
572
|
const sourcePath = this.#filePath(source, 'source path');
|
|
520
573
|
const destinationPath = this.#filePath(destination, 'destination path');
|
|
521
574
|
if (sourcePath === destinationPath) {
|
|
522
575
|
throw workspaceError(StorageErrorCode.CONFLICT, 'Move destination must differ from its source.', { path: destinationPath, permanent: true });
|
|
523
576
|
}
|
|
524
|
-
|
|
577
|
+
const capabilities = this.#state.client.capabilities;
|
|
578
|
+
if (capabilities.conditionalCreate?.resultEtag !== true ||
|
|
579
|
+
capabilities.conditionalDelete?.etag !== true ||
|
|
580
|
+
capabilities.conditionalRead?.etag !== true) {
|
|
581
|
+
throw workspaceError(StorageErrorCode.NOT_SUPPORTED, 'Safe move requires exact-ETag reads, create-only uploads that return an ETag, and conditional delete.', { operation: 'move', permanent: true });
|
|
582
|
+
}
|
|
583
|
+
assertEtag(options.etag, 'Move');
|
|
525
584
|
const copied = await this.#copyCreate(sourcePath, destinationPath, options);
|
|
526
585
|
if (copied.etag === undefined) {
|
|
527
586
|
throw workspaceError(StorageErrorCode.NOT_SUPPORTED, 'Safe move requires the destination provider to return an etag.', { path: destinationPath, permanent: true });
|
|
@@ -544,12 +603,22 @@ class StorageWorkspaceImplementation {
|
|
|
544
603
|
async deleteFile(path, options) {
|
|
545
604
|
this.#require('delete');
|
|
546
605
|
const logicalPath = this.#filePath(path);
|
|
547
|
-
|
|
606
|
+
const unconditional = hasExplicitMode(options, 'unconditional', 'Delete');
|
|
607
|
+
if (unconditional) {
|
|
608
|
+
this.#require('write');
|
|
609
|
+
}
|
|
548
610
|
try {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
611
|
+
if (unconditional) {
|
|
612
|
+
await this.#state.client.delete(this.#scope(logicalPath), operationOptions(options));
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
const conditional = options;
|
|
616
|
+
assertEtag(conditional.etag, 'Delete');
|
|
617
|
+
await this.#state.client.deleteConditional(this.#scope(logicalPath), {
|
|
618
|
+
condition: { etag: conditional.etag },
|
|
619
|
+
...operationOptions(conditional),
|
|
620
|
+
});
|
|
621
|
+
}
|
|
553
622
|
}
|
|
554
623
|
catch (error) {
|
|
555
624
|
throw sanitizeWorkspaceError(error, {
|
|
@@ -565,14 +634,20 @@ class StorageWorkspaceImplementation {
|
|
|
565
634
|
}
|
|
566
635
|
return new StorageWorkspaceImplementation(WORKSPACE_CONSTRUCTOR, {
|
|
567
636
|
...this.#state,
|
|
568
|
-
id: `${this.#state.id}/${path}`,
|
|
569
637
|
prefix: joinWorkspacePath(this.#state.prefix, path),
|
|
570
638
|
}, resolvePermissions(options.permissions, this.#permissions), resolveLimits(options.limits, this.#limits));
|
|
571
639
|
}
|
|
572
640
|
async #copyCreate(sourcePath, destinationPath, options) {
|
|
573
641
|
try {
|
|
574
|
-
const source = await this.#state.client.
|
|
642
|
+
const source = await this.#state.client.downloadConditional(this.#scope(sourcePath), {
|
|
643
|
+
condition: { etag: options.etag },
|
|
644
|
+
...operationOptions(options),
|
|
645
|
+
});
|
|
575
646
|
this.#assertResultPath(source.key, sourcePath);
|
|
647
|
+
if (source.etag !== options.etag) {
|
|
648
|
+
await source.body.cancel().catch(() => undefined);
|
|
649
|
+
throw workspaceError(StorageErrorCode.PROVIDER, 'Conditional read returned an unexpected source ETag.', { operation: 'copy', path: sourcePath, permanent: true });
|
|
650
|
+
}
|
|
576
651
|
const bytes = await collectBoundedBytes(source.body, source.size, this.#limits.maxWriteBytes, sourcePath);
|
|
577
652
|
const result = await this.#state.client.uploadConditional(this.#scope(destinationPath), bytes, {
|
|
578
653
|
condition: { type: 'create' },
|
|
@@ -590,6 +665,26 @@ class StorageWorkspaceImplementation {
|
|
|
590
665
|
});
|
|
591
666
|
}
|
|
592
667
|
}
|
|
668
|
+
async #copyOverwrite(sourcePath, destinationPath, options) {
|
|
669
|
+
try {
|
|
670
|
+
const source = await this.#state.client.downloadStream(this.#scope(sourcePath), operationOptions(options));
|
|
671
|
+
this.#assertResultPath(source.key, sourcePath);
|
|
672
|
+
const bytes = await collectBoundedBytes(source.body, source.size, this.#limits.maxWriteBytes, sourcePath);
|
|
673
|
+
const result = await this.#state.client.upload(this.#scope(destinationPath), bytes, {
|
|
674
|
+
contentType: source.contentType,
|
|
675
|
+
...(source.metadata !== undefined && { metadata: source.metadata }),
|
|
676
|
+
...operationOptions(options),
|
|
677
|
+
});
|
|
678
|
+
this.#assertResultPath(result.key, destinationPath);
|
|
679
|
+
return logicalFile(result, destinationPath);
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
throw sanitizeWorkspaceError(error, {
|
|
683
|
+
operation: 'copy',
|
|
684
|
+
path: destinationPath,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
593
688
|
async #searchPage(query, context) {
|
|
594
689
|
const entries = [];
|
|
595
690
|
let backendCursor = context.state.backendCursor;
|
|
@@ -648,17 +743,19 @@ class StorageWorkspaceImplementation {
|
|
|
648
743
|
if (scanned >= this.#limits.maxSearchScan && hasMore) {
|
|
649
744
|
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `Search exceeded the ${this.#limits.maxSearchScan}-object scan limit.`, { permanent: true });
|
|
650
745
|
}
|
|
746
|
+
const nextBackendCursor = backendCursor;
|
|
651
747
|
return {
|
|
652
|
-
...(
|
|
653
|
-
cursor: this.#state.
|
|
654
|
-
backendCursor,
|
|
748
|
+
...(nextBackendCursor !== undefined && {
|
|
749
|
+
cursor: await issueStorageWorkspaceCursor(this.#state.cursor, this.#cursorBinding(), {
|
|
750
|
+
backendCursor: nextBackendCursor,
|
|
655
751
|
caseInsensitive: context.caseInsensitive,
|
|
656
752
|
directory: context.directory,
|
|
657
753
|
limit: context.limit,
|
|
658
754
|
match: context.match,
|
|
755
|
+
operation: 'search',
|
|
659
756
|
query,
|
|
660
757
|
scanned,
|
|
661
|
-
}
|
|
758
|
+
}),
|
|
662
759
|
}),
|
|
663
760
|
entries,
|
|
664
761
|
};
|
|
@@ -725,8 +822,12 @@ class StorageWorkspaceImplementation {
|
|
|
725
822
|
}
|
|
726
823
|
return limit;
|
|
727
824
|
}
|
|
728
|
-
#
|
|
729
|
-
return
|
|
825
|
+
#cursorBinding() {
|
|
826
|
+
return {
|
|
827
|
+
limits: this.#limits,
|
|
828
|
+
prefix: this.#state.prefix,
|
|
829
|
+
store: this.#state.client.name,
|
|
830
|
+
};
|
|
730
831
|
}
|
|
731
832
|
}
|
|
732
833
|
export function mountStorageWorkspace(client, options) {
|
|
@@ -737,8 +838,7 @@ export function mountStorageWorkspace(client, options) {
|
|
|
737
838
|
});
|
|
738
839
|
return new StorageWorkspaceImplementation(WORKSPACE_CONSTRUCTOR, {
|
|
739
840
|
client,
|
|
740
|
-
|
|
741
|
-
id: `workspace:${crypto.randomUUID()}`,
|
|
841
|
+
cursor: resolveStorageWorkspaceCursorConfiguration(options.cursor),
|
|
742
842
|
prefix,
|
|
743
843
|
}, resolvePermissions(options.permissions), limits);
|
|
744
844
|
}
|