@statewalker/webrun-files-s3 0.5.0 → 0.7.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.
package/dist/esm/index.js CHANGED
@@ -1,353 +1,163 @@
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";
1
+ import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CopyObjectCommand, CreateMultipartUploadCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, UploadPartCommand } from "@aws-sdk/client-s3";
2
+ import { basename, normalizePath } from "@statewalker/webrun-files";
3
3
 
4
- //#region src/s3-file-handle.ts
4
+ //#region src/s3-files-api.ts
5
+ /** Default part size for multipart uploads: 5MB (S3 minimum) */
6
+ const DEFAULT_PART_SIZE = 5 * 1024 * 1024;
7
+ /** Threshold for using multipart upload: 5MB */
8
+ const MULTIPART_THRESHOLD = 5 * 1024 * 1024;
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 = 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 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 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 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 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 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 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 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 CompleteMultipartUploadCommand({
101
+ const uploadCommand = new 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 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 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 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({
146
+ async mkdir(path) {
147
+ const key = this.resolveKey(path);
148
+ const dirKey = key.endsWith("/") ? key : `${key}/`;
149
+ const command = new PutObjectCommand({
281
150
  Bucket: this.bucket,
282
- Key: this.key,
283
- Range: `bytes=${position}-${position + len - 1}`
151
+ Key: dirKey,
152
+ Body: 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 = 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
163
  const command = new ListObjectsV2Command({
@@ -361,24 +171,24 @@ 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: basename(path),
368
- path,
177
+ name: basename(dirPath),
178
+ path: dirPath,
369
179
  lastModified: 0
370
180
  };
371
181
  }
372
182
  if (response.Contents) for (const obj of response.Contents) {
373
183
  if (!obj.Key) continue;
374
184
  if (obj.Key === normalizedPrefix) continue;
375
- if (obj.Key + "/" === normalizedPrefix) continue;
185
+ if (`${obj.Key}/` === normalizedPrefix) continue;
376
186
  if (obj.Key.endsWith("/")) continue;
377
- const path = this.keyToPath(obj.Key);
187
+ const filePath = this.keyToPath(obj.Key);
378
188
  yield {
379
189
  kind: "file",
380
- name: basename(path),
381
- path,
190
+ name: basename(filePath),
191
+ path: filePath,
382
192
  size: obj.Size ?? 0,
383
193
  lastModified: obj.LastModified?.getTime() ?? 0
384
194
  };
@@ -386,16 +196,11 @@ var S3FilesApi = class {
386
196
  continuationToken = response.NextContinuationToken;
387
197
  } while (continuationToken);
388
198
  }
389
- /**
390
- * Gets object metadata using HEAD request.
391
- */
392
- async stats(file) {
393
- const normalized = resolveFileRef(file);
394
- const key = this.resolveKey(file);
199
+ async stats(path) {
200
+ const normalized = normalizePath(path);
201
+ const key = this.resolveKey(path);
395
202
  if (normalized === "/") return {
396
203
  kind: "directory",
397
- name: "",
398
- path: "/",
399
204
  lastModified: 0
400
205
  };
401
206
  try {
@@ -406,17 +211,14 @@ var S3FilesApi = class {
406
211
  const response = await this.client.send(command);
407
212
  return {
408
213
  kind: "file",
409
- name: basename(normalized),
410
- path: normalized,
411
214
  size: response.ContentLength ?? 0,
412
- lastModified: response.LastModified?.getTime() ?? 0,
413
- type: response.ContentType
215
+ lastModified: response.LastModified?.getTime() ?? 0
414
216
  };
415
217
  } catch (error) {
416
218
  const err = error;
417
219
  if (err.name !== "NotFound" && err.$metadata?.httpStatusCode !== 404) throw error;
418
220
  }
419
- const prefix = key ? key + "/" : "";
221
+ const prefix = key ? `${key}/` : "";
420
222
  const listCommand = new ListObjectsV2Command({
421
223
  Bucket: this.bucket,
422
224
  Prefix: prefix,
@@ -425,16 +227,14 @@ var S3FilesApi = class {
425
227
  const listResponse = await this.client.send(listCommand);
426
228
  if (listResponse.Contents && listResponse.Contents.length > 0 || listResponse.CommonPrefixes && listResponse.CommonPrefixes.length > 0) return {
427
229
  kind: "directory",
428
- name: basename(normalized),
429
- path: normalized,
430
230
  lastModified: 0
431
231
  };
432
232
  }
433
- /**
434
- * Deletes an object or all objects under a prefix (directory).
435
- */
436
- async remove(file) {
437
- const key = this.resolveKey(file);
233
+ async exists(path) {
234
+ return await this.stats(path) !== void 0;
235
+ }
236
+ async remove(path) {
237
+ const key = this.resolveKey(path);
438
238
  try {
439
239
  const headCommand = new HeadObjectCommand({
440
240
  Bucket: this.bucket,
@@ -451,7 +251,7 @@ var S3FilesApi = class {
451
251
  const err = error;
452
252
  if (err.name !== "NotFound" && err.$metadata?.httpStatusCode !== 404) throw error;
453
253
  }
454
- const prefix = key ? key + "/" : "";
254
+ const prefix = key ? `${key}/` : "";
455
255
  const keysToDelete = [];
456
256
  let continuationToken;
457
257
  do {
@@ -476,48 +276,16 @@ var S3FilesApi = class {
476
276
  }
477
277
  return true;
478
278
  }
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
279
  async move(source, target) {
508
280
  if (!await this.copy(source, target)) return false;
509
281
  return this.remove(source);
510
282
  }
511
- /**
512
- * Copies an object or directory using S3 CopyObject.
513
- */
514
- async copy(source, target, options = {}) {
283
+ async copy(source, target) {
515
284
  const sourceKey = this.resolveKey(source);
516
285
  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") {
286
+ const sourceStats = await this.stats(source);
287
+ if (!sourceStats) return false;
288
+ if (sourceStats.kind === "file") {
521
289
  const command = new CopyObjectCommand({
522
290
  Bucket: this.bucket,
523
291
  Key: targetKey,
@@ -526,9 +294,8 @@ var S3FilesApi = class {
526
294
  await this.client.send(command);
527
295
  return true;
528
296
  }
529
- if (!recursive) return false;
530
- const sourcePrefix = sourceKey + "/";
531
- const targetPrefix = targetKey + "/";
297
+ const sourcePrefix = `${sourceKey}/`;
298
+ const targetPrefix = `${targetKey}/`;
532
299
  let continuationToken;
533
300
  let copied = false;
534
301
  do {
@@ -553,26 +320,7 @@ var S3FilesApi = class {
553
320
  } while (continuationToken);
554
321
  return copied;
555
322
  }
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
323
  };
576
324
 
577
325
  //#endregion
578
- export { S3FileHandle, S3FilesApi };
326
+ export { S3FilesApi };