@statewalker/webrun-files-s3 0.7.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/README.md +11 -0
- package/dist/cjs/index.cjs +31 -39
- package/dist/esm/index.js +7 -16
- package/dist/s3-files-api.d.ts.map +1 -1
- package/package.json +14 -14
- package/src/s3-files-api.ts +10 -9
package/README.md
CHANGED
|
@@ -142,6 +142,17 @@ S3 doesn't have real directories, but this implementation simulates them using:
|
|
|
142
142
|
- **ListObjectsV2** with `Delimiter="/"` to get "subdirectories" via `CommonPrefixes`
|
|
143
143
|
- Files are returned from `Contents`
|
|
144
144
|
|
|
145
|
+
Because `FileStats` is a discriminated union, this implementation has to commit
|
|
146
|
+
to two things a store without real directories could otherwise leave vague:
|
|
147
|
+
|
|
148
|
+
- A **common prefix is the directory variant** - `{ kind: "directory" }` and
|
|
149
|
+
nothing more. There is no size or modification time to report for something
|
|
150
|
+
that exists only as an artefact of key naming.
|
|
151
|
+
- A **zero-byte key ending in `/` is a directory, not a file**. That is the
|
|
152
|
+
marker `mkdir()` writes so an empty directory is visible, and it is skipped
|
|
153
|
+
when reading `Contents`. Every other key is a file, and a genuinely empty
|
|
154
|
+
object is the file variant with `size: 0`.
|
|
155
|
+
|
|
145
156
|
```typescript
|
|
146
157
|
// List /docs with prefix "my-app"
|
|
147
158
|
// S3 request: ListObjectsV2(Prefix="my-app/docs/", Delimiter="/")
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
let
|
|
3
|
-
|
|
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
4
|
//#region src/s3-files-api.ts
|
|
5
5
|
/** Default part size for multipart uploads: 5MB (S3 minimum) */
|
|
6
|
-
const DEFAULT_PART_SIZE =
|
|
6
|
+
const DEFAULT_PART_SIZE = 5242880;
|
|
7
7
|
/** Threshold for using multipart upload: 5MB */
|
|
8
|
-
const MULTIPART_THRESHOLD =
|
|
8
|
+
const MULTIPART_THRESHOLD = 5242880;
|
|
9
9
|
/**
|
|
10
10
|
* S3 filesystem implementation of FilesApi.
|
|
11
11
|
*
|
|
@@ -24,7 +24,7 @@ var S3FilesApi = class {
|
|
|
24
24
|
this.partSize = options.multipartPartSize ?? DEFAULT_PART_SIZE;
|
|
25
25
|
}
|
|
26
26
|
resolveKey(path) {
|
|
27
|
-
const normalized = (0,
|
|
27
|
+
const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
|
|
28
28
|
const relativePath = normalized.startsWith("/") ? normalized.substring(1) : normalized;
|
|
29
29
|
if (this.prefix) return relativePath ? `${this.prefix}/${relativePath}` : this.prefix;
|
|
30
30
|
return relativePath;
|
|
@@ -45,7 +45,7 @@ var S3FilesApi = class {
|
|
|
45
45
|
if (length === 0) return;
|
|
46
46
|
let range;
|
|
47
47
|
if (start > 0 || length !== void 0) range = `bytes=${start}-${length !== void 0 ? start + length - 1 : ""}`;
|
|
48
|
-
const command = new
|
|
48
|
+
const command = new _aws_sdk_client_s3.GetObjectCommand({
|
|
49
49
|
Bucket: this.bucket,
|
|
50
50
|
Key: key,
|
|
51
51
|
Range: range
|
|
@@ -80,7 +80,7 @@ var S3FilesApi = class {
|
|
|
80
80
|
buffer = [];
|
|
81
81
|
bufferSize = 0;
|
|
82
82
|
if (!uploadId && isLast && partLength < MULTIPART_THRESHOLD) {
|
|
83
|
-
const command = new
|
|
83
|
+
const command = new _aws_sdk_client_s3.PutObjectCommand({
|
|
84
84
|
Bucket: this.bucket,
|
|
85
85
|
Key: key,
|
|
86
86
|
Body: partBuffer,
|
|
@@ -90,7 +90,7 @@ var S3FilesApi = class {
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
if (!uploadId) {
|
|
93
|
-
const createCommand = new
|
|
93
|
+
const createCommand = new _aws_sdk_client_s3.CreateMultipartUploadCommand({
|
|
94
94
|
Bucket: this.bucket,
|
|
95
95
|
Key: key
|
|
96
96
|
});
|
|
@@ -98,7 +98,7 @@ var S3FilesApi = class {
|
|
|
98
98
|
if (!UploadId) throw new Error("Failed to create multipart upload");
|
|
99
99
|
uploadId = UploadId;
|
|
100
100
|
}
|
|
101
|
-
const uploadCommand = new
|
|
101
|
+
const uploadCommand = new _aws_sdk_client_s3.UploadPartCommand({
|
|
102
102
|
Bucket: this.bucket,
|
|
103
103
|
Key: key,
|
|
104
104
|
UploadId: uploadId,
|
|
@@ -121,7 +121,7 @@ var S3FilesApi = class {
|
|
|
121
121
|
}
|
|
122
122
|
await flushPart(true);
|
|
123
123
|
if (uploadId) {
|
|
124
|
-
const completeCommand = new
|
|
124
|
+
const completeCommand = new _aws_sdk_client_s3.CompleteMultipartUploadCommand({
|
|
125
125
|
Bucket: this.bucket,
|
|
126
126
|
Key: key,
|
|
127
127
|
UploadId: uploadId,
|
|
@@ -131,7 +131,7 @@ var S3FilesApi = class {
|
|
|
131
131
|
}
|
|
132
132
|
} catch (error) {
|
|
133
133
|
if (uploadId) {
|
|
134
|
-
const abortCommand = new
|
|
134
|
+
const abortCommand = new _aws_sdk_client_s3.AbortMultipartUploadCommand({
|
|
135
135
|
Bucket: this.bucket,
|
|
136
136
|
Key: key,
|
|
137
137
|
UploadId: uploadId
|
|
@@ -146,10 +146,10 @@ var S3FilesApi = class {
|
|
|
146
146
|
async mkdir(path) {
|
|
147
147
|
const key = this.resolveKey(path);
|
|
148
148
|
const dirKey = key.endsWith("/") ? key : `${key}/`;
|
|
149
|
-
const command = new
|
|
149
|
+
const command = new _aws_sdk_client_s3.PutObjectCommand({
|
|
150
150
|
Bucket: this.bucket,
|
|
151
151
|
Key: dirKey,
|
|
152
|
-
Body: new Uint8Array(0),
|
|
152
|
+
Body: /* @__PURE__ */ new Uint8Array(0),
|
|
153
153
|
ContentLength: 0
|
|
154
154
|
});
|
|
155
155
|
await this.client.send(command);
|
|
@@ -160,7 +160,7 @@ var S3FilesApi = class {
|
|
|
160
160
|
const normalizedPrefix = prefix ? `${prefix}/` : this.prefix ? `${this.prefix}/` : "";
|
|
161
161
|
let continuationToken;
|
|
162
162
|
do {
|
|
163
|
-
const command = new
|
|
163
|
+
const command = new _aws_sdk_client_s3.ListObjectsV2Command({
|
|
164
164
|
Bucket: this.bucket,
|
|
165
165
|
Prefix: normalizedPrefix,
|
|
166
166
|
Delimiter: recursive ? void 0 : "/",
|
|
@@ -174,9 +174,8 @@ var S3FilesApi = class {
|
|
|
174
174
|
const dirPath = this.keyToPath(dirKey);
|
|
175
175
|
yield {
|
|
176
176
|
kind: "directory",
|
|
177
|
-
name: (0,
|
|
178
|
-
path: dirPath
|
|
179
|
-
lastModified: 0
|
|
177
|
+
name: (0, _statewalker_webrun_files.basename)(dirPath),
|
|
178
|
+
path: dirPath
|
|
180
179
|
};
|
|
181
180
|
}
|
|
182
181
|
if (response.Contents) for (const obj of response.Contents) {
|
|
@@ -187,7 +186,7 @@ var S3FilesApi = class {
|
|
|
187
186
|
const filePath = this.keyToPath(obj.Key);
|
|
188
187
|
yield {
|
|
189
188
|
kind: "file",
|
|
190
|
-
name: (0,
|
|
189
|
+
name: (0, _statewalker_webrun_files.basename)(filePath),
|
|
191
190
|
path: filePath,
|
|
192
191
|
size: obj.Size ?? 0,
|
|
193
192
|
lastModified: obj.LastModified?.getTime() ?? 0
|
|
@@ -197,14 +196,11 @@ var S3FilesApi = class {
|
|
|
197
196
|
} while (continuationToken);
|
|
198
197
|
}
|
|
199
198
|
async stats(path) {
|
|
200
|
-
const normalized = (0,
|
|
199
|
+
const normalized = (0, _statewalker_webrun_files.normalizePath)(path);
|
|
201
200
|
const key = this.resolveKey(path);
|
|
202
|
-
if (normalized === "/") return {
|
|
203
|
-
kind: "directory",
|
|
204
|
-
lastModified: 0
|
|
205
|
-
};
|
|
201
|
+
if (normalized === "/") return { kind: "directory" };
|
|
206
202
|
try {
|
|
207
|
-
const command = new
|
|
203
|
+
const command = new _aws_sdk_client_s3.HeadObjectCommand({
|
|
208
204
|
Bucket: this.bucket,
|
|
209
205
|
Key: key
|
|
210
206
|
});
|
|
@@ -219,16 +215,13 @@ var S3FilesApi = class {
|
|
|
219
215
|
if (err.name !== "NotFound" && err.$metadata?.httpStatusCode !== 404) throw error;
|
|
220
216
|
}
|
|
221
217
|
const prefix = key ? `${key}/` : "";
|
|
222
|
-
const listCommand = new
|
|
218
|
+
const listCommand = new _aws_sdk_client_s3.ListObjectsV2Command({
|
|
223
219
|
Bucket: this.bucket,
|
|
224
220
|
Prefix: prefix,
|
|
225
221
|
MaxKeys: 1
|
|
226
222
|
});
|
|
227
223
|
const listResponse = await this.client.send(listCommand);
|
|
228
|
-
if (listResponse.Contents && listResponse.Contents.length > 0 || listResponse.CommonPrefixes && listResponse.CommonPrefixes.length > 0) return {
|
|
229
|
-
kind: "directory",
|
|
230
|
-
lastModified: 0
|
|
231
|
-
};
|
|
224
|
+
if (listResponse.Contents && listResponse.Contents.length > 0 || listResponse.CommonPrefixes && listResponse.CommonPrefixes.length > 0) return { kind: "directory" };
|
|
232
225
|
}
|
|
233
226
|
async exists(path) {
|
|
234
227
|
return await this.stats(path) !== void 0;
|
|
@@ -236,12 +229,12 @@ var S3FilesApi = class {
|
|
|
236
229
|
async remove(path) {
|
|
237
230
|
const key = this.resolveKey(path);
|
|
238
231
|
try {
|
|
239
|
-
const headCommand = new
|
|
232
|
+
const headCommand = new _aws_sdk_client_s3.HeadObjectCommand({
|
|
240
233
|
Bucket: this.bucket,
|
|
241
234
|
Key: key
|
|
242
235
|
});
|
|
243
236
|
await this.client.send(headCommand);
|
|
244
|
-
const deleteCommand = new
|
|
237
|
+
const deleteCommand = new _aws_sdk_client_s3.DeleteObjectCommand({
|
|
245
238
|
Bucket: this.bucket,
|
|
246
239
|
Key: key
|
|
247
240
|
});
|
|
@@ -255,7 +248,7 @@ var S3FilesApi = class {
|
|
|
255
248
|
const keysToDelete = [];
|
|
256
249
|
let continuationToken;
|
|
257
250
|
do {
|
|
258
|
-
const listCommand = new
|
|
251
|
+
const listCommand = new _aws_sdk_client_s3.ListObjectsV2Command({
|
|
259
252
|
Bucket: this.bucket,
|
|
260
253
|
Prefix: prefix,
|
|
261
254
|
ContinuationToken: continuationToken
|
|
@@ -268,7 +261,7 @@ var S3FilesApi = class {
|
|
|
268
261
|
} while (continuationToken);
|
|
269
262
|
if (keysToDelete.length === 0) return false;
|
|
270
263
|
for (const keyToDelete of keysToDelete) {
|
|
271
|
-
const deleteCommand = new
|
|
264
|
+
const deleteCommand = new _aws_sdk_client_s3.DeleteObjectCommand({
|
|
272
265
|
Bucket: this.bucket,
|
|
273
266
|
Key: keyToDelete
|
|
274
267
|
});
|
|
@@ -286,7 +279,7 @@ var S3FilesApi = class {
|
|
|
286
279
|
const sourceStats = await this.stats(source);
|
|
287
280
|
if (!sourceStats) return false;
|
|
288
281
|
if (sourceStats.kind === "file") {
|
|
289
|
-
const command = new
|
|
282
|
+
const command = new _aws_sdk_client_s3.CopyObjectCommand({
|
|
290
283
|
Bucket: this.bucket,
|
|
291
284
|
Key: targetKey,
|
|
292
285
|
CopySource: encodeURIComponent(`${this.bucket}/${sourceKey}`)
|
|
@@ -299,7 +292,7 @@ var S3FilesApi = class {
|
|
|
299
292
|
let continuationToken;
|
|
300
293
|
let copied = false;
|
|
301
294
|
do {
|
|
302
|
-
const listCommand = new
|
|
295
|
+
const listCommand = new _aws_sdk_client_s3.ListObjectsV2Command({
|
|
303
296
|
Bucket: this.bucket,
|
|
304
297
|
Prefix: sourcePrefix,
|
|
305
298
|
ContinuationToken: continuationToken
|
|
@@ -308,7 +301,7 @@ var S3FilesApi = class {
|
|
|
308
301
|
if (response.Contents) for (const obj of response.Contents) {
|
|
309
302
|
if (!obj.Key) continue;
|
|
310
303
|
const newKey = targetPrefix + obj.Key.substring(sourcePrefix.length);
|
|
311
|
-
const copyCommand = new
|
|
304
|
+
const copyCommand = new _aws_sdk_client_s3.CopyObjectCommand({
|
|
312
305
|
Bucket: this.bucket,
|
|
313
306
|
Key: newKey,
|
|
314
307
|
CopySource: encodeURIComponent(`${this.bucket}/${obj.Key}`)
|
|
@@ -321,6 +314,5 @@ var S3FilesApi = class {
|
|
|
321
314
|
return copied;
|
|
322
315
|
}
|
|
323
316
|
};
|
|
324
|
-
|
|
325
317
|
//#endregion
|
|
326
|
-
exports.S3FilesApi = S3FilesApi;
|
|
318
|
+
exports.S3FilesApi = S3FilesApi;
|
package/dist/esm/index.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CopyObjectCommand, CreateMultipartUploadCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, UploadPartCommand } from "@aws-sdk/client-s3";
|
|
2
2
|
import { basename, normalizePath } from "@statewalker/webrun-files";
|
|
3
|
-
|
|
4
3
|
//#region src/s3-files-api.ts
|
|
5
4
|
/** Default part size for multipart uploads: 5MB (S3 minimum) */
|
|
6
|
-
const DEFAULT_PART_SIZE =
|
|
5
|
+
const DEFAULT_PART_SIZE = 5242880;
|
|
7
6
|
/** Threshold for using multipart upload: 5MB */
|
|
8
|
-
const MULTIPART_THRESHOLD =
|
|
7
|
+
const MULTIPART_THRESHOLD = 5242880;
|
|
9
8
|
/**
|
|
10
9
|
* S3 filesystem implementation of FilesApi.
|
|
11
10
|
*
|
|
@@ -149,7 +148,7 @@ var S3FilesApi = class {
|
|
|
149
148
|
const command = new PutObjectCommand({
|
|
150
149
|
Bucket: this.bucket,
|
|
151
150
|
Key: dirKey,
|
|
152
|
-
Body: new Uint8Array(0),
|
|
151
|
+
Body: /* @__PURE__ */ new Uint8Array(0),
|
|
153
152
|
ContentLength: 0
|
|
154
153
|
});
|
|
155
154
|
await this.client.send(command);
|
|
@@ -175,8 +174,7 @@ var S3FilesApi = class {
|
|
|
175
174
|
yield {
|
|
176
175
|
kind: "directory",
|
|
177
176
|
name: basename(dirPath),
|
|
178
|
-
path: dirPath
|
|
179
|
-
lastModified: 0
|
|
177
|
+
path: dirPath
|
|
180
178
|
};
|
|
181
179
|
}
|
|
182
180
|
if (response.Contents) for (const obj of response.Contents) {
|
|
@@ -199,10 +197,7 @@ var S3FilesApi = class {
|
|
|
199
197
|
async stats(path) {
|
|
200
198
|
const normalized = normalizePath(path);
|
|
201
199
|
const key = this.resolveKey(path);
|
|
202
|
-
if (normalized === "/") return {
|
|
203
|
-
kind: "directory",
|
|
204
|
-
lastModified: 0
|
|
205
|
-
};
|
|
200
|
+
if (normalized === "/") return { kind: "directory" };
|
|
206
201
|
try {
|
|
207
202
|
const command = new HeadObjectCommand({
|
|
208
203
|
Bucket: this.bucket,
|
|
@@ -225,10 +220,7 @@ var S3FilesApi = class {
|
|
|
225
220
|
MaxKeys: 1
|
|
226
221
|
});
|
|
227
222
|
const listResponse = await this.client.send(listCommand);
|
|
228
|
-
if (listResponse.Contents && listResponse.Contents.length > 0 || listResponse.CommonPrefixes && listResponse.CommonPrefixes.length > 0) return {
|
|
229
|
-
kind: "directory",
|
|
230
|
-
lastModified: 0
|
|
231
|
-
};
|
|
223
|
+
if (listResponse.Contents && listResponse.Contents.length > 0 || listResponse.CommonPrefixes && listResponse.CommonPrefixes.length > 0) return { kind: "directory" };
|
|
232
224
|
}
|
|
233
225
|
async exists(path) {
|
|
234
226
|
return await this.stats(path) !== void 0;
|
|
@@ -321,6 +313,5 @@ var S3FilesApi = class {
|
|
|
321
313
|
return copied;
|
|
322
314
|
}
|
|
323
315
|
};
|
|
324
|
-
|
|
325
316
|
//#endregion
|
|
326
|
-
export { S3FilesApi };
|
|
317
|
+
export { S3FilesApi };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"s3-files-api.d.ts","sourceRoot":"","sources":["../src/s3-files-api.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAanD,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AASnC,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,MAAM,EAAE,QAAQ,CAAC;IAEjB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,UAAW,YAAW,QAAQ;IACzC,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAS;
|
|
1
|
+
{"version":3,"file":"s3-files-api.d.ts","sourceRoot":"","sources":["../src/s3-files-api.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAanD,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AASnC,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,MAAM,EAAE,QAAQ,CAAC;IAEjB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,UAAW,YAAW,QAAQ;IACzC,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAS;IAEzB,YAAY,OAAO,EAAE,iBAAiB,EAKrC;IAED,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,SAAS;IAWV,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC,CA6C1E;IAEK,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC,CAkHf;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYvC;IAEM,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC,CAiExE;IAEK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CA+CxD;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAG3C;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAkE3C;IAEK,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAI3D;IAEK,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAwD3D;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-files-s3",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "S3-backed FilesApi implementation for webrun-files",
|
|
@@ -29,21 +29,20 @@
|
|
|
29
29
|
"src"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@statewalker/webrun-files": "0.
|
|
32
|
+
"@statewalker/webrun-files": "0.9.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@aws-sdk/client-s3": "^3.
|
|
35
|
+
"@aws-sdk/client-s3": "^3.1032.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@aws-sdk/client-s3": "^3.
|
|
39
|
-
"@
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"@statewalker/webrun-files-tests": "1.0.0"
|
|
38
|
+
"@aws-sdk/client-s3": "^3.1110.0",
|
|
39
|
+
"@types/node": "^26.2.0",
|
|
40
|
+
"rimraf": "^6.1.3",
|
|
41
|
+
"rolldown": "^1.2.4",
|
|
42
|
+
"testcontainers": "^12.1.0",
|
|
43
|
+
"typescript": "^7.0.2",
|
|
44
|
+
"vitest": "^4.1.10",
|
|
45
|
+
"@statewalker/webrun-files-tests": "2.0.0"
|
|
47
46
|
},
|
|
48
47
|
"sideEffects": false,
|
|
49
48
|
"publishConfig": {
|
|
@@ -51,7 +50,8 @@
|
|
|
51
50
|
},
|
|
52
51
|
"scripts": {
|
|
53
52
|
"build": "rimraf dist && rolldown -c && tsc --emitDeclarationOnly --declaration",
|
|
54
|
-
"
|
|
55
|
-
"
|
|
53
|
+
"lint": "biome lint src tests",
|
|
54
|
+
"test": "vitest run --passWithNoTests",
|
|
55
|
+
"test:integration": "vitest run --config vitest.integration.config.ts"
|
|
56
56
|
}
|
|
57
57
|
}
|
package/src/s3-files-api.ts
CHANGED
|
@@ -305,11 +305,13 @@ export class S3FilesApi implements FilesApi {
|
|
|
305
305
|
|
|
306
306
|
const dirPath = this.keyToPath(dirKey);
|
|
307
307
|
|
|
308
|
+
// A common prefix IS the directory variant: it exists only as an
|
|
309
|
+
// artefact of key naming, so there is nothing to report about it
|
|
310
|
+
// beyond its being a directory.
|
|
308
311
|
yield {
|
|
309
312
|
kind: "directory",
|
|
310
313
|
name: basename(dirPath),
|
|
311
314
|
path: dirPath,
|
|
312
|
-
lastModified: 0,
|
|
313
315
|
};
|
|
314
316
|
}
|
|
315
317
|
}
|
|
@@ -319,6 +321,11 @@ export class S3FilesApi implements FilesApi {
|
|
|
319
321
|
if (!obj.Key) continue;
|
|
320
322
|
if (obj.Key === normalizedPrefix) continue;
|
|
321
323
|
if (`${obj.Key}/` === normalizedPrefix) continue;
|
|
324
|
+
// A key ending in "/" is the zero-byte marker `mkdir` writes to make
|
|
325
|
+
// an empty directory visible. It is NOT a file with `size: 0` — it
|
|
326
|
+
// is how this store spells a directory, and it surfaces through
|
|
327
|
+
// `CommonPrefixes` above. Every other key is a file, and a genuinely
|
|
328
|
+
// empty object is the file variant with `size: 0`.
|
|
322
329
|
if (obj.Key.endsWith("/")) continue;
|
|
323
330
|
|
|
324
331
|
const filePath = this.keyToPath(obj.Key);
|
|
@@ -342,10 +349,7 @@ export class S3FilesApi implements FilesApi {
|
|
|
342
349
|
const key = this.resolveKey(path);
|
|
343
350
|
|
|
344
351
|
if (normalized === "/") {
|
|
345
|
-
return {
|
|
346
|
-
kind: "directory",
|
|
347
|
-
lastModified: 0,
|
|
348
|
-
};
|
|
352
|
+
return { kind: "directory" };
|
|
349
353
|
}
|
|
350
354
|
|
|
351
355
|
// Try as file first
|
|
@@ -383,10 +387,7 @@ export class S3FilesApi implements FilesApi {
|
|
|
383
387
|
(listResponse.Contents && listResponse.Contents.length > 0) ||
|
|
384
388
|
(listResponse.CommonPrefixes && listResponse.CommonPrefixes.length > 0)
|
|
385
389
|
) {
|
|
386
|
-
return {
|
|
387
|
-
kind: "directory",
|
|
388
|
-
lastModified: 0,
|
|
389
|
-
};
|
|
390
|
+
return { kind: "directory" };
|
|
390
391
|
}
|
|
391
392
|
|
|
392
393
|
return undefined;
|