@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/LICENSE +1 -1
- package/README.md +70 -148
- package/dist/cjs/index.cjs +155 -408
- package/dist/esm/index.js +158 -410
- 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 +3 -3
- package/src/index.ts +1 -2
- package/src/s3-files-api.ts +243 -167
- 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/cjs/index.cjs
CHANGED
|
@@ -1,353 +1,163 @@
|
|
|
1
1
|
let __aws_sdk_client_s3 = require("@aws-sdk/client-s3");
|
|
2
2
|
let __statewalker_webrun_files = require("@statewalker/webrun-files");
|
|
3
3
|
|
|
4
|
-
//#region src/s3-
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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.
|
|
37
|
-
this.
|
|
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
|
-
|
|
44
|
-
|
|
26
|
+
resolveKey(path) {
|
|
27
|
+
const normalized = (0, __statewalker_webrun_files.normalizePath)(path);
|
|
28
|
+
const relativePath = normalized.startsWith("/") ? normalized.substring(1) : normalized;
|
|
29
|
+
if (this.prefix) return relativePath ? `${this.prefix}/${relativePath}` : this.prefix;
|
|
30
|
+
return relativePath;
|
|
45
31
|
}
|
|
46
|
-
|
|
47
|
-
|
|
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 __aws_sdk_client_s3.GetObjectCommand({
|
|
57
|
-
Bucket: this.bucket,
|
|
58
|
-
Key: this.key,
|
|
59
|
-
Range: `bytes=${start}-${actualEnd - 1}`
|
|
60
|
-
});
|
|
61
|
-
const response = await this.client.send(command, { abortSignal: signal });
|
|
62
|
-
if (!response.Body) return;
|
|
63
|
-
for await (const chunk of streamBody(response.Body)) {
|
|
64
|
-
if (signal?.aborted) throw new Error("Operation aborted");
|
|
65
|
-
yield chunk;
|
|
32
|
+
keyToPath(key) {
|
|
33
|
+
let relativePath = key;
|
|
34
|
+
if (this.prefix && key.startsWith(this.prefix)) {
|
|
35
|
+
relativePath = key.substring(this.prefix.length);
|
|
36
|
+
if (relativePath.startsWith("/")) relativePath = relativePath.substring(1);
|
|
66
37
|
}
|
|
38
|
+
return `/${relativePath}`;
|
|
67
39
|
}
|
|
68
|
-
|
|
69
|
-
|
|
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);
|
|
40
|
+
async *read(path, options) {
|
|
41
|
+
const key = this.resolveKey(path);
|
|
42
|
+
try {
|
|
43
|
+
const start = options?.start ?? 0;
|
|
44
|
+
const length = options?.length;
|
|
45
|
+
if (length === 0) return;
|
|
46
|
+
let range;
|
|
47
|
+
if (start > 0 || length !== void 0) range = `bytes=${start}-${length !== void 0 ? start + length - 1 : ""}`;
|
|
48
|
+
const command = new __aws_sdk_client_s3.GetObjectCommand({
|
|
49
|
+
Bucket: this.bucket,
|
|
50
|
+
Key: key,
|
|
51
|
+
Range: range
|
|
52
|
+
});
|
|
53
|
+
const response = await this.client.send(command);
|
|
54
|
+
if (response.Body) {
|
|
55
|
+
const stream = response.Body;
|
|
56
|
+
for await (const chunk of stream) yield chunk;
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const err = error;
|
|
60
|
+
if (err.name === "NotFound" || err.name === "InvalidRange" || err.$metadata?.httpStatusCode === 404 || err.$metadata?.httpStatusCode === 416) return;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
93
63
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
*/
|
|
100
|
-
async streamingMultipartUpload(data, start, signal) {
|
|
101
|
-
const createCommand = new __aws_sdk_client_s3.CreateMultipartUploadCommand({
|
|
102
|
-
Bucket: this.bucket,
|
|
103
|
-
Key: this.key
|
|
104
|
-
});
|
|
105
|
-
const { UploadId } = await this.client.send(createCommand, { abortSignal: signal });
|
|
106
|
-
if (!UploadId) throw new Error("Failed to initiate multipart upload");
|
|
64
|
+
async write(path, content) {
|
|
65
|
+
const key = this.resolveKey(path);
|
|
66
|
+
let buffer = [];
|
|
67
|
+
let bufferSize = 0;
|
|
68
|
+
let uploadId;
|
|
107
69
|
const parts = [];
|
|
108
70
|
let partNumber = 1;
|
|
109
|
-
|
|
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 __aws_sdk_client_s3.UploadPartCopyCommand({
|
|
122
|
-
Bucket: this.bucket,
|
|
123
|
-
Key: this.key,
|
|
124
|
-
UploadId,
|
|
125
|
-
PartNumber: partNumber,
|
|
126
|
-
CopySource: `${this.bucket}/${this.key}`,
|
|
127
|
-
CopySourceRange: `bytes=${copyOffset}-${copyEnd - 1}`
|
|
128
|
-
});
|
|
129
|
-
const { CopyPartResult } = await this.client.send(copyCommand, { abortSignal: signal });
|
|
130
|
-
if (CopyPartResult?.ETag) {
|
|
131
|
-
parts.push({
|
|
132
|
-
ETag: CopyPartResult.ETag,
|
|
133
|
-
PartNumber: partNumber
|
|
134
|
-
});
|
|
135
|
-
partNumber++;
|
|
136
|
-
totalSize += this.multipartPartSize;
|
|
137
|
-
}
|
|
138
|
-
copyOffset = copyEnd;
|
|
139
|
-
}
|
|
140
|
-
if (fullPartsEnd < preserveEnd) {
|
|
141
|
-
const remainingBytes = await this.downloadRange(fullPartsEnd, preserveEnd, signal);
|
|
142
|
-
buffer.set(remainingBytes, bufferOffset);
|
|
143
|
-
bufferOffset += remainingBytes.length;
|
|
144
|
-
totalSize += remainingBytes.length;
|
|
145
|
-
}
|
|
146
|
-
if (start > this._size) {
|
|
147
|
-
const paddingSize = start - this._size;
|
|
148
|
-
let paddingOffset = 0;
|
|
149
|
-
while (paddingOffset < paddingSize) {
|
|
150
|
-
const spaceInBuffer = this.multipartPartSize - bufferOffset;
|
|
151
|
-
const bytesToAdd = Math.min(spaceInBuffer, paddingSize - paddingOffset);
|
|
152
|
-
bufferOffset += bytesToAdd;
|
|
153
|
-
paddingOffset += bytesToAdd;
|
|
154
|
-
totalSize += bytesToAdd;
|
|
155
|
-
if (bufferOffset >= this.multipartPartSize) {
|
|
156
|
-
await this.uploadPart(UploadId, partNumber, buffer, parts, signal);
|
|
157
|
-
partNumber++;
|
|
158
|
-
buffer = new Uint8Array(this.multipartPartSize);
|
|
159
|
-
bufferOffset = 0;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
} else if (start > 0) {
|
|
164
|
-
let paddingOffset = 0;
|
|
165
|
-
while (paddingOffset < start) {
|
|
166
|
-
const spaceInBuffer = this.multipartPartSize - bufferOffset;
|
|
167
|
-
const bytesToAdd = Math.min(spaceInBuffer, start - paddingOffset);
|
|
168
|
-
bufferOffset += bytesToAdd;
|
|
169
|
-
paddingOffset += bytesToAdd;
|
|
170
|
-
totalSize += bytesToAdd;
|
|
171
|
-
if (bufferOffset >= this.multipartPartSize) {
|
|
172
|
-
await this.uploadPart(UploadId, partNumber, buffer, parts, signal);
|
|
173
|
-
partNumber++;
|
|
174
|
-
buffer = new Uint8Array(this.multipartPartSize);
|
|
175
|
-
bufferOffset = 0;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
71
|
+
const flushPart = async (isLast) => {
|
|
72
|
+
if (buffer.length === 0) return;
|
|
73
|
+
const partLength = buffer.reduce((sum, c) => sum + c.length, 0);
|
|
74
|
+
const partBuffer = new Uint8Array(partLength);
|
|
75
|
+
let offset = 0;
|
|
76
|
+
for (const chunk of buffer) {
|
|
77
|
+
partBuffer.set(chunk, offset);
|
|
78
|
+
offset += chunk.length;
|
|
178
79
|
}
|
|
179
|
-
|
|
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
|
-
}
|
|
80
|
+
buffer = [];
|
|
81
|
+
bufferSize = 0;
|
|
82
|
+
if (!uploadId && isLast && partLength < MULTIPART_THRESHOLD) {
|
|
83
|
+
const command = new __aws_sdk_client_s3.PutObjectCommand({
|
|
84
|
+
Bucket: this.bucket,
|
|
85
|
+
Key: key,
|
|
86
|
+
Body: partBuffer,
|
|
87
|
+
ContentLength: partLength
|
|
88
|
+
});
|
|
89
|
+
await this.client.send(command);
|
|
90
|
+
return;
|
|
197
91
|
}
|
|
198
|
-
if (
|
|
199
|
-
const
|
|
200
|
-
|
|
92
|
+
if (!uploadId) {
|
|
93
|
+
const createCommand = new __aws_sdk_client_s3.CreateMultipartUploadCommand({
|
|
94
|
+
Bucket: this.bucket,
|
|
95
|
+
Key: key
|
|
96
|
+
});
|
|
97
|
+
const { UploadId } = await this.client.send(createCommand);
|
|
98
|
+
if (!UploadId) throw new Error("Failed to create multipart upload");
|
|
99
|
+
uploadId = UploadId;
|
|
201
100
|
}
|
|
202
|
-
|
|
203
|
-
const completeCommand = new __aws_sdk_client_s3.CompleteMultipartUploadCommand({
|
|
101
|
+
const uploadCommand = new __aws_sdk_client_s3.UploadPartCommand({
|
|
204
102
|
Bucket: this.bucket,
|
|
205
|
-
Key:
|
|
206
|
-
UploadId,
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
114
|
+
partNumber++;
|
|
115
|
+
};
|
|
116
|
+
try {
|
|
117
|
+
for await (const chunk of content) {
|
|
118
|
+
buffer.push(chunk);
|
|
119
|
+
bufferSize += chunk.length;
|
|
120
|
+
if (bufferSize >= this.partSize) await flushPart(false);
|
|
121
|
+
}
|
|
122
|
+
await flushPart(true);
|
|
123
|
+
if (uploadId) {
|
|
124
|
+
const completeCommand = new __aws_sdk_client_s3.CompleteMultipartUploadCommand({
|
|
125
|
+
Bucket: this.bucket,
|
|
126
|
+
Key: key,
|
|
127
|
+
UploadId: uploadId,
|
|
128
|
+
MultipartUpload: { Parts: parts.sort((a, b) => a.PartNumber - b.PartNumber) }
|
|
129
|
+
});
|
|
130
|
+
await this.client.send(completeCommand);
|
|
131
|
+
}
|
|
212
132
|
} catch (error) {
|
|
213
|
-
|
|
133
|
+
if (uploadId) {
|
|
134
|
+
const abortCommand = new __aws_sdk_client_s3.AbortMultipartUploadCommand({
|
|
135
|
+
Bucket: this.bucket,
|
|
136
|
+
Key: key,
|
|
137
|
+
UploadId: uploadId
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
await this.client.send(abortCommand);
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
214
143
|
throw error;
|
|
215
144
|
}
|
|
216
145
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (end <= start) return new Uint8Array(0);
|
|
222
|
-
const command = new __aws_sdk_client_s3.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 __aws_sdk_client_s3.UploadPartCommand({
|
|
247
|
-
Bucket: this.bucket,
|
|
248
|
-
Key: this.key,
|
|
249
|
-
UploadId: uploadId,
|
|
250
|
-
PartNumber: partNumber,
|
|
251
|
-
Body: data,
|
|
252
|
-
ContentLength: data.length
|
|
253
|
-
});
|
|
254
|
-
const { ETag } = await this.client.send(command, { abortSignal: signal });
|
|
255
|
-
if (ETag) parts.push({
|
|
256
|
-
ETag,
|
|
257
|
-
PartNumber: partNumber
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Abort a multipart upload, ignoring errors.
|
|
262
|
-
*/
|
|
263
|
-
async abortMultipartUpload(uploadId) {
|
|
264
|
-
const command = new __aws_sdk_client_s3.AbortMultipartUploadCommand({
|
|
265
|
-
Bucket: this.bucket,
|
|
266
|
-
Key: this.key,
|
|
267
|
-
UploadId: uploadId
|
|
268
|
-
});
|
|
269
|
-
await this.client.send(command).catch(() => {});
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Random access read from S3 object using HTTP Range header.
|
|
273
|
-
*/
|
|
274
|
-
async read(buffer, offset, length, position) {
|
|
275
|
-
if (this.closed) throw new Error("FileHandle is closed");
|
|
276
|
-
let len = Math.min(length, buffer.length - offset);
|
|
277
|
-
len = Math.min(len, this._size - position);
|
|
278
|
-
len = Math.max(0, len);
|
|
279
|
-
if (len === 0) return 0;
|
|
280
|
-
const command = new __aws_sdk_client_s3.GetObjectCommand({
|
|
146
|
+
async mkdir(path) {
|
|
147
|
+
const key = this.resolveKey(path);
|
|
148
|
+
const dirKey = key.endsWith("/") ? key : `${key}/`;
|
|
149
|
+
const command = new __aws_sdk_client_s3.PutObjectCommand({
|
|
281
150
|
Bucket: this.bucket,
|
|
282
|
-
Key:
|
|
283
|
-
|
|
151
|
+
Key: dirKey,
|
|
152
|
+
Body: new Uint8Array(0),
|
|
153
|
+
ContentLength: 0
|
|
284
154
|
});
|
|
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 = (0, __statewalker_webrun_files.resolveFileRef)(file);
|
|
326
|
-
const relativePath = normalized.startsWith("/") ? normalized.substring(1) : normalized;
|
|
327
|
-
if (this.prefix) return relativePath ? `${this.prefix}/${relativePath}` : this.prefix;
|
|
328
|
-
return relativePath;
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Converts an S3 key back to a virtual path.
|
|
332
|
-
*/
|
|
333
|
-
keyToPath(key) {
|
|
334
|
-
let relativePath = key;
|
|
335
|
-
if (this.prefix && key.startsWith(this.prefix)) {
|
|
336
|
-
relativePath = key.substring(this.prefix.length);
|
|
337
|
-
if (relativePath.startsWith("/")) relativePath = relativePath.substring(1);
|
|
338
|
-
}
|
|
339
|
-
return "/" + relativePath;
|
|
155
|
+
await this.client.send(command);
|
|
340
156
|
}
|
|
341
|
-
|
|
342
|
-
|
|
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 + "/" : "";
|
|
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 __aws_sdk_client_s3.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
|
|
174
|
+
const dirPath = this.keyToPath(dirKey);
|
|
365
175
|
yield {
|
|
366
176
|
kind: "directory",
|
|
367
|
-
name: (0, __statewalker_webrun_files.basename)(
|
|
368
|
-
path,
|
|
177
|
+
name: (0, __statewalker_webrun_files.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
|
|
185
|
+
if (`${obj.Key}/` === normalizedPrefix) continue;
|
|
376
186
|
if (obj.Key.endsWith("/")) continue;
|
|
377
|
-
const
|
|
187
|
+
const filePath = this.keyToPath(obj.Key);
|
|
378
188
|
yield {
|
|
379
189
|
kind: "file",
|
|
380
|
-
name: (0, __statewalker_webrun_files.basename)(
|
|
381
|
-
path,
|
|
190
|
+
name: (0, __statewalker_webrun_files.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
|
-
|
|
391
|
-
|
|
392
|
-
async stats(file) {
|
|
393
|
-
const normalized = (0, __statewalker_webrun_files.resolveFileRef)(file);
|
|
394
|
-
const key = this.resolveKey(file);
|
|
199
|
+
async stats(path) {
|
|
200
|
+
const normalized = (0, __statewalker_webrun_files.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: (0, __statewalker_webrun_files.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 __aws_sdk_client_s3.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: (0, __statewalker_webrun_files.basename)(normalized),
|
|
429
|
-
path: normalized,
|
|
430
230
|
lastModified: 0
|
|
431
231
|
};
|
|
432
232
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
async remove(
|
|
437
|
-
const key = this.resolveKey(
|
|
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 __aws_sdk_client_s3.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 __aws_sdk_client_s3.HeadObjectCommand({
|
|
488
|
-
Bucket: this.bucket,
|
|
489
|
-
Key: key
|
|
490
|
-
});
|
|
491
|
-
size = (await this.client.send(command)).ContentLength ?? 0;
|
|
492
|
-
} catch (error) {
|
|
493
|
-
const err = error;
|
|
494
|
-
if (err.name !== "NotFound" && err.$metadata?.httpStatusCode !== 404) throw error;
|
|
495
|
-
}
|
|
496
|
-
return new S3FileHandle({
|
|
497
|
-
client: this.client,
|
|
498
|
-
bucket: this.bucket,
|
|
499
|
-
key,
|
|
500
|
-
size,
|
|
501
|
-
multipartPartSize: this.multipartPartSize
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* Moves an object using Copy + Delete.
|
|
506
|
-
*/
|
|
507
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
|
|
518
|
-
|
|
519
|
-
if (
|
|
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 __aws_sdk_client_s3.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
|
-
|
|
530
|
-
const
|
|
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,27 +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 __aws_sdk_client_s3.PutObjectCommand({
|
|
568
|
-
Bucket: this.bucket,
|
|
569
|
-
Key: dirKey,
|
|
570
|
-
Body: new Uint8Array(0),
|
|
571
|
-
ContentLength: 0
|
|
572
|
-
});
|
|
573
|
-
await this.client.send(command);
|
|
574
|
-
}
|
|
575
323
|
};
|
|
576
324
|
|
|
577
325
|
//#endregion
|
|
578
|
-
exports.S3FileHandle = S3FileHandle;
|
|
579
326
|
exports.S3FilesApi = S3FilesApi;
|