Files
2026-03-20 23:12:25 +01:00

82 lines
2.2 KiB
JavaScript

const http = require('http');
/**
* Minimal in-memory S3-compatible server for testing.
* Supports PutObject and GetObject via path-style requests.
*/
function createFakeS3() {
const buckets = new Map();
const server = http.createServer((req, res) => {
const { bucket, key } = parsePath(req.url);
if (!bucket || !buckets.has(bucket)) {
res.writeHead(404, { 'Content-Type': 'application/xml' });
res.end('<Error><Code>NoSuchBucket</Code></Error>');
return;
}
const store = buckets.get(bucket);
if (req.method === 'PUT' && key) {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
store.set(key, Buffer.concat(chunks));
res.writeHead(200);
res.end();
});
} else if ((req.method === 'GET' || req.method === 'HEAD') && key) {
if (!store.has(key)) {
res.writeHead(404, { 'Content-Type': 'application/xml' });
res.end('<Error><Code>NoSuchKey</Code></Error>');
return;
}
const body = store.get(key);
res.writeHead(200, {
'Content-Length': body.length,
'Content-Type': 'application/octet-stream',
});
res.end(req.method === 'HEAD' ? undefined : body);
} else {
res.writeHead(405);
res.end();
}
});
return {
start() {
return new Promise((resolve) => {
server.listen(0, '127.0.0.1', () => {
const { port } = server.address();
resolve({
address: `http://127.0.0.1:${port}`,
createBucket(name) {
if (!buckets.has(name)) buckets.set(name, new Map());
},
reset() {
for (const store of buckets.values()) store.clear();
},
stop() {
return new Promise((r) => server.close(r));
},
});
});
});
},
};
}
function parsePath(url) {
// Path-style: /<bucket>/<key>
const parts = new URL(url, 'http://localhost').pathname
.replace(/^\//, '')
.split('/');
return {
bucket: parts[0] || null,
key: parts.slice(1).join('/') || null,
};
}
module.exports = { createFakeS3 };