@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 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="/")
@@ -1,11 +1,11 @@
1
- let __aws_sdk_client_s3 = require("@aws-sdk/client-s3");
2
- let __statewalker_webrun_files = require("@statewalker/webrun-files");
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 = 5 * 1024 * 1024;
6
+ const DEFAULT_PART_SIZE = 5242880;
7
7
  /** Threshold for using multipart upload: 5MB */
8
- const MULTIPART_THRESHOLD = 5 * 1024 * 1024;
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, __statewalker_webrun_files.normalizePath)(path);
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 __aws_sdk_client_s3.GetObjectCommand({
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 __aws_sdk_client_s3.PutObjectCommand({
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 __aws_sdk_client_s3.CreateMultipartUploadCommand({
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 __aws_sdk_client_s3.UploadPartCommand({
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 __aws_sdk_client_s3.CompleteMultipartUploadCommand({
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 __aws_sdk_client_s3.AbortMultipartUploadCommand({
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 __aws_sdk_client_s3.PutObjectCommand({
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 __aws_sdk_client_s3.ListObjectsV2Command({
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, __statewalker_webrun_files.basename)(dirPath),
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, __statewalker_webrun_files.basename)(filePath),
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, __statewalker_webrun_files.normalizePath)(path);
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 __aws_sdk_client_s3.HeadObjectCommand({
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 __aws_sdk_client_s3.ListObjectsV2Command({
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 __aws_sdk_client_s3.HeadObjectCommand({
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 __aws_sdk_client_s3.DeleteObjectCommand({
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 __aws_sdk_client_s3.ListObjectsV2Command({
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 __aws_sdk_client_s3.DeleteObjectCommand({
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 __aws_sdk_client_s3.CopyObjectCommand({
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 __aws_sdk_client_s3.ListObjectsV2Command({
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 __aws_sdk_client_s3.CopyObjectCommand({
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 * 1024 * 1024;
5
+ const DEFAULT_PART_SIZE = 5242880;
7
6
  /** Threshold for using multipart upload: 5MB */
8
- const MULTIPART_THRESHOLD = 5 * 1024 * 1024;
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;gBAEb,OAAO,EAAE,iBAAiB;IAOtC,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,SAAS;IAWV,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC;IA+CrE,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC;IAoHV,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcjC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,QAAQ,CAAC;IA4DnE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAuDnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKtC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoEtC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAMtD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAyD7D"}
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.7.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.7.0"
32
+ "@statewalker/webrun-files": "0.9.0"
33
33
  },
34
34
  "peerDependencies": {
35
- "@aws-sdk/client-s3": "^3.0.0"
35
+ "@aws-sdk/client-s3": "^3.1032.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@aws-sdk/client-s3": "^3.0.0",
39
- "@testcontainers/minio": "^10.0.0",
40
- "@types/node": "^24.10.1",
41
- "rimraf": "^6.1.2",
42
- "rolldown": "^1.0.0-beta.53",
43
- "testcontainers": "^10.0.0",
44
- "typescript": "^5.6.0",
45
- "vitest": "^4.0.12",
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
- "test": "vitest run",
55
- "lint": "biome lint src tests"
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
  }
@@ -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;