s3-test-server 0.0.0-stage → 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Israel Alagbe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,232 @@
1
- # Temporary Holding Version
1
+ # AWS S3 Local Emulator Test Server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A lightweight Amazon S3 emulator for local development and testing.
4
+
5
+ Zero runtime dependencies, a single small package, and wire-level
6
+ compatibility with both old and new S3 clients:
7
+
8
+ - **AWS SDK for JavaScript v3**, including flexible checksums
9
+ (CRC32, CRC32C, CRC64NVME, SHA1, SHA256), `aws-chunked` streaming uploads
10
+ with trailers, and full-object / composite multipart checksums
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.
17
+
18
+ ## Quick start
19
+
20
+ ```bash
21
+ npm install --save-dev s3-test-server
22
+ ```
23
+
24
+ ```js
25
+ const S3TestServer = require('s3-test-server');
26
+
27
+ const server = new S3TestServer({
28
+ port: 4568,
29
+ configureBuckets: [{ name: 'my-bucket' }],
30
+ });
31
+
32
+ const { endpoint } = await server.run(); // http://localhost:4568
33
+ // ... run your tests ...
34
+ await server.close();
35
+ ```
36
+
37
+ Point any S3 client at it:
38
+
39
+ ```js
40
+ import { S3Client } from '@aws-sdk/client-s3';
41
+
42
+ const client = new S3Client({
43
+ endpoint: 'http://localhost:4568',
44
+ region: 'us-east-1',
45
+ credentials: { accessKeyId: 'S3TEST', secretAccessKey: 'S3TEST' },
46
+ forcePathStyle: true,
47
+ });
48
+ ```
49
+
50
+ Or from the command line:
51
+
52
+ ```bash
53
+ npx s3-test-server -d ./s3-data -p 4568 --configure-bucket my-bucket
54
+ ```
55
+
56
+ Run `s3-test-server --help` for all flags.
57
+
58
+ ## Why not just mock?
59
+
60
+ Mocking the S3 client tests your assumptions about S3, not S3 itself. Stubs
61
+ skip request signing, checksum validation, multipart ETag rules, pagination
62
+ contracts and error shapes, so code that passes mocked tests can still break
63
+ against the real service, and every SDK upgrade silently invalidates the
64
+ stubs.
65
+
66
+ Before, with a mocked client, nothing below the stub actually runs:
67
+
68
+ ```js
69
+ jest.mock('@aws-sdk/client-s3');
70
+ S3Client.prototype.send = jest.fn().mockResolvedValue({ ETag: '"abc123"' });
71
+
72
+ test('uploads a report', async () => {
73
+ await uploadReport('march.csv', csv);
74
+ expect(S3Client.prototype.send).toHaveBeenCalled(); // proves nothing about S3
75
+ });
76
+ ```
77
+
78
+ After, the same test against `s3-test-server`. Your production code runs
79
+ unmodified and every byte goes over HTTP (`aws-s3.local` is any hostname you
80
+ map to 127.0.0.1):
81
+
82
+ ```js
83
+ const server = new S3TestServer({
84
+ address: 'aws-s3.local',
85
+ configureBuckets: [{ name: 'reports' }],
86
+ });
87
+
88
+ beforeAll(() => server.run()); // http://aws-s3.local:4568
89
+ afterAll(() => server.close());
90
+
91
+ test('uploads a report and returns its URL', async () => {
92
+ const url = await uploadReport('march.csv', csv); // real code, real wire
93
+
94
+ // the full object URL is real, not invented by a stub
95
+ expect(url).toBe('http://aws-s3.local:4568/reports/march.csv');
96
+
97
+ // and it resolves: read the object back like any HTTP resource
98
+ const saved = await fetch(url);
99
+ expect(await saved.text()).toBe(csv.toString());
100
+ });
101
+ ```
102
+
103
+ If `uploadReport` signs incorrectly, streams a corrupt body or mishandles
104
+ multipart thresholds, this test fails, exactly as it would against AWS.
105
+
106
+ ## Options
107
+
108
+ All options are optional. `new S3TestServer()` starts an HTTP server on
109
+ `localhost:4568` backed by a temporary directory.
110
+
111
+ ### Server
112
+
113
+ | Option | Type | Default | Description |
114
+ | -------------- | ------------------ | ------------- | ------------------------------------------------- |
115
+ | `address` | `string` | `'localhost'` | Hostname or IP to bind to. |
116
+ | `port` | `number` | `4568` | Port to listen on. Use `0` for an ephemeral port. |
117
+ | `key` / `cert` | `string \| Buffer` | none | TLS key and certificate. Provide both for HTTPS. |
118
+ | `silent` | `boolean` | `false` | Suppress log output. |
119
+
120
+ ### Storage
121
+
122
+ | Option | Type | Default | Description |
123
+ | ------------------ | ----------------------- | -------- | ------------------------------------------------------------------------------------------ |
124
+ | `directory` | `string` | temp dir | Where buckets and objects are stored. Data persists across restarts. |
125
+ | `resetOnStart` | `boolean` | `false` | Clear the data directory on every start, so a persistent `directory` begins empty. |
126
+ | `resetOnClose` | `boolean` | `false` | Delete all bucket data when `close()` is called. |
127
+ | `configureBuckets` | `BucketConfiguration[]` | `[]` | Buckets to create at startup, each with optional raw XML configs (CORS, website, tagging). |
128
+
129
+ ### Credentials and addressing
130
+
131
+ | Option | Type | Default | Description |
132
+ | --------------------------- | --------- | ----------------- | --------------------------------------------------------------------------------------- |
133
+ | `accessKeyId` | `string` | `'S3TEST'` | Access key clients must sign with. |
134
+ | `secretAccessKey` | `string` | `'S3TEST'` | Matching secret key. |
135
+ | `region` | `string` | `'us-east-1'` | Region reported in responses and event records. |
136
+ | `vhostBuckets` | `boolean` | `true` | Virtual-host-style addressing (`bucket.localhost`, `bucket.s3.<region>.amazonaws.com`). |
137
+ | `serviceEndpoint` | `string` | `'amazonaws.com'` | Root domain recognized for virtual-host-style access. |
138
+ | `allowMismatchedSignatures` | `boolean` | `false` | Accept well-formed but incorrect signatures. |
139
+
140
+ Anonymous (unsigned) requests are accepted, as on a public bucket, which is
141
+ handy for plain `fetch`/`curl` in tests. Signed requests are verified against
142
+ `accessKeyId`/`secretAccessKey`.
143
+
144
+ ## Supported operations
145
+
146
+ **Service / buckets**: ListBuckets, Create/Delete/HeadBucket,
147
+ GetBucketLocation, ListObjects (v1), ListObjectsV2, ListObjectVersions
148
+ (unversioned mapping), Get/Put/DeleteBucketCors, Get/Put/DeleteBucketWebsite,
149
+ Get/Put/DeleteBucketTagging, GetBucketVersioning, GetBucketEncryption,
150
+ GetBucketNotification/RequestPayment/Logging/Accelerate, ACLs (dummy),
151
+ ListMultipartUploads.
152
+
153
+ **Objects**: Put/Get/Head/DeleteObject, DeleteObjects, CopyObject
154
+ (COPY/REPLACE directives, copy conditions), POST form uploads
155
+ (`multipart/form-data`, `${filename}` substitution, `success_action_*`),
156
+ Get/Put/DeleteObjectTagging, GetObjectAttributes, object ACLs (dummy),
157
+ Range requests, conditional reads (`If-Match`, `If-None-Match`,
158
+ `If-(Un)Modified-Since`) and **conditional writes** (`If-None-Match: *`,
159
+ `If-Match`), `response-*` header overrides, `Content-MD5` and
160
+ `x-amz-checksum-*` validation (headers or `aws-chunked` trailers).
161
+
162
+ **Multipart**: Create/Abort/CompleteMultipartUpload, UploadPart,
163
+ UploadPartCopy (with `x-amz-copy-source-range`), ListParts, correct
164
+ multipart ETags (`md5-of-md5s-N`), composite and full-object checksums.
165
+
166
+ **Static website hosting**: index/error documents, folder redirects,
167
+ `x-amz-website-redirect-location`, `RedirectAllRequestsTo`; served for
168
+ anonymous GET/HEAD requests on buckets with a website configuration.
169
+
170
+ **CORS**: preflight (`OPTIONS`) evaluation and response headers, driven by
171
+ the bucket's CORS configuration.
172
+
173
+ Not implemented: bucket versioning (`PutBucketVersioning` with `Enabled`
174
+ returns `NotImplemented`), object locking, lifecycle execution, bucket
175
+ policies/IAM. Per-chunk signatures inside `aws-chunked` bodies are not
176
+ re-verified (the seed signature is).
177
+
178
+ ## Events
179
+
180
+ The server is an `EventEmitter` and emits S3-style notifications:
181
+
182
+ ```js
183
+ server.on('event', (notification) => {
184
+ const [record] = notification.Records;
185
+ console.log(record.eventName, record.s3.object.key);
186
+ });
187
+ ```
188
+
189
+ Emitted for `ObjectCreated:Put`, `ObjectCreated:Copy`, `ObjectCreated:Post`,
190
+ `ObjectCreated:CompleteMultipartUpload` and `ObjectRemoved:Delete`.
191
+
192
+ ## Using as middleware
193
+
194
+ ```js
195
+ const http = require('http');
196
+ const S3TestServer = require('s3-test-server');
197
+
198
+ const s3 = new S3TestServer({ silent: true });
199
+ http.createServer(s3.callback()).listen(4568);
200
+ // also works as an Express/Connect handler
201
+ ```
202
+
203
+ ## Programmatic API
204
+
205
+ - `run([callback])` returns `Promise<{ address, port, protocol, endpoint }>`
206
+ - `close([callback])` returns `Promise<void>`
207
+ - `callback()` / `getMiddleware()` returns a request handler
208
+ - `configureBuckets()` provisions `options.configureBuckets` without the API
209
+ - `reset()` deletes all buckets and configurations
210
+
211
+ TypeScript definitions ship with the package (`lib/index.d.ts`).
212
+
213
+ ## HTTPS
214
+
215
+ ```js
216
+ const fs = require('fs');
217
+ const server = new S3TestServer({
218
+ key: fs.readFileSync('./key.pem'),
219
+ cert: fs.readFileSync('./cert.pem'),
220
+ });
221
+ ```
222
+
223
+ With a self-signed certificate, configure your client to accept it (for the
224
+ AWS SDKs, a custom HTTPS agent with `rejectUnauthorized: false`).
225
+
226
+ ## Requirements
227
+
228
+ Node.js >= 20.
229
+
230
+ ## License
231
+
232
+ MIT
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ require('../lib/cli').main();
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ // CRC tables are built lazily. Implemented locally (rather than via
6
+ // zlib.crc32) so the package runs on all Node.js 20.x releases.
7
+
8
+ function makeCrc32Table(poly) {
9
+ const table = new Uint32Array(256);
10
+ for (let n = 0; n < 256; n++) {
11
+ let c = n;
12
+ for (let k = 0; k < 8; k++) {
13
+ c = c & 1 ? (c >>> 1) ^ poly : c >>> 1;
14
+ }
15
+ table[n] = c >>> 0;
16
+ }
17
+ return table;
18
+ }
19
+
20
+ let crc32Table;
21
+ let crc32cTable;
22
+ let crc64Table;
23
+
24
+ function makeCrc64Table(poly) {
25
+ const table = new BigUint64Array(256);
26
+ for (let n = 0n; n < 256n; n++) {
27
+ let c = n;
28
+ for (let k = 0; k < 8; k++) {
29
+ c = c & 1n ? (c >> 1n) ^ poly : c >> 1n;
30
+ }
31
+ table[Number(n)] = c;
32
+ }
33
+ return table;
34
+ }
35
+
36
+ class Crc32 {
37
+ constructor(castagnoli = false) {
38
+ if (castagnoli) {
39
+ crc32cTable = crc32cTable || makeCrc32Table(0x82f63b78);
40
+ this.table = crc32cTable;
41
+ } else {
42
+ crc32Table = crc32Table || makeCrc32Table(0xedb88320);
43
+ this.table = crc32Table;
44
+ }
45
+ this.crc = 0xffffffff;
46
+ }
47
+
48
+ update(buf) {
49
+ let crc = this.crc;
50
+ const table = this.table;
51
+ for (let i = 0; i < buf.length; i++) {
52
+ crc = table[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
53
+ }
54
+ this.crc = crc >>> 0;
55
+ }
56
+
57
+ digest() {
58
+ const out = Buffer.alloc(4);
59
+ out.writeUInt32BE((this.crc ^ 0xffffffff) >>> 0);
60
+ return out;
61
+ }
62
+ }
63
+
64
+ // CRC-64/NVME: reflected polynomial of 0xAD93D23594C93659.
65
+ const CRC64_POLY = 0x9a6c9329ac4bc9b5n;
66
+ const CRC64_MASK = 0xffffffffffffffffn;
67
+
68
+ class Crc64Nvme {
69
+ constructor() {
70
+ crc64Table = crc64Table || makeCrc64Table(CRC64_POLY);
71
+ this.crc = CRC64_MASK;
72
+ }
73
+
74
+ update(buf) {
75
+ let crc = this.crc;
76
+ const table = crc64Table;
77
+ for (let i = 0; i < buf.length; i++) {
78
+ crc = table[Number((crc ^ BigInt(buf[i])) & 0xffn)] ^ (crc >> 8n);
79
+ }
80
+ this.crc = crc;
81
+ }
82
+
83
+ digest() {
84
+ const out = Buffer.alloc(8);
85
+ out.writeBigUInt64BE((this.crc ^ CRC64_MASK) & CRC64_MASK);
86
+ return out;
87
+ }
88
+ }
89
+
90
+ class NodeHash {
91
+ constructor(algorithm) {
92
+ this.hash = crypto.createHash(algorithm);
93
+ }
94
+
95
+ update(buf) {
96
+ this.hash.update(buf);
97
+ }
98
+
99
+ digest() {
100
+ return this.hash.digest();
101
+ }
102
+ }
103
+
104
+ const CHECKSUM_ALGORITHMS = ['CRC32', 'CRC32C', 'CRC64NVME', 'SHA1', 'SHA256'];
105
+
106
+ function createChecksummer(algorithm) {
107
+ switch (algorithm) {
108
+ case 'CRC32':
109
+ return new Crc32(false);
110
+ case 'CRC32C':
111
+ return new Crc32(true);
112
+ case 'CRC64NVME':
113
+ return new Crc64Nvme();
114
+ case 'SHA1':
115
+ return new NodeHash('sha1');
116
+ case 'SHA256':
117
+ return new NodeHash('sha256');
118
+ default:
119
+ throw new Error(`Unsupported checksum algorithm: ${algorithm}`);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Computes MD5 + SHA256 (always) and any requested flexible checksums over a
125
+ * stream of buffers.
126
+ */
127
+ class DigestCalculator {
128
+ constructor(algorithms = []) {
129
+ this.md5 = crypto.createHash('md5');
130
+ this.sha256 = crypto.createHash('sha256');
131
+ this.size = 0;
132
+ this.checksummers = {};
133
+ for (const algorithm of algorithms) {
134
+ if (!this.checksummers[algorithm]) {
135
+ this.checksummers[algorithm] = createChecksummer(algorithm);
136
+ }
137
+ }
138
+ }
139
+
140
+ update(buf) {
141
+ this.size += buf.length;
142
+ this.md5.update(buf);
143
+ this.sha256.update(buf);
144
+ for (const checksummer of Object.values(this.checksummers)) {
145
+ checksummer.update(buf);
146
+ }
147
+ }
148
+
149
+ digest() {
150
+ const md5 = this.md5.digest();
151
+ const checksums = {};
152
+ for (const [algorithm, checksummer] of Object.entries(this.checksummers)) {
153
+ checksums[algorithm] = checksummer.digest().toString('base64');
154
+ }
155
+ return {
156
+ size: this.size,
157
+ md5Hex: md5.toString('hex'),
158
+ md5Base64: md5.toString('base64'),
159
+ sha256Hex: this.sha256.digest('hex'),
160
+ checksums,
161
+ };
162
+ }
163
+ }
164
+
165
+ function checksumOf(algorithm, buf) {
166
+ const checksummer = createChecksummer(algorithm);
167
+ checksummer.update(buf);
168
+ return checksummer.digest().toString('base64');
169
+ }
170
+
171
+ module.exports = {
172
+ CHECKSUM_ALGORITHMS,
173
+ DigestCalculator,
174
+ createChecksummer,
175
+ checksumOf,
176
+ };
package/lib/chunked.js ADDED
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ const { Transform } = require('stream');
4
+
5
+ const MAX_HEADER_LENGTH = 64 * 1024;
6
+
7
+ /**
8
+ * Decodes the `aws-chunked` content encoding used by SigV4 streaming uploads:
9
+ *
10
+ * <hex-size>[;chunk-signature=...]\r\n<data>\r\n ... 0[;chunk-signature=...]\r\n
11
+ * [trailer-name:value\r\n ...]\r\n
12
+ *
13
+ * Emits the raw payload bytes and collects any trailing checksum headers in
14
+ * `this.trailers` (lowercased names), available once the stream ends.
15
+ * Chunk signatures are not themselves verified; the seed signature in the
16
+ * Authorization header has already been checked by then.
17
+ */
18
+ class AwsChunkedDecoder extends Transform {
19
+ constructor() {
20
+ super();
21
+ this.buffer = Buffer.alloc(0);
22
+ this.state = 'size';
23
+ this.remaining = 0;
24
+ this.trailers = {};
25
+ }
26
+
27
+ _transform(chunk, _encoding, callback) {
28
+ this.buffer =
29
+ this.buffer.length === 0 ? chunk : Buffer.concat([this.buffer, chunk]);
30
+ try {
31
+ this._consume();
32
+ callback();
33
+ } catch (err) {
34
+ callback(err);
35
+ }
36
+ }
37
+
38
+ _flush(callback) {
39
+ if (
40
+ this.state === 'data' ||
41
+ (this.state === 'size' && this.buffer.length > 0)
42
+ ) {
43
+ callback(new Error('Truncated aws-chunked payload'));
44
+ return;
45
+ }
46
+ callback();
47
+ }
48
+
49
+ _readLine() {
50
+ const idx = this.buffer.indexOf('\r\n');
51
+ if (idx === -1) {
52
+ if (this.buffer.length > MAX_HEADER_LENGTH) {
53
+ throw new Error('aws-chunked header line too long');
54
+ }
55
+ return null;
56
+ }
57
+ const line = this.buffer.subarray(0, idx).toString('utf8');
58
+ this.buffer = this.buffer.subarray(idx + 2);
59
+ return line;
60
+ }
61
+
62
+ _consume() {
63
+ for (;;) {
64
+ if (this.state === 'done') {
65
+ this.buffer = Buffer.alloc(0);
66
+ return;
67
+ }
68
+ if (this.state === 'size') {
69
+ const line = this._readLine();
70
+ if (line === null) return;
71
+ if (line === '') continue; // tolerate stray blank lines between chunks
72
+ const sizeToken = line.split(';')[0].trim();
73
+ if (!/^[0-9a-fA-F]+$/.test(sizeToken)) {
74
+ throw new Error(
75
+ `Invalid aws-chunked size line: ${JSON.stringify(line)}`
76
+ );
77
+ }
78
+ this.remaining = parseInt(sizeToken, 16);
79
+ this.state = this.remaining === 0 ? 'trailer' : 'data';
80
+ } else if (this.state === 'data') {
81
+ if (this.buffer.length === 0) return;
82
+ const take = Math.min(this.remaining, this.buffer.length);
83
+ this.push(this.buffer.subarray(0, take));
84
+ this.buffer = this.buffer.subarray(take);
85
+ this.remaining -= take;
86
+ if (this.remaining === 0) this.state = 'data-crlf';
87
+ } else if (this.state === 'data-crlf') {
88
+ if (this.buffer.length < 2) return;
89
+ // Expect \r\n after the chunk body; be lenient if it is missing.
90
+ if (this.buffer[0] === 0x0d && this.buffer[1] === 0x0a) {
91
+ this.buffer = this.buffer.subarray(2);
92
+ }
93
+ this.state = 'size';
94
+ } else if (this.state === 'trailer') {
95
+ const line = this._readLine();
96
+ if (line === null) {
97
+ // No trailing CRLF after the zero chunk is also accepted.
98
+ if (this.buffer.length === 0) this.state = 'done';
99
+ return;
100
+ }
101
+ if (line === '') {
102
+ this.state = 'done';
103
+ continue;
104
+ }
105
+ const idx = line.indexOf(':');
106
+ if (idx > 0) {
107
+ const name = line.slice(0, idx).trim().toLowerCase();
108
+ const value = line.slice(idx + 1).trim();
109
+ if (name !== 'x-amz-trailer-signature') {
110
+ this.trailers[name] = value;
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ module.exports = { AwsChunkedDecoder };
package/lib/cli.js ADDED
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const S3TestServer = require('./index');
6
+
7
+ const HELP = `Usage: s3-test-server [options]
8
+
9
+ A lightweight Amazon S3 emulator for development and testing.
10
+
11
+ Options:
12
+ -a, --address <host> Hostname or IP to bind to (default: localhost)
13
+ -p, --port <n> Port of the server (default: 4568)
14
+ -d, --directory <path> Data directory (default: a temporary directory)
15
+ --access-key-id <id> Access key clients must sign with (default: S3TEST)
16
+ --secret-access-key <key> Matching secret access key (default: S3TEST)
17
+ --region <region> Region reported by the server (default: us-east-1)
18
+ --key <path> Path to a private key file for HTTPS
19
+ --cert <path> Path to a certificate file for HTTPS
20
+ --service-endpoint <host> Root domain for virtual-host-style access
21
+ (default: amazonaws.com)
22
+ --no-vhost-buckets Disable virtual-host-style bucket addressing
23
+ --allow-mismatched-signatures Accept well-formed but incorrect signatures
24
+ --reset-on-start Clear the data directory when the server starts
25
+ --reset-on-close Delete all bucket data when the server closes
26
+ --configure-bucket <name[,config,...]>
27
+ Create a bucket at startup; optional paths to
28
+ CORS/website/tagging XML files (repeatable)
29
+ -s, --silent Suppress log output
30
+ -v, --version Print the version and exit
31
+ -h, --help Show this help
32
+
33
+ Example:
34
+ s3-test-server -d ./s3-data -p 9000 \\
35
+ --configure-bucket "website,./cors.xml,./website.xml"
36
+ `;
37
+
38
+ function parseArgs(argv) {
39
+ const options = { configureBuckets: [] };
40
+ for (let i = 0; i < argv.length; i++) {
41
+ const arg = argv[i];
42
+ const next = () => {
43
+ i += 1;
44
+ if (i >= argv.length) {
45
+ console.error(`Missing value for ${arg}`);
46
+ process.exit(1);
47
+ }
48
+ return argv[i];
49
+ };
50
+ switch (arg) {
51
+ case '-a':
52
+ case '--address':
53
+ options.address = next();
54
+ break;
55
+ case '-p':
56
+ case '--port':
57
+ options.port = parseInt(next(), 10);
58
+ break;
59
+ case '-d':
60
+ case '--directory':
61
+ options.directory = path.resolve(next());
62
+ break;
63
+ case '--access-key-id':
64
+ options.accessKeyId = next();
65
+ break;
66
+ case '--secret-access-key':
67
+ options.secretAccessKey = next();
68
+ break;
69
+ case '--region':
70
+ options.region = next();
71
+ break;
72
+ case '--key':
73
+ options.key = fs.readFileSync(next());
74
+ break;
75
+ case '--cert':
76
+ options.cert = fs.readFileSync(next());
77
+ break;
78
+ case '--service-endpoint':
79
+ options.serviceEndpoint = next();
80
+ break;
81
+ case '--no-vhost-buckets':
82
+ options.vhostBuckets = false;
83
+ break;
84
+ case '--allow-mismatched-signatures':
85
+ options.allowMismatchedSignatures = true;
86
+ break;
87
+ case '--reset-on-start':
88
+ options.resetOnStart = true;
89
+ break;
90
+ case '--reset-on-close':
91
+ options.resetOnClose = true;
92
+ break;
93
+ case '--configure-bucket': {
94
+ const [name, ...configPaths] = next().split(',');
95
+ options.configureBuckets.push({
96
+ name: name.trim(),
97
+ configs: configPaths.map((configPath) =>
98
+ fs.readFileSync(path.resolve(configPath.trim()))
99
+ ),
100
+ });
101
+ break;
102
+ }
103
+ case '-s':
104
+ case '--silent':
105
+ options.silent = true;
106
+ break;
107
+ case '-v':
108
+ case '--version':
109
+ console.log(require('../package.json').version);
110
+ process.exit(0);
111
+ break;
112
+ case '-h':
113
+ case '--help':
114
+ console.log(HELP);
115
+ process.exit(0);
116
+ break;
117
+ default:
118
+ console.error(`Unknown option: ${arg}\n`);
119
+ console.error(HELP);
120
+ process.exit(1);
121
+ }
122
+ }
123
+ return options;
124
+ }
125
+
126
+ async function main() {
127
+ const options = parseArgs(process.argv.slice(2));
128
+ const server = new S3TestServer(options);
129
+ const shutdown = () => {
130
+ server.close().finally(() => process.exit(0));
131
+ };
132
+ process.on('SIGINT', shutdown);
133
+ process.on('SIGTERM', shutdown);
134
+ try {
135
+ await server.run();
136
+ } catch (err) {
137
+ console.error(err.message);
138
+ process.exit(1);
139
+ }
140
+ }
141
+
142
+ module.exports = { main };