@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/LICENSE +1 -1
- package/README.md +81 -148
- package/dist/cjs/index.cjs +171 -432
- package/dist/esm/index.js +160 -421
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/s3-files-api.d.ts +22 -53
- package/dist/s3-files-api.d.ts.map +1 -1
- package/package.json +15 -15
- package/src/index.ts +1 -2
- package/src/s3-files-api.ts +253 -176
- package/dist/s3-file-handle.d.ts +0 -75
- package/dist/s3-file-handle.d.ts.map +0 -1
- package/src/s3-file-handle.ts +0 -491
package/dist/esm/index.js
CHANGED
|
@@ -1,353 +1,162 @@
|
|
|
1
|
-
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CopyObjectCommand, CreateMultipartUploadCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, UploadPartCommand
|
|
2
|
-
import { basename,
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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.
|
|
37
|
-
this.
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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 (
|
|
199
|
-
const
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
const completeCommand = new CompleteMultipartUploadCommand({
|
|
100
|
+
const uploadCommand = new UploadPartCommand({
|
|
204
101
|
Bucket: this.bucket,
|
|
205
|
-
Key:
|
|
206
|
-
UploadId,
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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:
|
|
283
|
-
|
|
150
|
+
Key: dirKey,
|
|
151
|
+
Body: /* @__PURE__ */ new Uint8Array(0),
|
|
152
|
+
ContentLength: 0
|
|
284
153
|
});
|
|
285
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
173
|
+
const dirPath = this.keyToPath(dirKey);
|
|
365
174
|
yield {
|
|
366
175
|
kind: "directory",
|
|
367
|
-
name: basename(
|
|
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
|
|
183
|
+
if (`${obj.Key}/` === normalizedPrefix) continue;
|
|
376
184
|
if (obj.Key.endsWith("/")) continue;
|
|
377
|
-
const
|
|
185
|
+
const filePath = this.keyToPath(obj.Key);
|
|
378
186
|
yield {
|
|
379
187
|
kind: "file",
|
|
380
|
-
name: basename(
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
async remove(
|
|
437
|
-
const key = this.resolveKey(
|
|
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
|
|
518
|
-
|
|
519
|
-
if (
|
|
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
|
-
|
|
530
|
-
const
|
|
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 {
|
|
317
|
+
export { S3FilesApi };
|