@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.
package/dist/esm/index.js CHANGED
@@ -1,353 +1,162 @@
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
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
+ //#region src/s3-files-api.ts
4
+ /** Default part size for multipart uploads: 5MB (S3 minimum) */
5
+ const DEFAULT_PART_SIZE = 5242880;
6
+ /** Threshold for using multipart upload: 5MB */
7
+ const MULTIPART_THRESHOLD = 5242880;
5
8
  /**
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
+ * S3 filesystem implementation of FilesApi.
10
+ *
11
+ * Provides a filesystem-like interface over S3 object storage.
12
+ * Works with AWS S3 and S3-compatible services.
9
13
  */
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 {
14
+ var S3FilesApi = class {
27
15
  client;
28
16
  bucket;
29
- key;
30
- _size;
31
- multipartPartSize;
32
- closed = false;
17
+ prefix;
18
+ partSize;
33
19
  constructor(options) {
34
20
  this.client = options.client;
35
21
  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;
22
+ this.prefix = (options.prefix ?? "").replace(/^\/+|\/+$/g, "");
23
+ this.partSize = options.multipartPartSize ?? DEFAULT_PART_SIZE;
42
24
  }
43
- async close() {
44
- this.closed = true;
25
+ resolveKey(path) {
26
+ const normalized = normalizePath(path);
27
+ const relativePath = normalized.startsWith("/") ? normalized.substring(1) : normalized;
28
+ if (this.prefix) return relativePath ? `${this.prefix}/${relativePath}` : this.prefix;
29
+ return relativePath;
45
30
  }
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;
31
+ keyToPath(key) {
32
+ let relativePath = key;
33
+ if (this.prefix && key.startsWith(this.prefix)) {
34
+ relativePath = key.substring(this.prefix.length);
35
+ if (relativePath.startsWith("/")) relativePath = relativePath.substring(1);
66
36
  }
37
+ return `/${relativePath}`;
67
38
  }
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);
39
+ async *read(path, options) {
40
+ const key = this.resolveKey(path);
41
+ try {
42
+ const start = options?.start ?? 0;
43
+ const length = options?.length;
44
+ if (length === 0) return;
45
+ let range;
46
+ if (start > 0 || length !== void 0) range = `bytes=${start}-${length !== void 0 ? start + length - 1 : ""}`;
47
+ const command = new GetObjectCommand({
48
+ Bucket: this.bucket,
49
+ Key: key,
50
+ Range: range
51
+ });
52
+ const response = await this.client.send(command);
53
+ if (response.Body) {
54
+ const stream = response.Body;
55
+ for await (const chunk of stream) yield chunk;
56
+ }
57
+ } catch (error) {
58
+ const err = error;
59
+ if (err.name === "NotFound" || err.name === "InvalidRange" || err.$metadata?.httpStatusCode === 404 || err.$metadata?.httpStatusCode === 416) return;
60
+ throw error;
61
+ }
93
62
  }
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");
63
+ async write(path, content) {
64
+ const key = this.resolveKey(path);
65
+ let buffer = [];
66
+ let bufferSize = 0;
67
+ let uploadId;
107
68
  const parts = [];
108
69
  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
- }
70
+ const flushPart = async (isLast) => {
71
+ if (buffer.length === 0) return;
72
+ const partLength = buffer.reduce((sum, c) => sum + c.length, 0);
73
+ const partBuffer = new Uint8Array(partLength);
74
+ let offset = 0;
75
+ for (const chunk of buffer) {
76
+ partBuffer.set(chunk, offset);
77
+ offset += chunk.length;
178
78
  }
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
- }
79
+ buffer = [];
80
+ bufferSize = 0;
81
+ if (!uploadId && isLast && partLength < MULTIPART_THRESHOLD) {
82
+ const command = new PutObjectCommand({
83
+ Bucket: this.bucket,
84
+ Key: key,
85
+ Body: partBuffer,
86
+ ContentLength: partLength
87
+ });
88
+ await this.client.send(command);
89
+ return;
197
90
  }
198
- if (bufferOffset > 0) {
199
- const lastPart = buffer.subarray(0, bufferOffset);
200
- await this.uploadPart(UploadId, partNumber, lastPart, parts, signal);
91
+ if (!uploadId) {
92
+ const createCommand = new CreateMultipartUploadCommand({
93
+ Bucket: this.bucket,
94
+ Key: key
95
+ });
96
+ const { UploadId } = await this.client.send(createCommand);
97
+ if (!UploadId) throw new Error("Failed to create multipart upload");
98
+ uploadId = UploadId;
201
99
  }
202
- if (parts.length === 0) await this.uploadPart(UploadId, 1, new Uint8Array(0), parts, signal);
203
- const completeCommand = new CompleteMultipartUploadCommand({
100
+ const uploadCommand = new UploadPartCommand({
204
101
  Bucket: this.bucket,
205
- Key: this.key,
206
- UploadId,
207
- MultipartUpload: { Parts: parts }
102
+ Key: key,
103
+ UploadId: uploadId,
104
+ PartNumber: partNumber,
105
+ Body: partBuffer
106
+ });
107
+ const response = await this.client.send(uploadCommand);
108
+ if (!response.ETag) throw new Error(`Failed to upload part ${partNumber}`);
109
+ parts.push({
110
+ ETag: response.ETag,
111
+ PartNumber: partNumber
208
112
  });
209
- await this.client.send(completeCommand, { abortSignal: signal });
210
- this._size = totalSize;
211
- return bytesWritten;
113
+ partNumber++;
114
+ };
115
+ try {
116
+ for await (const chunk of content) {
117
+ buffer.push(chunk);
118
+ bufferSize += chunk.length;
119
+ if (bufferSize >= this.partSize) await flushPart(false);
120
+ }
121
+ await flushPart(true);
122
+ if (uploadId) {
123
+ const completeCommand = new CompleteMultipartUploadCommand({
124
+ Bucket: this.bucket,
125
+ Key: key,
126
+ UploadId: uploadId,
127
+ MultipartUpload: { Parts: parts.sort((a, b) => a.PartNumber - b.PartNumber) }
128
+ });
129
+ await this.client.send(completeCommand);
130
+ }
212
131
  } catch (error) {
213
- await this.abortMultipartUpload(UploadId);
132
+ if (uploadId) {
133
+ const abortCommand = new AbortMultipartUploadCommand({
134
+ Bucket: this.bucket,
135
+ Key: key,
136
+ UploadId: uploadId
137
+ });
138
+ try {
139
+ await this.client.send(abortCommand);
140
+ } catch {}
141
+ }
214
142
  throw error;
215
143
  }
216
144
  }
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({
145
+ async mkdir(path) {
146
+ const key = this.resolveKey(path);
147
+ const dirKey = key.endsWith("/") ? key : `${key}/`;
148
+ const command = new PutObjectCommand({
281
149
  Bucket: this.bucket,
282
- Key: this.key,
283
- Range: `bytes=${position}-${position + len - 1}`
150
+ Key: dirKey,
151
+ Body: /* @__PURE__ */ new Uint8Array(0),
152
+ ContentLength: 0
284
153
  });
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;
154
+ await this.client.send(command);
340
155
  }
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 + "/" : "";
156
+ async *list(path, options) {
157
+ const recursive = options?.recursive ?? false;
158
+ const prefix = this.resolveKey(path);
159
+ const normalizedPrefix = prefix ? `${prefix}/` : this.prefix ? `${this.prefix}/` : "";
351
160
  let continuationToken;
352
161
  do {
353
162
  const command = new ListObjectsV2Command({
@@ -361,24 +170,23 @@ var S3FilesApi = class {
361
170
  if (!recursive && response.CommonPrefixes) for (const cpPrefix of response.CommonPrefixes) {
362
171
  if (!cpPrefix.Prefix) continue;
363
172
  const dirKey = cpPrefix.Prefix.endsWith("/") ? cpPrefix.Prefix.slice(0, -1) : cpPrefix.Prefix;
364
- const path = this.keyToPath(dirKey);
173
+ const dirPath = this.keyToPath(dirKey);
365
174
  yield {
366
175
  kind: "directory",
367
- name: basename(path),
368
- path,
369
- lastModified: 0
176
+ name: basename(dirPath),
177
+ path: dirPath
370
178
  };
371
179
  }
372
180
  if (response.Contents) for (const obj of response.Contents) {
373
181
  if (!obj.Key) continue;
374
182
  if (obj.Key === normalizedPrefix) continue;
375
- if (obj.Key + "/" === normalizedPrefix) continue;
183
+ if (`${obj.Key}/` === normalizedPrefix) continue;
376
184
  if (obj.Key.endsWith("/")) continue;
377
- const path = this.keyToPath(obj.Key);
185
+ const filePath = this.keyToPath(obj.Key);
378
186
  yield {
379
187
  kind: "file",
380
- name: basename(path),
381
- path,
188
+ name: basename(filePath),
189
+ path: filePath,
382
190
  size: obj.Size ?? 0,
383
191
  lastModified: obj.LastModified?.getTime() ?? 0
384
192
  };
@@ -386,18 +194,10 @@ var S3FilesApi = class {
386
194
  continuationToken = response.NextContinuationToken;
387
195
  } while (continuationToken);
388
196
  }
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
- };
197
+ async stats(path) {
198
+ const normalized = normalizePath(path);
199
+ const key = this.resolveKey(path);
200
+ if (normalized === "/") return { kind: "directory" };
401
201
  try {
402
202
  const command = new HeadObjectCommand({
403
203
  Bucket: this.bucket,
@@ -406,35 +206,27 @@ var S3FilesApi = class {
406
206
  const response = await this.client.send(command);
407
207
  return {
408
208
  kind: "file",
409
- name: basename(normalized),
410
- path: normalized,
411
209
  size: response.ContentLength ?? 0,
412
- lastModified: response.LastModified?.getTime() ?? 0,
413
- type: response.ContentType
210
+ lastModified: response.LastModified?.getTime() ?? 0
414
211
  };
415
212
  } catch (error) {
416
213
  const err = error;
417
214
  if (err.name !== "NotFound" && err.$metadata?.httpStatusCode !== 404) throw error;
418
215
  }
419
- const prefix = key ? key + "/" : "";
216
+ const prefix = key ? `${key}/` : "";
420
217
  const listCommand = new ListObjectsV2Command({
421
218
  Bucket: this.bucket,
422
219
  Prefix: prefix,
423
220
  MaxKeys: 1
424
221
  });
425
222
  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
- };
223
+ if (listResponse.Contents && listResponse.Contents.length > 0 || listResponse.CommonPrefixes && listResponse.CommonPrefixes.length > 0) return { kind: "directory" };
432
224
  }
433
- /**
434
- * Deletes an object or all objects under a prefix (directory).
435
- */
436
- async remove(file) {
437
- const key = this.resolveKey(file);
225
+ async exists(path) {
226
+ return await this.stats(path) !== void 0;
227
+ }
228
+ async remove(path) {
229
+ const key = this.resolveKey(path);
438
230
  try {
439
231
  const headCommand = new HeadObjectCommand({
440
232
  Bucket: this.bucket,
@@ -451,7 +243,7 @@ var S3FilesApi = class {
451
243
  const err = error;
452
244
  if (err.name !== "NotFound" && err.$metadata?.httpStatusCode !== 404) throw error;
453
245
  }
454
- const prefix = key ? key + "/" : "";
246
+ const prefix = key ? `${key}/` : "";
455
247
  const keysToDelete = [];
456
248
  let continuationToken;
457
249
  do {
@@ -476,48 +268,16 @@ var S3FilesApi = class {
476
268
  }
477
269
  return true;
478
270
  }
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
271
  async move(source, target) {
508
272
  if (!await this.copy(source, target)) return false;
509
273
  return this.remove(source);
510
274
  }
511
- /**
512
- * Copies an object or directory using S3 CopyObject.
513
- */
514
- async copy(source, target, options = {}) {
275
+ async copy(source, target) {
515
276
  const sourceKey = this.resolveKey(source);
516
277
  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") {
278
+ const sourceStats = await this.stats(source);
279
+ if (!sourceStats) return false;
280
+ if (sourceStats.kind === "file") {
521
281
  const command = new CopyObjectCommand({
522
282
  Bucket: this.bucket,
523
283
  Key: targetKey,
@@ -526,9 +286,8 @@ var S3FilesApi = class {
526
286
  await this.client.send(command);
527
287
  return true;
528
288
  }
529
- if (!recursive) return false;
530
- const sourcePrefix = sourceKey + "/";
531
- const targetPrefix = targetKey + "/";
289
+ const sourcePrefix = `${sourceKey}/`;
290
+ const targetPrefix = `${targetKey}/`;
532
291
  let continuationToken;
533
292
  let copied = false;
534
293
  do {
@@ -553,26 +312,6 @@ var S3FilesApi = class {
553
312
  } while (continuationToken);
554
313
  return copied;
555
314
  }
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
315
  };
576
-
577
316
  //#endregion
578
- export { S3FileHandle, S3FilesApi };
317
+ export { S3FilesApi };