@statewalker/webrun-files-s3 0.5.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.
@@ -0,0 +1,578 @@
1
+ import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CopyObjectCommand, CreateMultipartUploadCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, UploadPartCommand, UploadPartCopyCommand } from "@aws-sdk/client-s3";
2
+ import { basename, resolveFileRef, toBinaryAsyncIterable } from "@statewalker/webrun-files";
3
+
4
+ //#region src/s3-file-handle.ts
5
+ /**
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.
9
+ */
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 {
27
+ client;
28
+ bucket;
29
+ key;
30
+ _size;
31
+ multipartPartSize;
32
+ closed = false;
33
+ constructor(options) {
34
+ this.client = options.client;
35
+ 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;
42
+ }
43
+ async close() {
44
+ this.closed = true;
45
+ }
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 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;
66
+ }
67
+ }
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);
93
+ }
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 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");
107
+ const parts = [];
108
+ 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 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
+ }
178
+ }
179
+ for await (const chunk of 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
+ }
197
+ }
198
+ if (bufferOffset > 0) {
199
+ const lastPart = buffer.subarray(0, bufferOffset);
200
+ await this.uploadPart(UploadId, partNumber, lastPart, parts, signal);
201
+ }
202
+ if (parts.length === 0) await this.uploadPart(UploadId, 1, new Uint8Array(0), parts, signal);
203
+ const completeCommand = new CompleteMultipartUploadCommand({
204
+ Bucket: this.bucket,
205
+ Key: this.key,
206
+ UploadId,
207
+ MultipartUpload: { Parts: parts }
208
+ });
209
+ await this.client.send(completeCommand, { abortSignal: signal });
210
+ this._size = totalSize;
211
+ return bytesWritten;
212
+ } catch (error) {
213
+ await this.abortMultipartUpload(UploadId);
214
+ throw error;
215
+ }
216
+ }
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 GetObjectCommand({
223
+ 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 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 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 GetObjectCommand({
281
+ Bucket: this.bucket,
282
+ Key: this.key,
283
+ Range: `bytes=${position}-${position + len - 1}`
284
+ });
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 = 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;
340
+ }
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 + "/" : "";
351
+ let continuationToken;
352
+ do {
353
+ const command = new ListObjectsV2Command({
354
+ Bucket: this.bucket,
355
+ Prefix: normalizedPrefix,
356
+ Delimiter: recursive ? void 0 : "/",
357
+ ContinuationToken: continuationToken,
358
+ MaxKeys: 1e3
359
+ });
360
+ const response = await this.client.send(command);
361
+ if (!recursive && response.CommonPrefixes) for (const cpPrefix of response.CommonPrefixes) {
362
+ if (!cpPrefix.Prefix) continue;
363
+ const dirKey = cpPrefix.Prefix.endsWith("/") ? cpPrefix.Prefix.slice(0, -1) : cpPrefix.Prefix;
364
+ const path = this.keyToPath(dirKey);
365
+ yield {
366
+ kind: "directory",
367
+ name: basename(path),
368
+ path,
369
+ lastModified: 0
370
+ };
371
+ }
372
+ if (response.Contents) for (const obj of response.Contents) {
373
+ if (!obj.Key) continue;
374
+ if (obj.Key === normalizedPrefix) continue;
375
+ if (obj.Key + "/" === normalizedPrefix) continue;
376
+ if (obj.Key.endsWith("/")) continue;
377
+ const path = this.keyToPath(obj.Key);
378
+ yield {
379
+ kind: "file",
380
+ name: basename(path),
381
+ path,
382
+ size: obj.Size ?? 0,
383
+ lastModified: obj.LastModified?.getTime() ?? 0
384
+ };
385
+ }
386
+ continuationToken = response.NextContinuationToken;
387
+ } while (continuationToken);
388
+ }
389
+ /**
390
+ * Gets object metadata using HEAD request.
391
+ */
392
+ async stats(file) {
393
+ const normalized = resolveFileRef(file);
394
+ const key = this.resolveKey(file);
395
+ if (normalized === "/") return {
396
+ kind: "directory",
397
+ name: "",
398
+ path: "/",
399
+ lastModified: 0
400
+ };
401
+ try {
402
+ const command = new HeadObjectCommand({
403
+ Bucket: this.bucket,
404
+ Key: key
405
+ });
406
+ const response = await this.client.send(command);
407
+ return {
408
+ kind: "file",
409
+ name: basename(normalized),
410
+ path: normalized,
411
+ size: response.ContentLength ?? 0,
412
+ lastModified: response.LastModified?.getTime() ?? 0,
413
+ type: response.ContentType
414
+ };
415
+ } catch (error) {
416
+ const err = error;
417
+ if (err.name !== "NotFound" && err.$metadata?.httpStatusCode !== 404) throw error;
418
+ }
419
+ const prefix = key ? key + "/" : "";
420
+ const listCommand = new ListObjectsV2Command({
421
+ Bucket: this.bucket,
422
+ Prefix: prefix,
423
+ MaxKeys: 1
424
+ });
425
+ 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: basename(normalized),
429
+ path: normalized,
430
+ lastModified: 0
431
+ };
432
+ }
433
+ /**
434
+ * Deletes an object or all objects under a prefix (directory).
435
+ */
436
+ async remove(file) {
437
+ const key = this.resolveKey(file);
438
+ try {
439
+ const headCommand = new HeadObjectCommand({
440
+ Bucket: this.bucket,
441
+ Key: key
442
+ });
443
+ await this.client.send(headCommand);
444
+ const deleteCommand = new DeleteObjectCommand({
445
+ Bucket: this.bucket,
446
+ Key: key
447
+ });
448
+ await this.client.send(deleteCommand);
449
+ return true;
450
+ } catch (error) {
451
+ const err = error;
452
+ if (err.name !== "NotFound" && err.$metadata?.httpStatusCode !== 404) throw error;
453
+ }
454
+ const prefix = key ? key + "/" : "";
455
+ const keysToDelete = [];
456
+ let continuationToken;
457
+ do {
458
+ const listCommand = new ListObjectsV2Command({
459
+ Bucket: this.bucket,
460
+ Prefix: prefix,
461
+ ContinuationToken: continuationToken
462
+ });
463
+ const response = await this.client.send(listCommand);
464
+ if (response.Contents) {
465
+ for (const obj of response.Contents) if (obj.Key) keysToDelete.push(obj.Key);
466
+ }
467
+ continuationToken = response.NextContinuationToken;
468
+ } while (continuationToken);
469
+ if (keysToDelete.length === 0) return false;
470
+ for (const keyToDelete of keysToDelete) {
471
+ const deleteCommand = new DeleteObjectCommand({
472
+ Bucket: this.bucket,
473
+ Key: keyToDelete
474
+ });
475
+ await this.client.send(deleteCommand);
476
+ }
477
+ return true;
478
+ }
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 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
+ async move(source, target) {
508
+ if (!await this.copy(source, target)) return false;
509
+ return this.remove(source);
510
+ }
511
+ /**
512
+ * Copies an object or directory using S3 CopyObject.
513
+ */
514
+ async copy(source, target, options = {}) {
515
+ const sourceKey = this.resolveKey(source);
516
+ 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 CopyObjectCommand({
522
+ Bucket: this.bucket,
523
+ Key: targetKey,
524
+ CopySource: encodeURIComponent(`${this.bucket}/${sourceKey}`)
525
+ });
526
+ await this.client.send(command);
527
+ return true;
528
+ }
529
+ if (!recursive) return false;
530
+ const sourcePrefix = sourceKey + "/";
531
+ const targetPrefix = targetKey + "/";
532
+ let continuationToken;
533
+ let copied = false;
534
+ do {
535
+ const listCommand = new ListObjectsV2Command({
536
+ Bucket: this.bucket,
537
+ Prefix: sourcePrefix,
538
+ ContinuationToken: continuationToken
539
+ });
540
+ const response = await this.client.send(listCommand);
541
+ if (response.Contents) for (const obj of response.Contents) {
542
+ if (!obj.Key) continue;
543
+ const newKey = targetPrefix + obj.Key.substring(sourcePrefix.length);
544
+ const copyCommand = new CopyObjectCommand({
545
+ Bucket: this.bucket,
546
+ Key: newKey,
547
+ CopySource: encodeURIComponent(`${this.bucket}/${obj.Key}`)
548
+ });
549
+ await this.client.send(copyCommand);
550
+ copied = true;
551
+ }
552
+ continuationToken = response.NextContinuationToken;
553
+ } while (continuationToken);
554
+ return copied;
555
+ }
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 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
+ };
576
+
577
+ //#endregion
578
+ export { S3FileHandle, S3FilesApi };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * webrun-files-s3 - S3-backed IFilesApi implementation
3
+ */
4
+ export { S3FilesApi, type S3FilesApiOptions } from "./s3-files-api.js";
5
+ export { S3FileHandle, type S3FileHandleOptions } from "./s3-file-handle.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * S3 FileHandle implementation
3
+ *
4
+ * Provides random access read/write operations for S3 objects.
5
+ * Uses HTTP Range headers for efficient partial reads.
6
+ * Since S3 objects are immutable, writes require re-uploading the entire object.
7
+ */
8
+ import type { S3Client } from "@aws-sdk/client-s3";
9
+ import type { AppendOptions, BinaryStream, FileHandle, ReadStreamOptions, WriteStreamOptions } from "@statewalker/webrun-files";
10
+ export interface S3FileHandleOptions {
11
+ client: S3Client;
12
+ bucket: string;
13
+ key: string;
14
+ size: number;
15
+ multipartPartSize: number;
16
+ }
17
+ export declare class S3FileHandle implements FileHandle {
18
+ private client;
19
+ private bucket;
20
+ private key;
21
+ private _size;
22
+ private multipartPartSize;
23
+ private closed;
24
+ constructor(options: S3FileHandleOptions);
25
+ get size(): number;
26
+ close(): Promise<void>;
27
+ /**
28
+ * Reads a range of bytes from the S3 object.
29
+ * Uses HTTP Range header for efficient partial reads.
30
+ * Streams data directly from S3 without buffering the entire response.
31
+ */
32
+ createReadStream(options?: ReadStreamOptions): AsyncGenerator<Uint8Array>;
33
+ /**
34
+ * Writes content to the S3 object using streaming.
35
+ *
36
+ * S3 objects are immutable, so this operation replaces the entire object.
37
+ * Uses streaming multipart upload to avoid buffering the entire file in memory.
38
+ * Only buffers one part at a time (5MB by default).
39
+ *
40
+ * Note: start position > 0 uses UploadPartCopy to preserve existing content
41
+ * without downloading it.
42
+ */
43
+ createWriteStream(data: BinaryStream, options?: WriteStreamOptions): Promise<number>;
44
+ /**
45
+ * Appends content to the end of the S3 object using streaming.
46
+ *
47
+ * Uses UploadPartCopy to preserve existing content without downloading it,
48
+ * then streams new data as additional parts.
49
+ */
50
+ appendFile(data: BinaryStream, options?: AppendOptions): Promise<number>;
51
+ /**
52
+ * Streaming multipart upload that buffers only one part at a time.
53
+ * Uses UploadPartCopy to preserve existing content without downloading it
54
+ * for large files. For small existing content (< part size), downloads and
55
+ * combines with new data to meet S3's minimum part size requirement.
56
+ */
57
+ private streamingMultipartUpload;
58
+ /**
59
+ * Download a range of bytes from the S3 object.
60
+ */
61
+ private downloadRange;
62
+ /**
63
+ * Upload a single part and record it in the parts array.
64
+ */
65
+ private uploadPart;
66
+ /**
67
+ * Abort a multipart upload, ignoring errors.
68
+ */
69
+ private abortMultipartUpload;
70
+ /**
71
+ * Random access read from S3 object using HTTP Range header.
72
+ */
73
+ read(buffer: Uint8Array, offset: number, length: number, position: number): Promise<number>;
74
+ }
75
+ //# sourceMappingURL=s3-file-handle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3-file-handle.d.ts","sourceRoot":"","sources":["../src/s3-file-handle.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AASnD,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,2BAA2B,CAAC;AAGnC,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AA+BD,qBAAa,YAAa,YAAW,UAAU;IAC7C,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,EAAE,mBAAmB;IAQxC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;;;OAIG;IACI,gBAAgB,CACrB,OAAO,GAAE,iBAAsB,GAC9B,cAAc,CAAC,UAAU,CAAC;IA6B7B;;;;;;;;;OASG;IACG,iBAAiB,CACrB,IAAI,EAAE,YAAY,EAClB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,CAAC;IAWlB;;;;;OAKG;IACG,UAAU,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBlF;;;;;OAKG;YACW,wBAAwB;IAgLtC;;OAEG;YACW,aAAa;IAwC3B;;OAEG;YACW,UAAU;IAuBxB;;OAEG;YACW,oBAAoB;IAYlC;;OAEG;IACG,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAoDlG"}