s3-test-server 2.0.0 → 2.1.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 +36 -12
- package/lib/index.d.ts +58 -0
- package/lib/index.js +144 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,18 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight Amazon S3 emulator for local development and testing.
|
|
4
4
|
|
|
5
|
-
Zero runtime dependencies
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- **AWS SDK for JavaScript v2**, SigV4 and legacy SigV2 (`signatureVersion: 's3'`)
|
|
12
|
-
- **Any other S3 client** that signs with Signature V2 or V4, header-based or
|
|
13
|
-
presigned, path-style or virtual-host-style
|
|
14
|
-
|
|
15
|
-
Data is stored on the filesystem (a temp directory by default), so it survives
|
|
16
|
-
restarts when you point it at a directory of your own.
|
|
5
|
+
Zero runtime dependencies and wire-level compatibility with old and new S3
|
|
6
|
+
clients: AWS SDK for JavaScript v3 (flexible checksums, `aws-chunked`
|
|
7
|
+
streaming, multipart), v2 (SigV4 and legacy SigV2), and any other client that
|
|
8
|
+
signs with Signature V2/V4, header-based or presigned, path-style or
|
|
9
|
+
virtual-host-style. Objects are stored on the filesystem and persist across
|
|
10
|
+
restarts when you point `directory` at a folder of your own.
|
|
17
11
|
|
|
18
12
|
## Quick start
|
|
19
13
|
|
|
@@ -314,6 +308,33 @@ server.on('event', (notification) => {
|
|
|
314
308
|
Emitted for `ObjectCreated:Put`, `ObjectCreated:Copy`, `ObjectCreated:Post`,
|
|
315
309
|
`ObjectCreated:CompleteMultipartUpload` and `ObjectRemoved:Delete`.
|
|
316
310
|
|
|
311
|
+
## Test helpers
|
|
312
|
+
|
|
313
|
+
Three helpers make assertions and fixtures easy without building an S3
|
|
314
|
+
client; they talk to the store directly:
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
// seed fixtures (bucket is created if missing; no events are emitted)
|
|
318
|
+
await server.putObjects('reports', [
|
|
319
|
+
{ key: 'public/a.csv', body: 'a,b,c', contentType: 'text/csv' },
|
|
320
|
+
{ key: 'internal/b.csv', body: Buffer.from('d,e,f') },
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
// inspect bucket contents in assertions
|
|
324
|
+
const all = await server.getObjects('reports');
|
|
325
|
+
const csvs = await server.getObjects('reports', {
|
|
326
|
+
prefix: 'public/',
|
|
327
|
+
bodies: true, // each summary gets a `body` Buffer
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// wait for background uploads from the code under test;
|
|
331
|
+
// multipart uploads only count once completed
|
|
332
|
+
const files = await server.waitForObjects('reports', 2, { timeout: 5000 });
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
After `run()` resolves, `server.hostPort` (`"localhost:4568"`) and
|
|
336
|
+
`server.endpoint` (`"http://localhost:4568"`) are also set on the instance.
|
|
337
|
+
|
|
317
338
|
## Using as middleware
|
|
318
339
|
|
|
319
340
|
```js
|
|
@@ -332,6 +353,9 @@ http.createServer(s3.callback()).listen(4568);
|
|
|
332
353
|
- `callback()` / `getMiddleware()` returns a request handler
|
|
333
354
|
- `configureBuckets()` provisions `options.configureBuckets` without the API
|
|
334
355
|
- `reset()` deletes all buckets and configurations
|
|
356
|
+
- `getObjects(bucket, { prefix, bodies })` lists completed objects
|
|
357
|
+
- `waitForObjects(bucket, count, { prefix, timeout })` awaits uploads
|
|
358
|
+
- `putObjects(bucket, objects)` seeds fixtures without the HTTP API
|
|
335
359
|
|
|
336
360
|
TypeScript definitions ship with the package (`lib/index.d.ts`).
|
|
337
361
|
|
package/lib/index.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ declare class S3TestServer extends EventEmitter {
|
|
|
11
11
|
/** The underlying HTTP(S) server, available after `run()` resolves. */
|
|
12
12
|
httpServer: HttpServer | null;
|
|
13
13
|
|
|
14
|
+
/** `host:port` the server listens on; set once `run()` resolves. */
|
|
15
|
+
hostPort: string | null;
|
|
16
|
+
|
|
17
|
+
/** Full endpoint URL (e.g. `http://localhost:4568`); set once `run()` resolves. */
|
|
18
|
+
endpoint: string | null;
|
|
19
|
+
|
|
14
20
|
/** Starts the server. */
|
|
15
21
|
run(): Promise<S3TestServer.RunAddress>;
|
|
16
22
|
run(
|
|
@@ -32,6 +38,34 @@ declare class S3TestServer extends EventEmitter {
|
|
|
32
38
|
/** Deletes all buckets, objects and configurations in the store. */
|
|
33
39
|
reset(): Promise<void>;
|
|
34
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Lists the completed objects in a bucket, sorted by key. With
|
|
43
|
+
* `bodies: true`, each summary also carries its content as a Buffer.
|
|
44
|
+
*/
|
|
45
|
+
getObjects(
|
|
46
|
+
bucket: string,
|
|
47
|
+
options?: { prefix?: string; bodies?: boolean }
|
|
48
|
+
): Promise<S3TestServer.ObjectSummary[]>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves with the bucket's objects once at least `count` exist
|
|
52
|
+
* (optionally under `prefix`); rejects after `timeout` ms (default 10000).
|
|
53
|
+
*/
|
|
54
|
+
waitForObjects(
|
|
55
|
+
bucket: string,
|
|
56
|
+
count: number,
|
|
57
|
+
options?: { prefix?: string; timeout?: number }
|
|
58
|
+
): Promise<S3TestServer.ObjectSummary[]>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Seeds objects directly into the store (the bucket is created when
|
|
62
|
+
* missing). No S3 event notifications are emitted for seeded objects.
|
|
63
|
+
*/
|
|
64
|
+
putObjects(
|
|
65
|
+
bucket: string,
|
|
66
|
+
objects: S3TestServer.SeedObject[]
|
|
67
|
+
): Promise<S3TestServer.ObjectSummary[]>;
|
|
68
|
+
|
|
35
69
|
on(
|
|
36
70
|
event: 'event',
|
|
37
71
|
listener: (notification: S3TestServer.S3EventNotification) => void
|
|
@@ -139,11 +173,33 @@ declare namespace S3TestServer {
|
|
|
139
173
|
>
|
|
140
174
|
>;
|
|
141
175
|
|
|
176
|
+
interface ObjectSummary {
|
|
177
|
+
key: string;
|
|
178
|
+
size: number;
|
|
179
|
+
/** Hex ETag without quotes. */
|
|
180
|
+
etag: string;
|
|
181
|
+
lastModified: Date;
|
|
182
|
+
contentType?: string;
|
|
183
|
+
/** User metadata (`x-amz-meta-*`), with the prefix stripped. */
|
|
184
|
+
metadata: Record<string, string>;
|
|
185
|
+
/** Present when requested with `bodies: true`. */
|
|
186
|
+
body?: Buffer;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface SeedObject {
|
|
190
|
+
key: string;
|
|
191
|
+
body: string | Buffer;
|
|
192
|
+
contentType?: string;
|
|
193
|
+
metadata?: Record<string, string>;
|
|
194
|
+
}
|
|
195
|
+
|
|
142
196
|
interface RunAddress {
|
|
143
197
|
/** Bound address, as reported by `net.Server#address()`. */
|
|
144
198
|
address: string;
|
|
145
199
|
family: string;
|
|
146
200
|
port: number;
|
|
201
|
+
/** `host:port` the server listens on. */
|
|
202
|
+
hostPort: string;
|
|
147
203
|
/** `"http"` or `"https"`. */
|
|
148
204
|
protocol: string;
|
|
149
205
|
/** Convenience endpoint URL, e.g. `"http://localhost:4568"`. */
|
|
@@ -198,6 +254,8 @@ declare namespace S3TestServer {
|
|
|
198
254
|
|
|
199
255
|
export {
|
|
200
256
|
S3TestServer,
|
|
257
|
+
ObjectSummary,
|
|
258
|
+
SeedObject,
|
|
201
259
|
CannedBucketAcl,
|
|
202
260
|
BucketConfiguration,
|
|
203
261
|
S3TestServerOptions,
|
package/lib/index.js
CHANGED
|
@@ -52,6 +52,8 @@ class S3TestServer extends EventEmitter {
|
|
|
52
52
|
};
|
|
53
53
|
this.httpServer = null;
|
|
54
54
|
this.store = null;
|
|
55
|
+
this.hostPort = null;
|
|
56
|
+
this.endpoint = null;
|
|
55
57
|
this._middleware = null;
|
|
56
58
|
this._sequencer = 0;
|
|
57
59
|
}
|
|
@@ -110,13 +112,14 @@ class S3TestServer extends EventEmitter {
|
|
|
110
112
|
});
|
|
111
113
|
const address = this.httpServer.address();
|
|
112
114
|
const protocol = this.options.key && this.options.cert ? 'https' : 'http';
|
|
113
|
-
this.
|
|
114
|
-
|
|
115
|
-
);
|
|
115
|
+
this.hostPort = `${this.options.address}:${address.port}`;
|
|
116
|
+
this.endpoint = `${protocol}://${this.hostPort}`;
|
|
117
|
+
this.logger.info(`S3 test server listening on ${this.endpoint}`);
|
|
116
118
|
return {
|
|
117
119
|
...address,
|
|
118
120
|
protocol,
|
|
119
|
-
|
|
121
|
+
hostPort: this.hostPort,
|
|
122
|
+
endpoint: this.endpoint,
|
|
120
123
|
};
|
|
121
124
|
})();
|
|
122
125
|
if (typeof callback === 'function') {
|
|
@@ -217,6 +220,143 @@ class S3TestServer extends EventEmitter {
|
|
|
217
220
|
await this.store.reset();
|
|
218
221
|
}
|
|
219
222
|
|
|
223
|
+
// ---------------------------------------------------------------- helpers
|
|
224
|
+
|
|
225
|
+
_toSummary(meta) {
|
|
226
|
+
return {
|
|
227
|
+
key: meta.key,
|
|
228
|
+
size: meta.size,
|
|
229
|
+
etag: meta.etag,
|
|
230
|
+
lastModified: new Date(meta.lastModified),
|
|
231
|
+
contentType: meta.contentType,
|
|
232
|
+
metadata: { ...(meta.userMetadata || {}) },
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Lists the completed objects in a bucket (multipart parts in flight are
|
|
238
|
+
* never included), sorted by key. With `bodies: true`, each entry also
|
|
239
|
+
* carries its content as a Buffer.
|
|
240
|
+
*/
|
|
241
|
+
async getObjects(bucket, { prefix = '', bodies = false } = {}) {
|
|
242
|
+
this._ensureStore();
|
|
243
|
+
if (!(await this.store.getBucket(bucket))) {
|
|
244
|
+
throw new S3Error('NoSuchBucket', 'The specified bucket does not exist', {
|
|
245
|
+
BucketName: bucket,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const metas = (await this.store.listObjects(bucket)).filter(
|
|
249
|
+
(meta) => !prefix || meta.key.startsWith(prefix)
|
|
250
|
+
);
|
|
251
|
+
const summaries = metas.map((meta) => this._toSummary(meta));
|
|
252
|
+
if (bodies) {
|
|
253
|
+
for (const summary of summaries) {
|
|
254
|
+
summary.body = await this._readObject(bucket, summary.key);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return summaries;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_readObject(bucket, key) {
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
const chunks = [];
|
|
263
|
+
this.store
|
|
264
|
+
.createObjectReadStream(bucket, key)
|
|
265
|
+
.on('data', (chunk) => chunks.push(chunk))
|
|
266
|
+
.on('end', () => resolve(Buffer.concat(chunks)))
|
|
267
|
+
.on('error', reject);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Resolves with the bucket's objects once at least `count` of them exist
|
|
273
|
+
* (optionally under `prefix`). Useful when the code under test uploads in
|
|
274
|
+
* the background. A bucket that does not exist yet simply counts as empty.
|
|
275
|
+
*/
|
|
276
|
+
async waitForObjects(bucket, count, { prefix = '', timeout = 10000 } = {}) {
|
|
277
|
+
this._ensureStore();
|
|
278
|
+
const matching = async () => {
|
|
279
|
+
if (!(await this.store.getBucket(bucket))) return null;
|
|
280
|
+
const metas = (await this.store.listObjects(bucket)).filter(
|
|
281
|
+
(meta) => !prefix || meta.key.startsWith(prefix)
|
|
282
|
+
);
|
|
283
|
+
return metas.length >= count
|
|
284
|
+
? metas.map((meta) => this._toSummary(meta))
|
|
285
|
+
: null;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const found = await matching();
|
|
289
|
+
if (found) return found;
|
|
290
|
+
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
let interval;
|
|
293
|
+
let timer;
|
|
294
|
+
const check = async () => {
|
|
295
|
+
const objects = await matching().catch(() => null);
|
|
296
|
+
if (objects) {
|
|
297
|
+
cleanup();
|
|
298
|
+
resolve(objects);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
const cleanup = () => {
|
|
302
|
+
clearInterval(interval);
|
|
303
|
+
clearTimeout(timer);
|
|
304
|
+
this.removeListener('event', check);
|
|
305
|
+
};
|
|
306
|
+
this.on('event', check);
|
|
307
|
+
// Poll as well, so out-of-band writes (e.g. putObjects) are seen too.
|
|
308
|
+
interval = setInterval(check, 150);
|
|
309
|
+
timer = setTimeout(() => {
|
|
310
|
+
cleanup();
|
|
311
|
+
reject(
|
|
312
|
+
new Error(
|
|
313
|
+
`Timed out after ${timeout}ms waiting for ${count} object(s) in "${bucket}"` +
|
|
314
|
+
(prefix ? ` under "${prefix}"` : '')
|
|
315
|
+
)
|
|
316
|
+
);
|
|
317
|
+
}, timeout);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Seeds objects directly into the store, bypassing the HTTP API. The
|
|
323
|
+
* bucket is created when missing. Each entry is
|
|
324
|
+
* `{ key, body, contentType?, metadata? }` with a string or Buffer body.
|
|
325
|
+
* No S3 event notifications are emitted for seeded objects.
|
|
326
|
+
*/
|
|
327
|
+
async putObjects(bucket, objects) {
|
|
328
|
+
this._ensureStore();
|
|
329
|
+
await this.store.initialize();
|
|
330
|
+
const { validateBucketName } = require('./utils');
|
|
331
|
+
validateBucketName(bucket);
|
|
332
|
+
await this.store.createBucket(bucket);
|
|
333
|
+
const { Readable } = require('stream');
|
|
334
|
+
const results = [];
|
|
335
|
+
for (const object of objects) {
|
|
336
|
+
if (!object || typeof object.key !== 'string') {
|
|
337
|
+
throw new TypeError('putObjects entries need a string `key`');
|
|
338
|
+
}
|
|
339
|
+
const body = Buffer.isBuffer(object.body)
|
|
340
|
+
? object.body
|
|
341
|
+
: Buffer.from(object.body ?? '');
|
|
342
|
+
const staged = await this.store.stageObject(
|
|
343
|
+
bucket,
|
|
344
|
+
Readable.from([body]),
|
|
345
|
+
['CRC64NVME']
|
|
346
|
+
);
|
|
347
|
+
const meta = await staged.commit(object.key, {
|
|
348
|
+
contentType: object.contentType || 'binary/octet-stream',
|
|
349
|
+
userMetadata: { ...(object.metadata || {}) },
|
|
350
|
+
tagging: {},
|
|
351
|
+
checksums: staged.digests.checksums,
|
|
352
|
+
checksumAlgorithm: 'CRC64NVME',
|
|
353
|
+
checksumType: 'FULL_OBJECT',
|
|
354
|
+
});
|
|
355
|
+
results.push(this._toSummary(meta));
|
|
356
|
+
}
|
|
357
|
+
return results;
|
|
358
|
+
}
|
|
359
|
+
|
|
220
360
|
_emitEvent(eventName, bucket, objectMeta) {
|
|
221
361
|
if (!objectMeta) return;
|
|
222
362
|
this._sequencer += 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s3-test-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A modern, lightweight Amazon S3 emulator for local development and testing. Compatible with AWS SDK v2 and v3, Signature V2/V4, path-style and virtual-host-style addressing, streaming (aws-chunked) uploads and flexible checksums.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Israel Alagbe <israelalagbe53@gmail.com>",
|