@statewalker/webrun-files-s3 0.5.0 → 0.9.0

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.
@@ -1,356 +1,166 @@
1
- let __aws_sdk_client_s3 = require("@aws-sdk/client-s3");
2
- let __statewalker_webrun_files = require("@statewalker/webrun-files");
3
-
4
- //#region src/s3-file-handle.ts
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
3
+ let _statewalker_webrun_files = require("@statewalker/webrun-files");
4
+ //#region src/s3-files-api.ts
5
+ /** Default part size for multipart uploads: 5MB (S3 minimum) */
6
+ const DEFAULT_PART_SIZE = 5242880;
7
+ /** Threshold for using multipart upload: 5MB */
8
+ const MULTIPART_THRESHOLD = 5242880;
5
9
  /**
6
- * Helper to stream S3 response body as async iterable of Uint8Array chunks.
7
- * Handles both Node.js Readable streams and browser ReadableStreams.
8
- * Yields chunks as they arrive without buffering the entire response.
10
+ * S3 filesystem implementation of FilesApi.
11
+ *
12
+ * Provides a filesystem-like interface over S3 object storage.
13
+ * Works with AWS S3 and S3-compatible services.
9
14
  */
10
- async function* streamBody(body) {
11
- if (!body) return;
12
- if (Symbol.asyncIterator in body) {
13
- for await (const chunk of body) yield chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
14
- return;
15
- }
16
- if (body instanceof Uint8Array) {
17
- yield body;
18
- return;
19
- }
20
- if (body instanceof ArrayBuffer) {
21
- yield new Uint8Array(body);
22
- return;
23
- }
24
- throw new Error("Unsupported body type");
25
- }
26
- var S3FileHandle = class {
15
+ var S3FilesApi = class {
27
16
  client;
28
17
  bucket;
29
- key;
30
- _size;
31
- multipartPartSize;
32
- closed = false;
18
+ prefix;
19
+ partSize;
33
20
  constructor(options) {
34
21
  this.client = options.client;
35
22
  this.bucket = options.bucket;
36
- this.key = options.key;
37
- this._size = options.size;
38
- this.multipartPartSize = options.multipartPartSize;
39
- }
40
- get size() {
41
- return this._size;
23
+ this.prefix = (options.prefix ?? "").replace(/^\/+|\/+$/g, "");
24
+ this.partSize = options.multipartPartSize ?? DEFAULT_PART_SIZE;
42
25
  }
43
- async close() {
44
- this.closed = true;
26
+ resolveKey(path) {
27
+ const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
28
+ const relativePath = normalized.startsWith("/") ? normalized.substring(1) : normalized;
29
+ if (this.prefix) return relativePath ? `${this.prefix}/${relativePath}` : this.prefix;
30
+ return relativePath;
45
31
  }
46
- /**
47
- * Reads a range of bytes from the S3 object.
48
- * Uses HTTP Range header for efficient partial reads.
49
- * Streams data directly from S3 without buffering the entire response.
50
- */
51
- async *createReadStream(options = {}) {
52
- if (this.closed) throw new Error("FileHandle is closed");
53
- const { start = 0, end = Infinity, signal } = options;
54
- const actualEnd = Math.min(end, this._size);
55
- if (start >= actualEnd || this._size === 0) return;
56
- const command = new __aws_sdk_client_s3.GetObjectCommand({
57
- Bucket: this.bucket,
58
- Key: this.key,
59
- Range: `bytes=${start}-${actualEnd - 1}`
60
- });
61
- const response = await this.client.send(command, { abortSignal: signal });
62
- if (!response.Body) return;
63
- for await (const chunk of streamBody(response.Body)) {
64
- if (signal?.aborted) throw new Error("Operation aborted");
65
- yield chunk;
32
+ keyToPath(key) {
33
+ let relativePath = key;
34
+ if (this.prefix && key.startsWith(this.prefix)) {
35
+ relativePath = key.substring(this.prefix.length);
36
+ if (relativePath.startsWith("/")) relativePath = relativePath.substring(1);
66
37
  }
38
+ return `/${relativePath}`;
67
39
  }
68
- /**
69
- * Writes content to the S3 object using streaming.
70
- *
71
- * S3 objects are immutable, so this operation replaces the entire object.
72
- * Uses streaming multipart upload to avoid buffering the entire file in memory.
73
- * Only buffers one part at a time (5MB by default).
74
- *
75
- * Note: start position > 0 uses UploadPartCopy to preserve existing content
76
- * without downloading it.
77
- */
78
- async createWriteStream(data, options = {}) {
79
- if (this.closed) throw new Error("FileHandle is closed");
80
- const { start = 0, signal } = options;
81
- return this.streamingMultipartUpload(data, start, signal);
82
- }
83
- /**
84
- * Appends content to the end of the S3 object using streaming.
85
- *
86
- * Uses UploadPartCopy to preserve existing content without downloading it,
87
- * then streams new data as additional parts.
88
- */
89
- async appendFile(data, options = {}) {
90
- if (this.closed) throw new Error("FileHandle is closed");
91
- const { signal } = options;
92
- return this.streamingMultipartUpload(data, this._size, signal);
40
+ async *read(path, options) {
41
+ const key = this.resolveKey(path);
42
+ try {
43
+ const start = options?.start ?? 0;
44
+ const length = options?.length;
45
+ if (length === 0) return;
46
+ let range;
47
+ if (start > 0 || length !== void 0) range = `bytes=${start}-${length !== void 0 ? start + length - 1 : ""}`;
48
+ const command = new _aws_sdk_client_s3.GetObjectCommand({
49
+ Bucket: this.bucket,
50
+ Key: key,
51
+ Range: range
52
+ });
53
+ const response = await this.client.send(command);
54
+ if (response.Body) {
55
+ const stream = response.Body;
56
+ for await (const chunk of stream) yield chunk;
57
+ }
58
+ } catch (error) {
59
+ const err = error;
60
+ if (err.name === "NotFound" || err.name === "InvalidRange" || err.$metadata?.httpStatusCode === 404 || err.$metadata?.httpStatusCode === 416) return;
61
+ throw error;
62
+ }
93
63
  }
94
- /**
95
- * Streaming multipart upload that buffers only one part at a time.
96
- * Uses UploadPartCopy to preserve existing content without downloading it
97
- * for large files. For small existing content (< part size), downloads and
98
- * combines with new data to meet S3's minimum part size requirement.
99
- */
100
- async streamingMultipartUpload(data, start, signal) {
101
- const createCommand = new __aws_sdk_client_s3.CreateMultipartUploadCommand({
102
- Bucket: this.bucket,
103
- Key: this.key
104
- });
105
- const { UploadId } = await this.client.send(createCommand, { abortSignal: signal });
106
- if (!UploadId) throw new Error("Failed to initiate multipart upload");
64
+ async write(path, content) {
65
+ const key = this.resolveKey(path);
66
+ let buffer = [];
67
+ let bufferSize = 0;
68
+ let uploadId;
107
69
  const parts = [];
108
70
  let partNumber = 1;
109
- let bytesWritten = 0;
110
- let totalSize = 0;
111
- let buffer = new Uint8Array(this.multipartPartSize);
112
- let bufferOffset = 0;
113
- try {
114
- if (start > 0 && this._size > 0) {
115
- const preserveEnd = Math.min(start, this._size);
116
- const fullPartsEnd = Math.floor(preserveEnd / this.multipartPartSize) * this.multipartPartSize;
117
- let copyOffset = 0;
118
- while (copyOffset < fullPartsEnd) {
119
- if (signal?.aborted) throw new Error("Operation aborted");
120
- const copyEnd = copyOffset + this.multipartPartSize;
121
- const copyCommand = new __aws_sdk_client_s3.UploadPartCopyCommand({
122
- Bucket: this.bucket,
123
- Key: this.key,
124
- UploadId,
125
- PartNumber: partNumber,
126
- CopySource: `${this.bucket}/${this.key}`,
127
- CopySourceRange: `bytes=${copyOffset}-${copyEnd - 1}`
128
- });
129
- const { CopyPartResult } = await this.client.send(copyCommand, { abortSignal: signal });
130
- if (CopyPartResult?.ETag) {
131
- parts.push({
132
- ETag: CopyPartResult.ETag,
133
- PartNumber: partNumber
134
- });
135
- partNumber++;
136
- totalSize += this.multipartPartSize;
137
- }
138
- copyOffset = copyEnd;
139
- }
140
- if (fullPartsEnd < preserveEnd) {
141
- const remainingBytes = await this.downloadRange(fullPartsEnd, preserveEnd, signal);
142
- buffer.set(remainingBytes, bufferOffset);
143
- bufferOffset += remainingBytes.length;
144
- totalSize += remainingBytes.length;
145
- }
146
- if (start > this._size) {
147
- const paddingSize = start - this._size;
148
- let paddingOffset = 0;
149
- while (paddingOffset < paddingSize) {
150
- const spaceInBuffer = this.multipartPartSize - bufferOffset;
151
- const bytesToAdd = Math.min(spaceInBuffer, paddingSize - paddingOffset);
152
- bufferOffset += bytesToAdd;
153
- paddingOffset += bytesToAdd;
154
- totalSize += bytesToAdd;
155
- if (bufferOffset >= this.multipartPartSize) {
156
- await this.uploadPart(UploadId, partNumber, buffer, parts, signal);
157
- partNumber++;
158
- buffer = new Uint8Array(this.multipartPartSize);
159
- bufferOffset = 0;
160
- }
161
- }
162
- }
163
- } else if (start > 0) {
164
- let paddingOffset = 0;
165
- while (paddingOffset < start) {
166
- const spaceInBuffer = this.multipartPartSize - bufferOffset;
167
- const bytesToAdd = Math.min(spaceInBuffer, start - paddingOffset);
168
- bufferOffset += bytesToAdd;
169
- paddingOffset += bytesToAdd;
170
- totalSize += bytesToAdd;
171
- if (bufferOffset >= this.multipartPartSize) {
172
- await this.uploadPart(UploadId, partNumber, buffer, parts, signal);
173
- partNumber++;
174
- buffer = new Uint8Array(this.multipartPartSize);
175
- bufferOffset = 0;
176
- }
177
- }
71
+ const flushPart = async (isLast) => {
72
+ if (buffer.length === 0) return;
73
+ const partLength = buffer.reduce((sum, c) => sum + c.length, 0);
74
+ const partBuffer = new Uint8Array(partLength);
75
+ let offset = 0;
76
+ for (const chunk of buffer) {
77
+ partBuffer.set(chunk, offset);
78
+ offset += chunk.length;
178
79
  }
179
- for await (const chunk of (0, __statewalker_webrun_files.toBinaryAsyncIterable)(data)) {
180
- if (signal?.aborted) throw new Error("Operation aborted");
181
- let chunkOffset = 0;
182
- while (chunkOffset < chunk.length) {
183
- const spaceInBuffer = this.multipartPartSize - bufferOffset;
184
- const bytesToCopy = Math.min(spaceInBuffer, chunk.length - chunkOffset);
185
- buffer.set(chunk.subarray(chunkOffset, chunkOffset + bytesToCopy), bufferOffset);
186
- bufferOffset += bytesToCopy;
187
- chunkOffset += bytesToCopy;
188
- bytesWritten += bytesToCopy;
189
- totalSize += bytesToCopy;
190
- if (bufferOffset >= this.multipartPartSize) {
191
- await this.uploadPart(UploadId, partNumber, buffer, parts, signal);
192
- partNumber++;
193
- buffer = new Uint8Array(this.multipartPartSize);
194
- bufferOffset = 0;
195
- }
196
- }
80
+ buffer = [];
81
+ bufferSize = 0;
82
+ if (!uploadId && isLast && partLength < MULTIPART_THRESHOLD) {
83
+ const command = new _aws_sdk_client_s3.PutObjectCommand({
84
+ Bucket: this.bucket,
85
+ Key: key,
86
+ Body: partBuffer,
87
+ ContentLength: partLength
88
+ });
89
+ await this.client.send(command);
90
+ return;
197
91
  }
198
- if (bufferOffset > 0) {
199
- const lastPart = buffer.subarray(0, bufferOffset);
200
- await this.uploadPart(UploadId, partNumber, lastPart, parts, signal);
92
+ if (!uploadId) {
93
+ const createCommand = new _aws_sdk_client_s3.CreateMultipartUploadCommand({
94
+ Bucket: this.bucket,
95
+ Key: key
96
+ });
97
+ const { UploadId } = await this.client.send(createCommand);
98
+ if (!UploadId) throw new Error("Failed to create multipart upload");
99
+ uploadId = UploadId;
201
100
  }
202
- if (parts.length === 0) await this.uploadPart(UploadId, 1, new Uint8Array(0), parts, signal);
203
- const completeCommand = new __aws_sdk_client_s3.CompleteMultipartUploadCommand({
101
+ const uploadCommand = new _aws_sdk_client_s3.UploadPartCommand({
204
102
  Bucket: this.bucket,
205
- Key: this.key,
206
- UploadId,
207
- MultipartUpload: { Parts: parts }
103
+ Key: key,
104
+ UploadId: uploadId,
105
+ PartNumber: partNumber,
106
+ Body: partBuffer
107
+ });
108
+ const response = await this.client.send(uploadCommand);
109
+ if (!response.ETag) throw new Error(`Failed to upload part ${partNumber}`);
110
+ parts.push({
111
+ ETag: response.ETag,
112
+ PartNumber: partNumber
208
113
  });
209
- await this.client.send(completeCommand, { abortSignal: signal });
210
- this._size = totalSize;
211
- return bytesWritten;
114
+ partNumber++;
115
+ };
116
+ try {
117
+ for await (const chunk of content) {
118
+ buffer.push(chunk);
119
+ bufferSize += chunk.length;
120
+ if (bufferSize >= this.partSize) await flushPart(false);
121
+ }
122
+ await flushPart(true);
123
+ if (uploadId) {
124
+ const completeCommand = new _aws_sdk_client_s3.CompleteMultipartUploadCommand({
125
+ Bucket: this.bucket,
126
+ Key: key,
127
+ UploadId: uploadId,
128
+ MultipartUpload: { Parts: parts.sort((a, b) => a.PartNumber - b.PartNumber) }
129
+ });
130
+ await this.client.send(completeCommand);
131
+ }
212
132
  } catch (error) {
213
- await this.abortMultipartUpload(UploadId);
133
+ if (uploadId) {
134
+ const abortCommand = new _aws_sdk_client_s3.AbortMultipartUploadCommand({
135
+ Bucket: this.bucket,
136
+ Key: key,
137
+ UploadId: uploadId
138
+ });
139
+ try {
140
+ await this.client.send(abortCommand);
141
+ } catch {}
142
+ }
214
143
  throw error;
215
144
  }
216
145
  }
217
- /**
218
- * Download a range of bytes from the S3 object.
219
- */
220
- async downloadRange(start, end, signal) {
221
- if (end <= start) return new Uint8Array(0);
222
- const command = new __aws_sdk_client_s3.GetObjectCommand({
146
+ async mkdir(path) {
147
+ const key = this.resolveKey(path);
148
+ const dirKey = key.endsWith("/") ? key : `${key}/`;
149
+ const command = new _aws_sdk_client_s3.PutObjectCommand({
223
150
  Bucket: this.bucket,
224
- Key: this.key,
225
- Range: `bytes=${start}-${end - 1}`
226
- });
227
- const response = await this.client.send(command, { abortSignal: signal });
228
- if (!response.Body) return new Uint8Array(0);
229
- const chunks = [];
230
- for await (const chunk of streamBody(response.Body)) chunks.push(chunk);
231
- if (chunks.length === 0) return new Uint8Array(0);
232
- if (chunks.length === 1) return chunks[0];
233
- const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
234
- const result = new Uint8Array(totalLength);
235
- let offset = 0;
236
- for (const chunk of chunks) {
237
- result.set(chunk, offset);
238
- offset += chunk.length;
239
- }
240
- return result;
241
- }
242
- /**
243
- * Upload a single part and record it in the parts array.
244
- */
245
- async uploadPart(uploadId, partNumber, data, parts, signal) {
246
- const command = new __aws_sdk_client_s3.UploadPartCommand({
247
- Bucket: this.bucket,
248
- Key: this.key,
249
- UploadId: uploadId,
250
- PartNumber: partNumber,
251
- Body: data,
252
- ContentLength: data.length
253
- });
254
- const { ETag } = await this.client.send(command, { abortSignal: signal });
255
- if (ETag) parts.push({
256
- ETag,
257
- PartNumber: partNumber
258
- });
259
- }
260
- /**
261
- * Abort a multipart upload, ignoring errors.
262
- */
263
- async abortMultipartUpload(uploadId) {
264
- const command = new __aws_sdk_client_s3.AbortMultipartUploadCommand({
265
- Bucket: this.bucket,
266
- Key: this.key,
267
- UploadId: uploadId
268
- });
269
- await this.client.send(command).catch(() => {});
270
- }
271
- /**
272
- * Random access read from S3 object using HTTP Range header.
273
- */
274
- async read(buffer, offset, length, position) {
275
- if (this.closed) throw new Error("FileHandle is closed");
276
- let len = Math.min(length, buffer.length - offset);
277
- len = Math.min(len, this._size - position);
278
- len = Math.max(0, len);
279
- if (len === 0) return 0;
280
- const command = new __aws_sdk_client_s3.GetObjectCommand({
281
- Bucket: this.bucket,
282
- Key: this.key,
283
- Range: `bytes=${position}-${position + len - 1}`
151
+ Key: dirKey,
152
+ Body: /* @__PURE__ */ new Uint8Array(0),
153
+ ContentLength: 0
284
154
  });
285
- const response = await this.client.send(command);
286
- if (!response.Body) return 0;
287
- const chunks = [];
288
- for await (const chunk of streamBody(response.Body)) chunks.push(chunk);
289
- if (chunks.length === 0) return 0;
290
- let data;
291
- if (chunks.length === 1) data = chunks[0];
292
- else {
293
- const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
294
- data = new Uint8Array(totalLength);
295
- let pos = 0;
296
- for (const chunk of chunks) {
297
- data.set(chunk, pos);
298
- pos += chunk.length;
299
- }
300
- }
301
- const bytesToCopy = Math.min(data.length, len);
302
- buffer.set(data.subarray(0, bytesToCopy), offset);
303
- return bytesToCopy;
304
- }
305
- };
306
-
307
- //#endregion
308
- //#region src/s3-files-api.ts
309
- var S3FilesApi = class {
310
- client;
311
- bucket;
312
- prefix;
313
- multipartPartSize;
314
- constructor(options) {
315
- this.client = options.client;
316
- this.bucket = options.bucket;
317
- this.prefix = (options.prefix ?? "").replace(/^\/+|\/+$/g, "");
318
- this.multipartPartSize = options.multipartPartSize ?? 5 * 1024 * 1024;
319
- }
320
- /**
321
- * Converts a virtual path to an S3 key.
322
- * @example resolveKey("/docs/file.txt") => "prefix/docs/file.txt"
323
- */
324
- resolveKey(file) {
325
- const normalized = (0, __statewalker_webrun_files.resolveFileRef)(file);
326
- const relativePath = normalized.startsWith("/") ? normalized.substring(1) : normalized;
327
- if (this.prefix) return relativePath ? `${this.prefix}/${relativePath}` : this.prefix;
328
- return relativePath;
329
- }
330
- /**
331
- * Converts an S3 key back to a virtual path.
332
- */
333
- keyToPath(key) {
334
- let relativePath = key;
335
- if (this.prefix && key.startsWith(this.prefix)) {
336
- relativePath = key.substring(this.prefix.length);
337
- if (relativePath.startsWith("/")) relativePath = relativePath.substring(1);
338
- }
339
- return "/" + relativePath;
155
+ await this.client.send(command);
340
156
  }
341
- /**
342
- * Lists objects in a virtual directory.
343
- *
344
- * Uses S3's delimiter feature to simulate directory listing.
345
- * CommonPrefixes represent subdirectories.
346
- */
347
- async *list(file, options = {}) {
348
- const { recursive = false } = options;
349
- const prefix = this.resolveKey(file);
350
- const normalizedPrefix = prefix ? prefix + "/" : this.prefix ? this.prefix + "/" : "";
157
+ async *list(path, options) {
158
+ const recursive = options?.recursive ?? false;
159
+ const prefix = this.resolveKey(path);
160
+ const normalizedPrefix = prefix ? `${prefix}/` : this.prefix ? `${this.prefix}/` : "";
351
161
  let continuationToken;
352
162
  do {
353
- const command = new __aws_sdk_client_s3.ListObjectsV2Command({
163
+ const command = new _aws_sdk_client_s3.ListObjectsV2Command({
354
164
  Bucket: this.bucket,
355
165
  Prefix: normalizedPrefix,
356
166
  Delimiter: recursive ? void 0 : "/",
@@ -361,24 +171,23 @@ var S3FilesApi = class {
361
171
  if (!recursive && response.CommonPrefixes) for (const cpPrefix of response.CommonPrefixes) {
362
172
  if (!cpPrefix.Prefix) continue;
363
173
  const dirKey = cpPrefix.Prefix.endsWith("/") ? cpPrefix.Prefix.slice(0, -1) : cpPrefix.Prefix;
364
- const path = this.keyToPath(dirKey);
174
+ const dirPath = this.keyToPath(dirKey);
365
175
  yield {
366
176
  kind: "directory",
367
- name: (0, __statewalker_webrun_files.basename)(path),
368
- path,
369
- lastModified: 0
177
+ name: (0, _statewalker_webrun_files.basename)(dirPath),
178
+ path: dirPath
370
179
  };
371
180
  }
372
181
  if (response.Contents) for (const obj of response.Contents) {
373
182
  if (!obj.Key) continue;
374
183
  if (obj.Key === normalizedPrefix) continue;
375
- if (obj.Key + "/" === normalizedPrefix) continue;
184
+ if (`${obj.Key}/` === normalizedPrefix) continue;
376
185
  if (obj.Key.endsWith("/")) continue;
377
- const path = this.keyToPath(obj.Key);
186
+ const filePath = this.keyToPath(obj.Key);
378
187
  yield {
379
188
  kind: "file",
380
- name: (0, __statewalker_webrun_files.basename)(path),
381
- path,
189
+ name: (0, _statewalker_webrun_files.basename)(filePath),
190
+ path: filePath,
382
191
  size: obj.Size ?? 0,
383
192
  lastModified: obj.LastModified?.getTime() ?? 0
384
193
  };
@@ -386,62 +195,46 @@ var S3FilesApi = class {
386
195
  continuationToken = response.NextContinuationToken;
387
196
  } while (continuationToken);
388
197
  }
389
- /**
390
- * Gets object metadata using HEAD request.
391
- */
392
- async stats(file) {
393
- const normalized = (0, __statewalker_webrun_files.resolveFileRef)(file);
394
- const key = this.resolveKey(file);
395
- if (normalized === "/") return {
396
- kind: "directory",
397
- name: "",
398
- path: "/",
399
- lastModified: 0
400
- };
198
+ async stats(path) {
199
+ const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
200
+ const key = this.resolveKey(path);
201
+ if (normalized === "/") return { kind: "directory" };
401
202
  try {
402
- const command = new __aws_sdk_client_s3.HeadObjectCommand({
203
+ const command = new _aws_sdk_client_s3.HeadObjectCommand({
403
204
  Bucket: this.bucket,
404
205
  Key: key
405
206
  });
406
207
  const response = await this.client.send(command);
407
208
  return {
408
209
  kind: "file",
409
- name: (0, __statewalker_webrun_files.basename)(normalized),
410
- path: normalized,
411
210
  size: response.ContentLength ?? 0,
412
- lastModified: response.LastModified?.getTime() ?? 0,
413
- type: response.ContentType
211
+ lastModified: response.LastModified?.getTime() ?? 0
414
212
  };
415
213
  } catch (error) {
416
214
  const err = error;
417
215
  if (err.name !== "NotFound" && err.$metadata?.httpStatusCode !== 404) throw error;
418
216
  }
419
- const prefix = key ? key + "/" : "";
420
- const listCommand = new __aws_sdk_client_s3.ListObjectsV2Command({
217
+ const prefix = key ? `${key}/` : "";
218
+ const listCommand = new _aws_sdk_client_s3.ListObjectsV2Command({
421
219
  Bucket: this.bucket,
422
220
  Prefix: prefix,
423
221
  MaxKeys: 1
424
222
  });
425
223
  const listResponse = await this.client.send(listCommand);
426
- if (listResponse.Contents && listResponse.Contents.length > 0 || listResponse.CommonPrefixes && listResponse.CommonPrefixes.length > 0) return {
427
- kind: "directory",
428
- name: (0, __statewalker_webrun_files.basename)(normalized),
429
- path: normalized,
430
- lastModified: 0
431
- };
224
+ if (listResponse.Contents && listResponse.Contents.length > 0 || listResponse.CommonPrefixes && listResponse.CommonPrefixes.length > 0) return { kind: "directory" };
225
+ }
226
+ async exists(path) {
227
+ return await this.stats(path) !== void 0;
432
228
  }
433
- /**
434
- * Deletes an object or all objects under a prefix (directory).
435
- */
436
- async remove(file) {
437
- const key = this.resolveKey(file);
229
+ async remove(path) {
230
+ const key = this.resolveKey(path);
438
231
  try {
439
- const headCommand = new __aws_sdk_client_s3.HeadObjectCommand({
232
+ const headCommand = new _aws_sdk_client_s3.HeadObjectCommand({
440
233
  Bucket: this.bucket,
441
234
  Key: key
442
235
  });
443
236
  await this.client.send(headCommand);
444
- const deleteCommand = new __aws_sdk_client_s3.DeleteObjectCommand({
237
+ const deleteCommand = new _aws_sdk_client_s3.DeleteObjectCommand({
445
238
  Bucket: this.bucket,
446
239
  Key: key
447
240
  });
@@ -451,11 +244,11 @@ var S3FilesApi = class {
451
244
  const err = error;
452
245
  if (err.name !== "NotFound" && err.$metadata?.httpStatusCode !== 404) throw error;
453
246
  }
454
- const prefix = key ? key + "/" : "";
247
+ const prefix = key ? `${key}/` : "";
455
248
  const keysToDelete = [];
456
249
  let continuationToken;
457
250
  do {
458
- const listCommand = new __aws_sdk_client_s3.ListObjectsV2Command({
251
+ const listCommand = new _aws_sdk_client_s3.ListObjectsV2Command({
459
252
  Bucket: this.bucket,
460
253
  Prefix: prefix,
461
254
  ContinuationToken: continuationToken
@@ -468,7 +261,7 @@ var S3FilesApi = class {
468
261
  } while (continuationToken);
469
262
  if (keysToDelete.length === 0) return false;
470
263
  for (const keyToDelete of keysToDelete) {
471
- const deleteCommand = new __aws_sdk_client_s3.DeleteObjectCommand({
264
+ const deleteCommand = new _aws_sdk_client_s3.DeleteObjectCommand({
472
265
  Bucket: this.bucket,
473
266
  Key: keyToDelete
474
267
  });
@@ -476,49 +269,17 @@ var S3FilesApi = class {
476
269
  }
477
270
  return true;
478
271
  }
479
- /**
480
- * Opens an S3 object for random access.
481
- * Creates the object if it doesn't exist.
482
- */
483
- async open(file) {
484
- const key = this.resolveKey(file);
485
- let size = 0;
486
- try {
487
- const command = new __aws_sdk_client_s3.HeadObjectCommand({
488
- Bucket: this.bucket,
489
- Key: key
490
- });
491
- size = (await this.client.send(command)).ContentLength ?? 0;
492
- } catch (error) {
493
- const err = error;
494
- if (err.name !== "NotFound" && err.$metadata?.httpStatusCode !== 404) throw error;
495
- }
496
- return new S3FileHandle({
497
- client: this.client,
498
- bucket: this.bucket,
499
- key,
500
- size,
501
- multipartPartSize: this.multipartPartSize
502
- });
503
- }
504
- /**
505
- * Moves an object using Copy + Delete.
506
- */
507
272
  async move(source, target) {
508
273
  if (!await this.copy(source, target)) return false;
509
274
  return this.remove(source);
510
275
  }
511
- /**
512
- * Copies an object or directory using S3 CopyObject.
513
- */
514
- async copy(source, target, options = {}) {
276
+ async copy(source, target) {
515
277
  const sourceKey = this.resolveKey(source);
516
278
  const targetKey = this.resolveKey(target);
517
- const { recursive = true } = options;
518
- const sourceInfo = await this.stats(source);
519
- if (!sourceInfo) return false;
520
- if (sourceInfo.kind === "file") {
521
- const command = new __aws_sdk_client_s3.CopyObjectCommand({
279
+ const sourceStats = await this.stats(source);
280
+ if (!sourceStats) return false;
281
+ if (sourceStats.kind === "file") {
282
+ const command = new _aws_sdk_client_s3.CopyObjectCommand({
522
283
  Bucket: this.bucket,
523
284
  Key: targetKey,
524
285
  CopySource: encodeURIComponent(`${this.bucket}/${sourceKey}`)
@@ -526,13 +287,12 @@ var S3FilesApi = class {
526
287
  await this.client.send(command);
527
288
  return true;
528
289
  }
529
- if (!recursive) return false;
530
- const sourcePrefix = sourceKey + "/";
531
- const targetPrefix = targetKey + "/";
290
+ const sourcePrefix = `${sourceKey}/`;
291
+ const targetPrefix = `${targetKey}/`;
532
292
  let continuationToken;
533
293
  let copied = false;
534
294
  do {
535
- const listCommand = new __aws_sdk_client_s3.ListObjectsV2Command({
295
+ const listCommand = new _aws_sdk_client_s3.ListObjectsV2Command({
536
296
  Bucket: this.bucket,
537
297
  Prefix: sourcePrefix,
538
298
  ContinuationToken: continuationToken
@@ -541,7 +301,7 @@ var S3FilesApi = class {
541
301
  if (response.Contents) for (const obj of response.Contents) {
542
302
  if (!obj.Key) continue;
543
303
  const newKey = targetPrefix + obj.Key.substring(sourcePrefix.length);
544
- const copyCommand = new __aws_sdk_client_s3.CopyObjectCommand({
304
+ const copyCommand = new _aws_sdk_client_s3.CopyObjectCommand({
545
305
  Bucket: this.bucket,
546
306
  Key: newKey,
547
307
  CopySource: encodeURIComponent(`${this.bucket}/${obj.Key}`)
@@ -553,27 +313,6 @@ var S3FilesApi = class {
553
313
  } while (continuationToken);
554
314
  return copied;
555
315
  }
556
- /**
557
- * Creates a directory marker in S3.
558
- *
559
- * S3 doesn't have real directories, but some tools expect
560
- * a zero-byte object with trailing slash as a directory marker.
561
- * However, for our purposes we don't actually need to create markers
562
- * since directories are implied by files within them.
563
- */
564
- async mkdir(file) {
565
- const key = this.resolveKey(file);
566
- const dirKey = key.endsWith("/") ? key : key + "/";
567
- const command = new __aws_sdk_client_s3.PutObjectCommand({
568
- Bucket: this.bucket,
569
- Key: dirKey,
570
- Body: new Uint8Array(0),
571
- ContentLength: 0
572
- });
573
- await this.client.send(command);
574
- }
575
316
  };
576
-
577
317
  //#endregion
578
- exports.S3FileHandle = S3FileHandle;
579
- exports.S3FilesApi = S3FilesApi;
318
+ exports.S3FilesApi = S3FilesApi;