@nestm/storage 0.1.0-alpha.0 → 0.1.0-alpha.10
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 +264 -0
- package/README.md +724 -25
- package/SECURITY.md +101 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +91 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts.map +1 -0
- package/dist/ai-sdk/ai-sdk-workspace-tools.js +491 -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/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/files-sdk/files-sdk.driver.d.ts +85 -3
- package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
- package/dist/files-sdk/files-sdk.driver.js +1437 -37
- package/dist/files-sdk/files-sdk.driver.js.map +1 -1
- package/dist/files-sdk/fs/index.d.ts +42 -0
- package/dist/files-sdk/fs/index.d.ts.map +1 -0
- package/dist/files-sdk/fs/index.js +1117 -0
- package/dist/files-sdk/fs/index.js.map +1 -0
- 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 +58 -0
- package/dist/files-sdk/provider/index.d.ts.map +1 -0
- package/dist/files-sdk/provider/index.js +147 -0
- package/dist/files-sdk/provider/index.js.map +1 -0
- package/dist/files-sdk/s3/construction-metadata.d.ts +9 -0
- package/dist/files-sdk/s3/construction-metadata.d.ts.map +1 -0
- package/dist/files-sdk/s3/construction-metadata.js +24 -0
- package/dist/files-sdk/s3/construction-metadata.js.map +1 -0
- package/dist/files-sdk/s3/index.d.ts +54 -0
- package/dist/files-sdk/s3/index.d.ts.map +1 -0
- package/dist/files-sdk/s3/index.js +1505 -0
- package/dist/files-sdk/s3/index.js.map +1 -0
- package/dist/gateway/index.d.ts +1 -1
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js.map +1 -1
- package/dist/gateway/storage-gateway-fastify-parser.d.ts.map +1 -1
- package/dist/gateway/storage-gateway-fastify-parser.js.map +1 -1
- package/dist/gateway/storage-gateway.controller.d.ts +12 -11
- package/dist/gateway/storage-gateway.controller.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.controller.js +240 -70
- package/dist/gateway/storage-gateway.controller.js.map +1 -1
- package/dist/gateway/storage-gateway.guard.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.guard.js.map +1 -1
- package/dist/gateway/storage-gateway.module.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.module.js +60 -1
- package/dist/gateway/storage-gateway.module.js.map +1 -1
- package/dist/gateway/storage-gateway.tokens.d.ts +1 -0
- package/dist/gateway/storage-gateway.tokens.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.tokens.js +1 -0
- package/dist/gateway/storage-gateway.tokens.js.map +1 -1
- package/dist/gateway/storage-gateway.types.d.ts +53 -10
- package/dist/gateway/storage-gateway.types.d.ts.map +1 -1
- package/dist/gateway/storage-gateway.types.js.map +1 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/inject-storage.decorator.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-upload-control.d.ts.map +1 -1
- package/dist/storage.client.d.ts +15 -3
- package/dist/storage.client.d.ts.map +1 -1
- package/dist/storage.client.js +204 -2
- package/dist/storage.client.js.map +1 -1
- package/dist/storage.driver.d.ts +28 -1
- package/dist/storage.driver.d.ts.map +1 -1
- package/dist/storage.driver.js.map +1 -1
- package/dist/storage.error.d.ts +17 -10
- package/dist/storage.error.d.ts.map +1 -1
- package/dist/storage.error.js +47 -1
- package/dist/storage.error.js.map +1 -1
- package/dist/storage.module.d.ts.map +1 -1
- package/dist/storage.module.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.tokens.js.map +1 -1
- package/dist/storage.types.d.ts +150 -1
- 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 +869 -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 +32 -0
- package/dist/workspace/storage-workspace.error.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.error.js +75 -0
- package/dist/workspace/storage-workspace.error.js.map +1 -0
- package/dist/workspace/storage-workspace.js +849 -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 +123 -0
- package/dist/workspace/storage-workspace.types.d.ts.map +1 -0
- package/dist/workspace/storage-workspace.types.js +22 -0
- package/dist/workspace/storage-workspace.types.js.map +1 -0
- package/package.json +103 -35
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
import { StorageClient } from '../storage.client.js';
|
|
2
|
+
import { isCanonicalStorageEtag } from '../storage-etag.js';
|
|
3
|
+
import { StorageErrorCode } from '../storage.error.js';
|
|
4
|
+
import { STORAGE_WORKSPACE_MAX_CURSOR_BYTES, decodeStorageWorkspaceCursor, issueStorageWorkspaceCursor, resolveStorageWorkspaceCursorConfiguration, } from './storage-workspace.cursor.js';
|
|
5
|
+
import { sanitizeWorkspaceError, workspaceError, } from './storage-workspace.error.js';
|
|
6
|
+
import { assertWorkspacePath, containsControlCharacter, isPathInside, joinWorkspacePath, workspaceBasename, } from './storage-workspace.path.js';
|
|
7
|
+
import { DEFAULT_STORAGE_WORKSPACE_LIMITS, STORAGE_WORKSPACE_PERMISSIONS, } from './storage-workspace.types.js';
|
|
8
|
+
const DEFAULT_PERMISSIONS = new Set([
|
|
9
|
+
'list',
|
|
10
|
+
'read',
|
|
11
|
+
'search',
|
|
12
|
+
]);
|
|
13
|
+
const PERMISSION_VALUES = new Set(STORAGE_WORKSPACE_PERMISSIONS);
|
|
14
|
+
const encoder = new TextEncoder();
|
|
15
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
16
|
+
const WORKSPACE_CONSTRUCTOR = Symbol('StorageWorkspace.constructor');
|
|
17
|
+
function operationOptions(options) {
|
|
18
|
+
if (options === undefined) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
...(options.retries !== undefined && { retries: options.retries }),
|
|
23
|
+
...(options.signal !== undefined && { signal: options.signal }),
|
|
24
|
+
...(options.timeout !== undefined && { timeout: options.timeout }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function positiveSafeInteger(value, label) {
|
|
28
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
29
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, `${label} must be a positive safe integer.`, { permanent: true });
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
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 });
|
|
36
|
+
}
|
|
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
|
+
}
|
|
64
|
+
function resolveLimits(requested, parent) {
|
|
65
|
+
const baseline = parent ?? DEFAULT_STORAGE_WORKSPACE_LIMITS;
|
|
66
|
+
const next = {
|
|
67
|
+
cursorTtlMs: requested?.cursorTtlMs ?? baseline.cursorTtlMs,
|
|
68
|
+
maxCursorBytes: requested?.maxCursorBytes ?? baseline.maxCursorBytes,
|
|
69
|
+
maxPageSize: requested?.maxPageSize ?? baseline.maxPageSize,
|
|
70
|
+
maxPathBytes: requested?.maxPathBytes ?? baseline.maxPathBytes,
|
|
71
|
+
maxReadBytes: requested?.maxReadBytes ?? baseline.maxReadBytes,
|
|
72
|
+
maxSearchResults: requested?.maxSearchResults ?? baseline.maxSearchResults,
|
|
73
|
+
maxSearchScan: requested?.maxSearchScan ?? baseline.maxSearchScan,
|
|
74
|
+
maxWriteBytes: requested?.maxWriteBytes ?? baseline.maxWriteBytes,
|
|
75
|
+
};
|
|
76
|
+
for (const [name, value] of Object.entries(next)) {
|
|
77
|
+
positiveSafeInteger(value, `limits.${name}`);
|
|
78
|
+
const parentValue = baseline[name];
|
|
79
|
+
if (parent !== undefined && value > parentValue) {
|
|
80
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, `Child workspace limit "${name}" cannot exceed its parent limit.`, { permanent: true });
|
|
81
|
+
}
|
|
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
|
+
}
|
|
86
|
+
return Object.freeze(next);
|
|
87
|
+
}
|
|
88
|
+
function resolvePermissions(requested, parent) {
|
|
89
|
+
const baseline = parent ?? DEFAULT_PERMISSIONS;
|
|
90
|
+
const values = requested === undefined ? [...baseline] : [...requested];
|
|
91
|
+
const permissions = new Set();
|
|
92
|
+
for (const permission of values) {
|
|
93
|
+
if (!PERMISSION_VALUES.has(permission)) {
|
|
94
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, `Unknown workspace permission "${String(permission)}".`, { permanent: true });
|
|
95
|
+
}
|
|
96
|
+
if (parent !== undefined && !parent.has(permission)) {
|
|
97
|
+
throw workspaceError(StorageErrorCode.UNAUTHORIZED, `Child workspace cannot add the "${permission}" permission.`, { permanent: true });
|
|
98
|
+
}
|
|
99
|
+
permissions.add(permission);
|
|
100
|
+
}
|
|
101
|
+
return permissions;
|
|
102
|
+
}
|
|
103
|
+
function logicalFile(metadata, path) {
|
|
104
|
+
if (!Number.isSafeInteger(metadata.size) ||
|
|
105
|
+
metadata.size < 0 ||
|
|
106
|
+
typeof metadata.contentType !== 'string' ||
|
|
107
|
+
metadata.contentType.length === 0 ||
|
|
108
|
+
(metadata.etag !== undefined && !isCanonicalStorageEtag(metadata.etag)) ||
|
|
109
|
+
(metadata.lastModified !== undefined &&
|
|
110
|
+
!Number.isFinite(new Date(metadata.lastModified).getTime()))) {
|
|
111
|
+
throw workspaceError(StorageErrorCode.PROVIDER, 'Storage provider returned malformed file metadata.', { permanent: true });
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
contentType: metadata.contentType,
|
|
115
|
+
...(metadata.etag !== undefined && { etag: metadata.etag }),
|
|
116
|
+
kind: 'file',
|
|
117
|
+
...(metadata.lastModified !== undefined && {
|
|
118
|
+
lastModified: new Date(metadata.lastModified),
|
|
119
|
+
}),
|
|
120
|
+
name: workspaceBasename(path),
|
|
121
|
+
path,
|
|
122
|
+
size: metadata.size,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function logicalDirectory(path) {
|
|
126
|
+
const normalized = path.endsWith('/') ? path.slice(0, -1) : path;
|
|
127
|
+
return {
|
|
128
|
+
kind: 'directory',
|
|
129
|
+
name: normalized.length === 0 ? '' : workspaceBasename(normalized),
|
|
130
|
+
path: normalized,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function segmentGlobMatches(pattern, value) {
|
|
134
|
+
let patternIndex = 0;
|
|
135
|
+
let valueIndex = 0;
|
|
136
|
+
let starIndex = -1;
|
|
137
|
+
let starValueIndex = -1;
|
|
138
|
+
while (valueIndex < value.length) {
|
|
139
|
+
if (patternIndex < pattern.length &&
|
|
140
|
+
(pattern[patternIndex] === '?' ||
|
|
141
|
+
pattern[patternIndex] === value[valueIndex])) {
|
|
142
|
+
patternIndex += 1;
|
|
143
|
+
valueIndex += 1;
|
|
144
|
+
}
|
|
145
|
+
else if (pattern[patternIndex] === '*') {
|
|
146
|
+
starIndex = patternIndex;
|
|
147
|
+
patternIndex += 1;
|
|
148
|
+
starValueIndex = valueIndex;
|
|
149
|
+
}
|
|
150
|
+
else if (starIndex !== -1) {
|
|
151
|
+
patternIndex = starIndex + 1;
|
|
152
|
+
starValueIndex += 1;
|
|
153
|
+
valueIndex = starValueIndex;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
while (pattern[patternIndex] === '*') {
|
|
160
|
+
patternIndex += 1;
|
|
161
|
+
}
|
|
162
|
+
return patternIndex === pattern.length;
|
|
163
|
+
}
|
|
164
|
+
function globMatches(pattern, value) {
|
|
165
|
+
const patternSegments = pattern.split('/');
|
|
166
|
+
const valueSegments = value.split('/');
|
|
167
|
+
let patternIndex = 0;
|
|
168
|
+
let valueIndex = 0;
|
|
169
|
+
let globstarIndex = -1;
|
|
170
|
+
let globstarValueIndex = -1;
|
|
171
|
+
while (valueIndex < valueSegments.length) {
|
|
172
|
+
const patternSegment = patternSegments[patternIndex];
|
|
173
|
+
const valueSegment = valueSegments[valueIndex];
|
|
174
|
+
if (patternSegment === '**') {
|
|
175
|
+
globstarIndex = patternIndex;
|
|
176
|
+
globstarValueIndex = valueIndex;
|
|
177
|
+
patternIndex += 1;
|
|
178
|
+
}
|
|
179
|
+
else if (patternSegment !== undefined &&
|
|
180
|
+
valueSegment !== undefined &&
|
|
181
|
+
segmentGlobMatches(patternSegment, valueSegment)) {
|
|
182
|
+
patternIndex += 1;
|
|
183
|
+
valueIndex += 1;
|
|
184
|
+
}
|
|
185
|
+
else if (globstarIndex !== -1) {
|
|
186
|
+
patternIndex = globstarIndex + 1;
|
|
187
|
+
globstarValueIndex += 1;
|
|
188
|
+
valueIndex = globstarValueIndex;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
while (patternSegments[patternIndex] === '**') {
|
|
195
|
+
patternIndex += 1;
|
|
196
|
+
}
|
|
197
|
+
return patternIndex === patternSegments.length;
|
|
198
|
+
}
|
|
199
|
+
function searchMatches(query, path, match, caseInsensitive) {
|
|
200
|
+
const candidate = caseInsensitive ? path.toLowerCase() : path;
|
|
201
|
+
const needle = caseInsensitive ? query.toLowerCase() : query;
|
|
202
|
+
if (match === 'exact') {
|
|
203
|
+
return candidate === needle;
|
|
204
|
+
}
|
|
205
|
+
if (match === 'substring') {
|
|
206
|
+
return candidate.includes(needle);
|
|
207
|
+
}
|
|
208
|
+
return globMatches(needle, candidate);
|
|
209
|
+
}
|
|
210
|
+
async function collectBoundedText(stream, announcedSize, maxBytes, path) {
|
|
211
|
+
if (announcedSize > maxBytes) {
|
|
212
|
+
await stream.cancel().catch(() => undefined);
|
|
213
|
+
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `Workspace file "${path}" exceeds the ${maxBytes}-byte read limit.`, { path, permanent: true });
|
|
214
|
+
}
|
|
215
|
+
const reader = stream.getReader();
|
|
216
|
+
const chunks = [];
|
|
217
|
+
let size = 0;
|
|
218
|
+
try {
|
|
219
|
+
while (true) {
|
|
220
|
+
const result = await reader.read();
|
|
221
|
+
if (result.done) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
if (result.value.byteLength === 0) {
|
|
225
|
+
throw workspaceError(StorageErrorCode.PROVIDER, 'Storage provider returned an empty non-terminal body chunk.', { path, permanent: true });
|
|
226
|
+
}
|
|
227
|
+
size += result.value.byteLength;
|
|
228
|
+
if (size > maxBytes) {
|
|
229
|
+
await reader.cancel().catch(() => undefined);
|
|
230
|
+
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `Workspace file "${path}" exceeded the ${maxBytes}-byte read limit.`, { path, permanent: true });
|
|
231
|
+
}
|
|
232
|
+
chunks.push(result.value);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
reader.releaseLock();
|
|
237
|
+
}
|
|
238
|
+
const bytes = new Uint8Array(size);
|
|
239
|
+
let offset = 0;
|
|
240
|
+
for (const chunk of chunks) {
|
|
241
|
+
bytes.set(chunk, offset);
|
|
242
|
+
offset += chunk.byteLength;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
return decoder.decode(bytes);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, `Workspace file "${path}" is not valid UTF-8 text.`, { path, permanent: true });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function collectBoundedBytes(stream, announcedSize, maxBytes, path) {
|
|
252
|
+
if (announcedSize > maxBytes) {
|
|
253
|
+
await stream.cancel().catch(() => undefined);
|
|
254
|
+
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `Workspace file "${path}" exceeds the ${maxBytes}-byte limit.`, { path, permanent: true });
|
|
255
|
+
}
|
|
256
|
+
const reader = stream.getReader();
|
|
257
|
+
const chunks = [];
|
|
258
|
+
let size = 0;
|
|
259
|
+
try {
|
|
260
|
+
while (true) {
|
|
261
|
+
const result = await reader.read();
|
|
262
|
+
if (result.done) {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
if (result.value.byteLength === 0) {
|
|
266
|
+
throw workspaceError(StorageErrorCode.PROVIDER, 'Storage provider returned an empty non-terminal body chunk.', { path, permanent: true });
|
|
267
|
+
}
|
|
268
|
+
size += result.value.byteLength;
|
|
269
|
+
if (size > maxBytes) {
|
|
270
|
+
await reader.cancel().catch(() => undefined);
|
|
271
|
+
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `Workspace file "${path}" exceeded the ${maxBytes}-byte limit.`, { path, permanent: true });
|
|
272
|
+
}
|
|
273
|
+
chunks.push(result.value);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
reader.releaseLock();
|
|
278
|
+
}
|
|
279
|
+
const bytes = new Uint8Array(size);
|
|
280
|
+
let offset = 0;
|
|
281
|
+
for (const chunk of chunks) {
|
|
282
|
+
bytes.set(chunk, offset);
|
|
283
|
+
offset += chunk.byteLength;
|
|
284
|
+
}
|
|
285
|
+
return bytes;
|
|
286
|
+
}
|
|
287
|
+
class StorageWorkspaceImplementation {
|
|
288
|
+
#state;
|
|
289
|
+
#permissions;
|
|
290
|
+
#limits;
|
|
291
|
+
/** @internal Construct workspaces through {@link mountStorageWorkspace}. */
|
|
292
|
+
constructor(constructorToken, state, permissions, limits) {
|
|
293
|
+
if (constructorToken !== WORKSPACE_CONSTRUCTOR) {
|
|
294
|
+
throw workspaceError(StorageErrorCode.UNAUTHORIZED, 'StorageWorkspace must be created by mountStorageWorkspace().', { permanent: true });
|
|
295
|
+
}
|
|
296
|
+
this.#state = state;
|
|
297
|
+
this.#permissions = permissions;
|
|
298
|
+
this.#limits = limits;
|
|
299
|
+
}
|
|
300
|
+
get permissions() {
|
|
301
|
+
return new Set(this.#permissions);
|
|
302
|
+
}
|
|
303
|
+
get limits() {
|
|
304
|
+
return Object.freeze({ ...this.#limits });
|
|
305
|
+
}
|
|
306
|
+
allows(permission) {
|
|
307
|
+
return this.#permissions.has(permission);
|
|
308
|
+
}
|
|
309
|
+
async stat(path, options) {
|
|
310
|
+
this.#require('read');
|
|
311
|
+
const logicalPath = this.#filePath(path);
|
|
312
|
+
try {
|
|
313
|
+
const metadata = await this.#state.client.head(this.#scope(logicalPath), options);
|
|
314
|
+
this.#assertResultPath(metadata.key, logicalPath);
|
|
315
|
+
return logicalFile(metadata, logicalPath);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
throw sanitizeWorkspaceError(error, {
|
|
319
|
+
operation: 'stat',
|
|
320
|
+
path: logicalPath,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async readText(path, options) {
|
|
325
|
+
this.#require('read');
|
|
326
|
+
const logicalPath = this.#filePath(path);
|
|
327
|
+
const maxBytes = options?.maxBytes ?? this.#limits.maxReadBytes;
|
|
328
|
+
positiveSafeInteger(maxBytes, 'maxBytes');
|
|
329
|
+
if (maxBytes > this.#limits.maxReadBytes) {
|
|
330
|
+
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `maxBytes cannot exceed the ${this.#limits.maxReadBytes}-byte workspace read limit.`, { path: logicalPath, permanent: true });
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const object = await this.#state.client.downloadStream(this.#scope(logicalPath), operationOptions(options));
|
|
334
|
+
this.#assertResultPath(object.key, logicalPath);
|
|
335
|
+
const text = await collectBoundedText(object.body, object.size, maxBytes, logicalPath);
|
|
336
|
+
return { ...logicalFile(object, logicalPath), text };
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
throw sanitizeWorkspaceError(error, {
|
|
340
|
+
operation: 'read',
|
|
341
|
+
path: logicalPath,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
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
|
+
}
|
|
366
|
+
async list(options = {}) {
|
|
367
|
+
this.#require('list');
|
|
368
|
+
const continuation = options.cursor !== undefined
|
|
369
|
+
? await decodeStorageWorkspaceCursor(this.#state.cursor, this.#cursorBinding(), 'list', options.cursor)
|
|
370
|
+
: undefined;
|
|
371
|
+
if (continuation !== undefined &&
|
|
372
|
+
((options.directory !== undefined &&
|
|
373
|
+
options.directory !== continuation.directory) ||
|
|
374
|
+
(options.limit !== undefined && options.limit !== continuation.limit) ||
|
|
375
|
+
(options.recursive !== undefined &&
|
|
376
|
+
options.recursive !== continuation.recursive))) {
|
|
377
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Workspace cursor conflicts with the supplied list query.', { permanent: true });
|
|
378
|
+
}
|
|
379
|
+
const directory = this.#directoryPath(continuation?.directory ?? options.directory ?? '');
|
|
380
|
+
const limit = this.#pageLimit(continuation?.limit ?? options.limit, this.#limits.maxPageSize);
|
|
381
|
+
const recursive = continuation?.recursive ?? options.recursive === true;
|
|
382
|
+
const scopedDirectory = this.#scopeDirectory(directory);
|
|
383
|
+
try {
|
|
384
|
+
const page = await this.#state.client.list({
|
|
385
|
+
...(continuation !== undefined && {
|
|
386
|
+
cursor: continuation.backendCursor,
|
|
387
|
+
}),
|
|
388
|
+
...(!recursive && { delimiter: '/' }),
|
|
389
|
+
limit,
|
|
390
|
+
...operationOptions(options),
|
|
391
|
+
prefix: scopedDirectory,
|
|
392
|
+
});
|
|
393
|
+
if (page.items.length + (page.prefixes?.length ?? 0) > limit) {
|
|
394
|
+
this.#malformedPage();
|
|
395
|
+
}
|
|
396
|
+
if (page.cursor !== undefined && page.cursor.length === 0) {
|
|
397
|
+
this.#malformedPage();
|
|
398
|
+
}
|
|
399
|
+
if (page.cursor !== undefined &&
|
|
400
|
+
(page.items.length + (page.prefixes?.length ?? 0) === 0 ||
|
|
401
|
+
page.cursor === continuation?.backendCursor)) {
|
|
402
|
+
this.#malformedPage();
|
|
403
|
+
}
|
|
404
|
+
const entries = [];
|
|
405
|
+
for (const item of page.items) {
|
|
406
|
+
const path = this.#unscoped(item.key);
|
|
407
|
+
if (!isPathInside(directory, path)) {
|
|
408
|
+
this.#providerScopeFailure();
|
|
409
|
+
}
|
|
410
|
+
if (!recursive) {
|
|
411
|
+
const relative = directory.length === 0 ? path : path.slice(directory.length + 1);
|
|
412
|
+
if (relative.includes('/')) {
|
|
413
|
+
this.#malformedPage();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
entries.push(logicalFile(item, path));
|
|
417
|
+
}
|
|
418
|
+
for (const prefix of page.prefixes ?? []) {
|
|
419
|
+
const path = this.#unscoped(prefix.endsWith('/') ? prefix.slice(0, -1) : prefix);
|
|
420
|
+
if (!isPathInside(directory, path)) {
|
|
421
|
+
this.#providerScopeFailure();
|
|
422
|
+
}
|
|
423
|
+
if (!recursive) {
|
|
424
|
+
const relative = directory.length === 0 ? path : path.slice(directory.length + 1);
|
|
425
|
+
if (relative.includes('/')) {
|
|
426
|
+
this.#malformedPage();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
entries.push(logicalDirectory(path));
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
...(page.cursor !== undefined && {
|
|
433
|
+
cursor: await issueStorageWorkspaceCursor(this.#state.cursor, this.#cursorBinding(), {
|
|
434
|
+
backendCursor: page.cursor,
|
|
435
|
+
directory,
|
|
436
|
+
limit,
|
|
437
|
+
operation: 'list',
|
|
438
|
+
recursive,
|
|
439
|
+
}),
|
|
440
|
+
}),
|
|
441
|
+
entries,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
throw sanitizeWorkspaceError(error, {
|
|
446
|
+
operation: 'list',
|
|
447
|
+
...(directory.length > 0 && { path: directory }),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async search(query, options = {}) {
|
|
452
|
+
this.#require('search');
|
|
453
|
+
const continuation = options.cursor !== undefined
|
|
454
|
+
? await decodeStorageWorkspaceCursor(this.#state.cursor, this.#cursorBinding(), 'search', options.cursor)
|
|
455
|
+
: undefined;
|
|
456
|
+
if (continuation !== undefined &&
|
|
457
|
+
((query.length > 0 && query !== continuation.query) ||
|
|
458
|
+
(options.directory !== undefined &&
|
|
459
|
+
options.directory !== continuation.directory) ||
|
|
460
|
+
(options.match !== undefined && options.match !== continuation.match) ||
|
|
461
|
+
(options.caseInsensitive !== undefined &&
|
|
462
|
+
options.caseInsensitive !== continuation.caseInsensitive) ||
|
|
463
|
+
(options.limit !== undefined && options.limit !== continuation.limit))) {
|
|
464
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Workspace cursor conflicts with the supplied search query.', { permanent: true });
|
|
465
|
+
}
|
|
466
|
+
query = continuation?.query ?? query;
|
|
467
|
+
if (typeof query !== 'string' || query.length === 0) {
|
|
468
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Search query must be a non-empty string.', { permanent: true });
|
|
469
|
+
}
|
|
470
|
+
if (query.includes('\\') || containsControlCharacter(query)) {
|
|
471
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Search query contains forbidden characters.', { permanent: true });
|
|
472
|
+
}
|
|
473
|
+
if (encoder.encode(query).byteLength > this.#limits.maxPathBytes) {
|
|
474
|
+
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, 'Search query exceeds the workspace path-byte limit.', { permanent: true });
|
|
475
|
+
}
|
|
476
|
+
const directory = this.#directoryPath(continuation?.directory ?? options.directory ?? '');
|
|
477
|
+
const match = continuation?.match ?? options.match ?? 'glob';
|
|
478
|
+
if (match !== 'glob' && match !== 'substring' && match !== 'exact') {
|
|
479
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Search match must be glob, substring, or exact.', { permanent: true });
|
|
480
|
+
}
|
|
481
|
+
const caseInsensitive = continuation?.caseInsensitive ?? options.caseInsensitive === true;
|
|
482
|
+
const limit = this.#pageLimit(continuation?.limit ?? options.limit, this.#limits.maxSearchResults);
|
|
483
|
+
const prior = {
|
|
484
|
+
backendCursor: continuation?.backendCursor,
|
|
485
|
+
scanned: continuation?.scanned ?? 0,
|
|
486
|
+
};
|
|
487
|
+
return this.#searchPage(query, {
|
|
488
|
+
caseInsensitive,
|
|
489
|
+
directory,
|
|
490
|
+
limit,
|
|
491
|
+
match,
|
|
492
|
+
operation: operationOptions(options),
|
|
493
|
+
state: prior,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
async writeFile(path, body, options) {
|
|
497
|
+
const mode = writeModeOf(options);
|
|
498
|
+
this.#require(mode === 'overwrite' ? 'write' : mode);
|
|
499
|
+
const logicalPath = this.#filePath(path);
|
|
500
|
+
if (typeof body !== 'string' && !(body instanceof Uint8Array)) {
|
|
501
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Workspace writes accept only strings or Uint8Array bodies.', { path: logicalPath, permanent: true });
|
|
502
|
+
}
|
|
503
|
+
const size = typeof body === 'string'
|
|
504
|
+
? encoder.encode(body).byteLength
|
|
505
|
+
: body.byteLength;
|
|
506
|
+
if (size > this.#limits.maxWriteBytes) {
|
|
507
|
+
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `Workspace write exceeds the ${this.#limits.maxWriteBytes}-byte limit.`, { path: logicalPath, permanent: true });
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
const common = {
|
|
511
|
+
...(options.contentType !== undefined && {
|
|
512
|
+
contentType: options.contentType,
|
|
513
|
+
}),
|
|
514
|
+
...(options.metadata !== undefined && { metadata: options.metadata }),
|
|
515
|
+
...operationOptions(options),
|
|
516
|
+
};
|
|
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, {
|
|
528
|
+
...common,
|
|
529
|
+
condition: { etag, type: 'replace' },
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
this.#assertResultPath(result.key, logicalPath);
|
|
533
|
+
return logicalFile(result, logicalPath);
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
throw sanitizeWorkspaceError(error, {
|
|
537
|
+
operation: mode,
|
|
538
|
+
path: logicalPath,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async copyFile(source, destination, options) {
|
|
543
|
+
this.#require('copy');
|
|
544
|
+
this.#require('read');
|
|
545
|
+
const overwrite = hasExplicitMode(options, 'overwrite', 'Copy');
|
|
546
|
+
this.#require(overwrite ? 'write' : 'create');
|
|
547
|
+
const sourcePath = this.#filePath(source, 'source path');
|
|
548
|
+
const destinationPath = this.#filePath(destination, 'destination path');
|
|
549
|
+
if (sourcePath === destinationPath) {
|
|
550
|
+
throw workspaceError(StorageErrorCode.CONFLICT, 'Copy destination must differ from its source.', { path: destinationPath, permanent: true });
|
|
551
|
+
}
|
|
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 });
|
|
561
|
+
}
|
|
562
|
+
return this.#copyCreate(sourcePath, destinationPath, conditional);
|
|
563
|
+
}
|
|
564
|
+
async moveFile(source, destination, options) {
|
|
565
|
+
this.#require('move');
|
|
566
|
+
this.#require('read');
|
|
567
|
+
this.#require('create');
|
|
568
|
+
this.#require('delete');
|
|
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 });
|
|
571
|
+
}
|
|
572
|
+
const sourcePath = this.#filePath(source, 'source path');
|
|
573
|
+
const destinationPath = this.#filePath(destination, 'destination path');
|
|
574
|
+
if (sourcePath === destinationPath) {
|
|
575
|
+
throw workspaceError(StorageErrorCode.CONFLICT, 'Move destination must differ from its source.', { path: destinationPath, permanent: true });
|
|
576
|
+
}
|
|
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');
|
|
584
|
+
const copied = await this.#copyCreate(sourcePath, destinationPath, options);
|
|
585
|
+
if (copied.etag === undefined) {
|
|
586
|
+
throw workspaceError(StorageErrorCode.NOT_SUPPORTED, 'Safe move requires the destination provider to return an etag.', { path: destinationPath, permanent: true });
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
await this.#state.client.deleteConditional(this.#scope(sourcePath), {
|
|
590
|
+
condition: { etag: options.etag },
|
|
591
|
+
...operationOptions(options),
|
|
592
|
+
});
|
|
593
|
+
return copied;
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
throw workspaceError(StorageErrorCode.CONFLICT, `Workspace move could not confirm source deletion; destination "${destinationPath}" was retained. Inspect both paths before retrying.`, {
|
|
597
|
+
applied: true,
|
|
598
|
+
appliedEtag: copied.etag,
|
|
599
|
+
operation: 'move',
|
|
600
|
+
path: destinationPath,
|
|
601
|
+
permanent: true,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async deleteFile(path, options) {
|
|
606
|
+
this.#require('delete');
|
|
607
|
+
const logicalPath = this.#filePath(path);
|
|
608
|
+
const unconditional = hasExplicitMode(options, 'unconditional', 'Delete');
|
|
609
|
+
if (unconditional) {
|
|
610
|
+
this.#require('write');
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
if (unconditional) {
|
|
614
|
+
await this.#state.client.delete(this.#scope(logicalPath), operationOptions(options));
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
const conditional = options;
|
|
618
|
+
assertEtag(conditional.etag, 'Delete');
|
|
619
|
+
await this.#state.client.deleteConditional(this.#scope(logicalPath), {
|
|
620
|
+
condition: { etag: conditional.etag },
|
|
621
|
+
...operationOptions(conditional),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
throw sanitizeWorkspaceError(error, {
|
|
627
|
+
operation: 'delete',
|
|
628
|
+
path: logicalPath,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
mount(directory, options = {}) {
|
|
633
|
+
const path = this.#directoryPath(directory);
|
|
634
|
+
if (path.length === 0) {
|
|
635
|
+
throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'A child workspace mount directory must not be empty.', { permanent: true });
|
|
636
|
+
}
|
|
637
|
+
return new StorageWorkspaceImplementation(WORKSPACE_CONSTRUCTOR, {
|
|
638
|
+
...this.#state,
|
|
639
|
+
prefix: joinWorkspacePath(this.#state.prefix, path),
|
|
640
|
+
}, resolvePermissions(options.permissions, this.#permissions), resolveLimits(options.limits, this.#limits));
|
|
641
|
+
}
|
|
642
|
+
async #copyCreate(sourcePath, destinationPath, options) {
|
|
643
|
+
try {
|
|
644
|
+
const source = await this.#state.client.downloadConditional(this.#scope(sourcePath), {
|
|
645
|
+
condition: { etag: options.etag },
|
|
646
|
+
...operationOptions(options),
|
|
647
|
+
});
|
|
648
|
+
this.#assertResultPath(source.key, sourcePath);
|
|
649
|
+
if (source.etag !== options.etag) {
|
|
650
|
+
await source.body.cancel().catch(() => undefined);
|
|
651
|
+
throw workspaceError(StorageErrorCode.PROVIDER, 'Conditional read returned an unexpected source ETag.', { operation: 'copy', path: sourcePath, permanent: true });
|
|
652
|
+
}
|
|
653
|
+
const bytes = await collectBoundedBytes(source.body, source.size, this.#limits.maxWriteBytes, sourcePath);
|
|
654
|
+
const result = await this.#state.client.uploadConditional(this.#scope(destinationPath), bytes, {
|
|
655
|
+
condition: { type: 'create' },
|
|
656
|
+
contentType: source.contentType,
|
|
657
|
+
...(source.metadata !== undefined && { metadata: source.metadata }),
|
|
658
|
+
...operationOptions(options),
|
|
659
|
+
});
|
|
660
|
+
this.#assertResultPath(result.key, destinationPath);
|
|
661
|
+
return logicalFile(result, destinationPath);
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
throw sanitizeWorkspaceError(error, {
|
|
665
|
+
operation: 'copy',
|
|
666
|
+
path: destinationPath,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
async #copyOverwrite(sourcePath, destinationPath, options) {
|
|
671
|
+
try {
|
|
672
|
+
const source = await this.#state.client.downloadStream(this.#scope(sourcePath), operationOptions(options));
|
|
673
|
+
this.#assertResultPath(source.key, sourcePath);
|
|
674
|
+
const bytes = await collectBoundedBytes(source.body, source.size, this.#limits.maxWriteBytes, sourcePath);
|
|
675
|
+
const result = await this.#state.client.upload(this.#scope(destinationPath), bytes, {
|
|
676
|
+
contentType: source.contentType,
|
|
677
|
+
...(source.metadata !== undefined && { metadata: source.metadata }),
|
|
678
|
+
...operationOptions(options),
|
|
679
|
+
});
|
|
680
|
+
this.#assertResultPath(result.key, destinationPath);
|
|
681
|
+
return logicalFile(result, destinationPath);
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
throw sanitizeWorkspaceError(error, {
|
|
685
|
+
operation: 'copy',
|
|
686
|
+
path: destinationPath,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async #searchPage(query, context) {
|
|
691
|
+
const entries = [];
|
|
692
|
+
let backendCursor = context.state.backendCursor;
|
|
693
|
+
let scanned = context.state.scanned;
|
|
694
|
+
try {
|
|
695
|
+
while (entries.length < context.limit &&
|
|
696
|
+
scanned < this.#limits.maxSearchScan) {
|
|
697
|
+
const remaining = this.#limits.maxSearchScan - scanned;
|
|
698
|
+
const remainingResults = context.limit - entries.length;
|
|
699
|
+
const page = await this.#state.client.list({
|
|
700
|
+
...(backendCursor !== undefined && { cursor: backendCursor }),
|
|
701
|
+
limit: Math.min(this.#limits.maxPageSize, remaining, remainingResults),
|
|
702
|
+
...context.operation,
|
|
703
|
+
prefix: this.#scopeDirectory(context.directory),
|
|
704
|
+
});
|
|
705
|
+
const requested = Math.min(this.#limits.maxPageSize, remaining, remainingResults);
|
|
706
|
+
if (page.items.length > requested) {
|
|
707
|
+
this.#malformedPage();
|
|
708
|
+
}
|
|
709
|
+
if ((page.prefixes?.length ?? 0) > 0) {
|
|
710
|
+
this.#malformedPage();
|
|
711
|
+
}
|
|
712
|
+
if (page.cursor !== undefined && page.cursor.length === 0) {
|
|
713
|
+
this.#malformedPage();
|
|
714
|
+
}
|
|
715
|
+
if (page.cursor !== undefined &&
|
|
716
|
+
(page.items.length === 0 || page.cursor === backendCursor)) {
|
|
717
|
+
this.#malformedPage();
|
|
718
|
+
}
|
|
719
|
+
for (let index = 0; index < page.items.length; index += 1) {
|
|
720
|
+
const item = page.items[index];
|
|
721
|
+
if (item === undefined) {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
scanned += 1;
|
|
725
|
+
const path = this.#unscoped(item.key);
|
|
726
|
+
if (!isPathInside(context.directory, path)) {
|
|
727
|
+
this.#providerScopeFailure();
|
|
728
|
+
}
|
|
729
|
+
if (searchMatches(query, context.directory.length === 0
|
|
730
|
+
? path
|
|
731
|
+
: path.slice(context.directory.length + 1), context.match, context.caseInsensitive)) {
|
|
732
|
+
entries.push(logicalFile(item, path));
|
|
733
|
+
}
|
|
734
|
+
if (entries.length === context.limit ||
|
|
735
|
+
scanned === this.#limits.maxSearchScan) {
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
backendCursor = page.cursor;
|
|
740
|
+
if (backendCursor === undefined) {
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const hasMore = backendCursor !== undefined;
|
|
745
|
+
if (scanned >= this.#limits.maxSearchScan && hasMore) {
|
|
746
|
+
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `Search exceeded the ${this.#limits.maxSearchScan}-object scan limit.`, { permanent: true });
|
|
747
|
+
}
|
|
748
|
+
const nextBackendCursor = backendCursor;
|
|
749
|
+
return {
|
|
750
|
+
...(nextBackendCursor !== undefined && {
|
|
751
|
+
cursor: await issueStorageWorkspaceCursor(this.#state.cursor, this.#cursorBinding(), {
|
|
752
|
+
backendCursor: nextBackendCursor,
|
|
753
|
+
caseInsensitive: context.caseInsensitive,
|
|
754
|
+
directory: context.directory,
|
|
755
|
+
limit: context.limit,
|
|
756
|
+
match: context.match,
|
|
757
|
+
operation: 'search',
|
|
758
|
+
query,
|
|
759
|
+
scanned,
|
|
760
|
+
}),
|
|
761
|
+
}),
|
|
762
|
+
entries,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
throw sanitizeWorkspaceError(error, {
|
|
767
|
+
operation: 'search',
|
|
768
|
+
...(context.directory.length > 0 && { path: context.directory }),
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
#require(permission) {
|
|
773
|
+
if (!this.#permissions.has(permission)) {
|
|
774
|
+
throw workspaceError(StorageErrorCode.UNAUTHORIZED, `Workspace does not allow ${permission} operations.`, { permanent: true });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
#filePath(path, label = 'path') {
|
|
778
|
+
return assertWorkspacePath(path, this.#limits.maxPathBytes, {
|
|
779
|
+
allowRoot: false,
|
|
780
|
+
label,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
#directoryPath(path) {
|
|
784
|
+
return assertWorkspacePath(path, this.#limits.maxPathBytes, {
|
|
785
|
+
allowRoot: true,
|
|
786
|
+
label: 'directory',
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
#scope(path) {
|
|
790
|
+
return `${this.#state.prefix}/${path}`;
|
|
791
|
+
}
|
|
792
|
+
#scopeDirectory(directory) {
|
|
793
|
+
return directory.length === 0
|
|
794
|
+
? `${this.#state.prefix}/`
|
|
795
|
+
: `${this.#state.prefix}/${directory}/`;
|
|
796
|
+
}
|
|
797
|
+
#unscoped(key) {
|
|
798
|
+
const scoped = `${this.#state.prefix}/`;
|
|
799
|
+
if (!key.startsWith(scoped)) {
|
|
800
|
+
this.#providerScopeFailure();
|
|
801
|
+
}
|
|
802
|
+
const path = key.slice(scoped.length);
|
|
803
|
+
return assertWorkspacePath(path, this.#limits.maxPathBytes, {
|
|
804
|
+
allowRoot: false,
|
|
805
|
+
label: 'backend result path',
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
#assertResultPath(key, expected) {
|
|
809
|
+
if (this.#unscoped(key) !== expected) {
|
|
810
|
+
this.#providerScopeFailure();
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
#providerScopeFailure() {
|
|
814
|
+
throw workspaceError(StorageErrorCode.PROVIDER, 'Storage provider returned data outside the mounted workspace.', { permanent: true });
|
|
815
|
+
}
|
|
816
|
+
#malformedPage() {
|
|
817
|
+
throw workspaceError(StorageErrorCode.PROVIDER, 'Storage provider returned a malformed workspace page.', { permanent: true });
|
|
818
|
+
}
|
|
819
|
+
#pageLimit(value, maximum) {
|
|
820
|
+
const limit = value ?? maximum;
|
|
821
|
+
positiveSafeInteger(limit, 'limit');
|
|
822
|
+
if (limit > maximum) {
|
|
823
|
+
throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, `limit cannot exceed the workspace maximum of ${maximum}.`, { permanent: true });
|
|
824
|
+
}
|
|
825
|
+
return limit;
|
|
826
|
+
}
|
|
827
|
+
#cursorBinding() {
|
|
828
|
+
return {
|
|
829
|
+
limits: this.#limits,
|
|
830
|
+
prefix: this.#state.prefix,
|
|
831
|
+
store: this.#state.client.name,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
export function mountStorageWorkspace(client, options) {
|
|
836
|
+
const limits = resolveLimits(options.limits);
|
|
837
|
+
const prefix = assertWorkspacePath(options.prefix, limits.maxPathBytes, {
|
|
838
|
+
allowRoot: false,
|
|
839
|
+
label: 'workspace prefix',
|
|
840
|
+
});
|
|
841
|
+
return new StorageWorkspaceImplementation(WORKSPACE_CONSTRUCTOR, {
|
|
842
|
+
client,
|
|
843
|
+
cursor: resolveStorageWorkspaceCursorConfiguration(options.cursor),
|
|
844
|
+
prefix,
|
|
845
|
+
}, resolvePermissions(options.permissions), limits);
|
|
846
|
+
}
|
|
847
|
+
/** @deprecated Prefer {@link mountStorageWorkspace}. */
|
|
848
|
+
export const createStorageWorkspace = mountStorageWorkspace;
|
|
849
|
+
//# sourceMappingURL=storage-workspace.js.map
|