@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/cjs/index.cjs
CHANGED
|
@@ -1,356 +1,166 @@
|
|
|
1
|
-
|
|
2
|
-
let
|
|
3
|
-
|
|
4
|
-
//#region src/s3-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
|
|
3
|
+
let _statewalker_webrun_files = require("@statewalker/webrun-files");
|
|
4
|
+
//#region src/s3-files-api.ts
|
|
5
|
+
/** Default part size for multipart uploads: 5MB (S3 minimum) */
|
|
6
|
+
const DEFAULT_PART_SIZE = 5242880;
|
|
7
|
+
/** Threshold for using multipart upload: 5MB */
|
|
8
|
+
const MULTIPART_THRESHOLD = 5242880;
|
|
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({
|
|
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({
|
|
223
150
|
Bucket: this.bucket,
|
|
224
|
-
Key:
|
|
225
|
-
|
|
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({
|
|
281
|
-
Bucket: this.bucket,
|
|
282
|
-
Key: this.key,
|
|
283
|
-
Range: `bytes=${position}-${position + len - 1}`
|
|
151
|
+
Key: dirKey,
|
|
152
|
+
Body: /* @__PURE__ */ 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
|
-
const command = new
|
|
163
|
+
const command = new _aws_sdk_client_s3.ListObjectsV2Command({
|
|
354
164
|
Bucket: this.bucket,
|
|
355
165
|
Prefix: normalizedPrefix,
|
|
356
166
|
Delimiter: recursive ? void 0 : "/",
|
|
@@ -361,24 +171,23 @@ 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,
|
|
368
|
-
path
|
|
369
|
-
lastModified: 0
|
|
177
|
+
name: (0, _statewalker_webrun_files.basename)(dirPath),
|
|
178
|
+
path: dirPath
|
|
370
179
|
};
|
|
371
180
|
}
|
|
372
181
|
if (response.Contents) for (const obj of response.Contents) {
|
|
373
182
|
if (!obj.Key) continue;
|
|
374
183
|
if (obj.Key === normalizedPrefix) continue;
|
|
375
|
-
if (obj.Key
|
|
184
|
+
if (`${obj.Key}/` === normalizedPrefix) continue;
|
|
376
185
|
if (obj.Key.endsWith("/")) continue;
|
|
377
|
-
const
|
|
186
|
+
const filePath = this.keyToPath(obj.Key);
|
|
378
187
|
yield {
|
|
379
188
|
kind: "file",
|
|
380
|
-
name: (0,
|
|
381
|
-
path,
|
|
189
|
+
name: (0, _statewalker_webrun_files.basename)(filePath),
|
|
190
|
+
path: filePath,
|
|
382
191
|
size: obj.Size ?? 0,
|
|
383
192
|
lastModified: obj.LastModified?.getTime() ?? 0
|
|
384
193
|
};
|
|
@@ -386,62 +195,46 @@ var S3FilesApi = class {
|
|
|
386
195
|
continuationToken = response.NextContinuationToken;
|
|
387
196
|
} while (continuationToken);
|
|
388
197
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const normalized = (0, __statewalker_webrun_files.resolveFileRef)(file);
|
|
394
|
-
const key = this.resolveKey(file);
|
|
395
|
-
if (normalized === "/") return {
|
|
396
|
-
kind: "directory",
|
|
397
|
-
name: "",
|
|
398
|
-
path: "/",
|
|
399
|
-
lastModified: 0
|
|
400
|
-
};
|
|
198
|
+
async stats(path) {
|
|
199
|
+
const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
|
|
200
|
+
const key = this.resolveKey(path);
|
|
201
|
+
if (normalized === "/") return { kind: "directory" };
|
|
401
202
|
try {
|
|
402
|
-
const command = new
|
|
203
|
+
const command = new _aws_sdk_client_s3.HeadObjectCommand({
|
|
403
204
|
Bucket: this.bucket,
|
|
404
205
|
Key: key
|
|
405
206
|
});
|
|
406
207
|
const response = await this.client.send(command);
|
|
407
208
|
return {
|
|
408
209
|
kind: "file",
|
|
409
|
-
name: (0, __statewalker_webrun_files.basename)(normalized),
|
|
410
|
-
path: normalized,
|
|
411
210
|
size: response.ContentLength ?? 0,
|
|
412
|
-
lastModified: response.LastModified?.getTime() ?? 0
|
|
413
|
-
type: response.ContentType
|
|
211
|
+
lastModified: response.LastModified?.getTime() ?? 0
|
|
414
212
|
};
|
|
415
213
|
} catch (error) {
|
|
416
214
|
const err = error;
|
|
417
215
|
if (err.name !== "NotFound" && err.$metadata?.httpStatusCode !== 404) throw error;
|
|
418
216
|
}
|
|
419
|
-
const prefix = key ? key
|
|
420
|
-
const listCommand = new
|
|
217
|
+
const prefix = key ? `${key}/` : "";
|
|
218
|
+
const listCommand = new _aws_sdk_client_s3.ListObjectsV2Command({
|
|
421
219
|
Bucket: this.bucket,
|
|
422
220
|
Prefix: prefix,
|
|
423
221
|
MaxKeys: 1
|
|
424
222
|
});
|
|
425
223
|
const listResponse = await this.client.send(listCommand);
|
|
426
|
-
if (listResponse.Contents && listResponse.Contents.length > 0 || listResponse.CommonPrefixes && listResponse.CommonPrefixes.length > 0) return {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
lastModified: 0
|
|
431
|
-
};
|
|
224
|
+
if (listResponse.Contents && listResponse.Contents.length > 0 || listResponse.CommonPrefixes && listResponse.CommonPrefixes.length > 0) return { kind: "directory" };
|
|
225
|
+
}
|
|
226
|
+
async exists(path) {
|
|
227
|
+
return await this.stats(path) !== void 0;
|
|
432
228
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
*/
|
|
436
|
-
async remove(file) {
|
|
437
|
-
const key = this.resolveKey(file);
|
|
229
|
+
async remove(path) {
|
|
230
|
+
const key = this.resolveKey(path);
|
|
438
231
|
try {
|
|
439
|
-
const headCommand = new
|
|
232
|
+
const headCommand = new _aws_sdk_client_s3.HeadObjectCommand({
|
|
440
233
|
Bucket: this.bucket,
|
|
441
234
|
Key: key
|
|
442
235
|
});
|
|
443
236
|
await this.client.send(headCommand);
|
|
444
|
-
const deleteCommand = new
|
|
237
|
+
const deleteCommand = new _aws_sdk_client_s3.DeleteObjectCommand({
|
|
445
238
|
Bucket: this.bucket,
|
|
446
239
|
Key: key
|
|
447
240
|
});
|
|
@@ -451,11 +244,11 @@ var S3FilesApi = class {
|
|
|
451
244
|
const err = error;
|
|
452
245
|
if (err.name !== "NotFound" && err.$metadata?.httpStatusCode !== 404) throw error;
|
|
453
246
|
}
|
|
454
|
-
const prefix = key ? key
|
|
247
|
+
const prefix = key ? `${key}/` : "";
|
|
455
248
|
const keysToDelete = [];
|
|
456
249
|
let continuationToken;
|
|
457
250
|
do {
|
|
458
|
-
const listCommand = new
|
|
251
|
+
const listCommand = new _aws_sdk_client_s3.ListObjectsV2Command({
|
|
459
252
|
Bucket: this.bucket,
|
|
460
253
|
Prefix: prefix,
|
|
461
254
|
ContinuationToken: continuationToken
|
|
@@ -468,7 +261,7 @@ var S3FilesApi = class {
|
|
|
468
261
|
} while (continuationToken);
|
|
469
262
|
if (keysToDelete.length === 0) return false;
|
|
470
263
|
for (const keyToDelete of keysToDelete) {
|
|
471
|
-
const deleteCommand = new
|
|
264
|
+
const deleteCommand = new _aws_sdk_client_s3.DeleteObjectCommand({
|
|
472
265
|
Bucket: this.bucket,
|
|
473
266
|
Key: keyToDelete
|
|
474
267
|
});
|
|
@@ -476,49 +269,17 @@ var S3FilesApi = class {
|
|
|
476
269
|
}
|
|
477
270
|
return true;
|
|
478
271
|
}
|
|
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
272
|
async move(source, target) {
|
|
508
273
|
if (!await this.copy(source, target)) return false;
|
|
509
274
|
return this.remove(source);
|
|
510
275
|
}
|
|
511
|
-
|
|
512
|
-
* Copies an object or directory using S3 CopyObject.
|
|
513
|
-
*/
|
|
514
|
-
async copy(source, target, options = {}) {
|
|
276
|
+
async copy(source, target) {
|
|
515
277
|
const sourceKey = this.resolveKey(source);
|
|
516
278
|
const targetKey = this.resolveKey(target);
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
const command = new __aws_sdk_client_s3.CopyObjectCommand({
|
|
279
|
+
const sourceStats = await this.stats(source);
|
|
280
|
+
if (!sourceStats) return false;
|
|
281
|
+
if (sourceStats.kind === "file") {
|
|
282
|
+
const command = new _aws_sdk_client_s3.CopyObjectCommand({
|
|
522
283
|
Bucket: this.bucket,
|
|
523
284
|
Key: targetKey,
|
|
524
285
|
CopySource: encodeURIComponent(`${this.bucket}/${sourceKey}`)
|
|
@@ -526,13 +287,12 @@ var S3FilesApi = class {
|
|
|
526
287
|
await this.client.send(command);
|
|
527
288
|
return true;
|
|
528
289
|
}
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
const targetPrefix = targetKey + "/";
|
|
290
|
+
const sourcePrefix = `${sourceKey}/`;
|
|
291
|
+
const targetPrefix = `${targetKey}/`;
|
|
532
292
|
let continuationToken;
|
|
533
293
|
let copied = false;
|
|
534
294
|
do {
|
|
535
|
-
const listCommand = new
|
|
295
|
+
const listCommand = new _aws_sdk_client_s3.ListObjectsV2Command({
|
|
536
296
|
Bucket: this.bucket,
|
|
537
297
|
Prefix: sourcePrefix,
|
|
538
298
|
ContinuationToken: continuationToken
|
|
@@ -541,7 +301,7 @@ var S3FilesApi = class {
|
|
|
541
301
|
if (response.Contents) for (const obj of response.Contents) {
|
|
542
302
|
if (!obj.Key) continue;
|
|
543
303
|
const newKey = targetPrefix + obj.Key.substring(sourcePrefix.length);
|
|
544
|
-
const copyCommand = new
|
|
304
|
+
const copyCommand = new _aws_sdk_client_s3.CopyObjectCommand({
|
|
545
305
|
Bucket: this.bucket,
|
|
546
306
|
Key: newKey,
|
|
547
307
|
CopySource: encodeURIComponent(`${this.bucket}/${obj.Key}`)
|
|
@@ -553,27 +313,6 @@ var S3FilesApi = class {
|
|
|
553
313
|
} while (continuationToken);
|
|
554
314
|
return copied;
|
|
555
315
|
}
|
|
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
316
|
};
|
|
576
|
-
|
|
577
317
|
//#endregion
|
|
578
|
-
exports.
|
|
579
|
-
exports.S3FilesApi = S3FilesApi;
|
|
318
|
+
exports.S3FilesApi = S3FilesApi;
|