Skip to content

Commit 0740ff9

Browse files
committed
Add support for serving content directly from .zip files
1 parent 4cb413a commit 0740ff9

29 files changed

Lines changed: 2681 additions & 957 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
*.tgz
88
/package/treeshake/*.output.mjs
99
/package/cli/sample/file.txt.gz
10+
test-zips/

docs/API.md

Lines changed: 358 additions & 76 deletions
Large diffs are not rendered by default.

src/extras/cache/etag.mts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { createReadStream, type Stats } from 'node:fs';
2-
import type { FileHandle } from 'node:fs/promises';
32
import { createHash } from 'node:crypto';
43
import { pipeline } from 'node:stream/promises';
4+
import type { ReadOnlyFileHandle } from '../../util/ReadOnlyFileHandle.mts';
5+
import { createSafeReadStream } from '../../util/createSafeReadStream.mts';
56

67
export function generateWeakETag(
78
contentEncoding: string | number | string[] | undefined,
@@ -12,12 +13,14 @@ export function generateWeakETag(
1213
return `W/"${hash}"`;
1314
}
1415

15-
export async function generateStrongETag(file: string | FileHandle) {
16+
export async function generateStrongETag(
17+
file: string | Pick<ReadOnlyFileHandle, 'createReadStream'>,
18+
) {
1619
const hash = createHash('sha256');
1720
if (typeof file === 'string') {
1821
await pipeline(createReadStream(file), hash);
1922
} else {
20-
await pipeline(file.createReadStream({ start: 0, autoClose: false }), hash);
23+
await pipeline(createSafeReadStream(file, { start: 0, autoClose: false }), hash);
2124
}
2225
return `"sha256-${hash.digest('base64')}"`;
2326
}

src/extras/filesystem/FileFinder.test.mts renamed to src/extras/filesystem/FileFinder.interface-spec.mts

Lines changed: 70 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import { join, sep } from 'node:path';
2-
import {
3-
makeFileStructure,
4-
makeTestTempDir,
5-
type FilesDefinition,
6-
} from '../../test-helpers/makeFileStructure.mts';
2+
import { type FilesDefinition } from '../../test-helpers/makeFileStructure.mts';
73
import { Negotiator } from '../request/Negotiator.mts';
8-
import { FileFinder, type FileFinderCore, type FileFinderOptions } from './FileFinder.mts';
4+
import type { FileFinder, FileFinderOptions } from './FileFinder.mts';
5+
import type { TypedParameters } from 'lean-test';
96
import 'lean-test';
107

11-
function fileFinderTestSuite(isPrecomputed: boolean) {
12-
it('resolves files within a directory', async ({ getTyped }) => {
13-
const fileFinder = await initialise(getTyped(TEST_DIR), {
8+
export function fileFinderTestSuite(
9+
initialise: (
10+
ctx: TypedParameters,
11+
structure: FilesDefinition,
12+
options?: FileFinderOptions,
13+
relativePath?: string[],
14+
) => Promise<FileFinder>,
15+
getRootDir: (ctx: TypedParameters) => string,
16+
) {
17+
it('resolves files within a directory', async (ctx) => {
18+
const fileFinder = await initialise(ctx, {
1419
'one.txt': 'Content',
1520
'two.txt': 'Other',
1621
sub: { 'three.txt': 'Nested Content' },
@@ -23,8 +28,8 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
2328
expect(one!.headers['content-language']).isUndefined();
2429
expect(one!.headers['content-language']).isUndefined();
2530
expect(one!.headers['vary']).isUndefined();
26-
expect(one!.canonicalPath).endsWith(sep + 'one.txt');
27-
expect(one!.negotiatedPath).endsWith(sep + 'one.txt');
31+
expect(one!.canonicalFilename).equals('one.txt');
32+
expect(one!.filesystemPath).endsWith(sep + 'one.txt');
2833
expect(one!.stats.size).equals(7);
2934
} finally {
3035
one?.handle.close();
@@ -33,8 +38,8 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
3338
const three = await fileFinder.find(['sub', 'three.txt']);
3439
expect(three).isTruthy();
3540
try {
36-
expect(three!.canonicalPath).endsWith(sep + join('sub', 'three.txt'));
37-
expect(three!.negotiatedPath).endsWith(sep + join('sub', 'three.txt'));
41+
expect(three!.canonicalFilename).equals('three.txt');
42+
expect(three!.filesystemPath).endsWith(sep + join('sub', 'three.txt'));
3843
expect(three!.stats.size).equals(14);
3944
} finally {
4045
three?.handle.close();
@@ -43,8 +48,8 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
4348
expect(await fileExists(fileFinder, ['three.txt'])).isFalse();
4449
});
4550

46-
it('serves index files if a directory is requested', async ({ getTyped }) => {
47-
const fileFinder = await initialise(getTyped(TEST_DIR), {
51+
it('serves index files if a directory is requested', async (ctx) => {
52+
const fileFinder = await initialise(ctx, {
4853
sub1: {
4954
'index.htm': 'Index Content',
5055
'foo.htm': 'Other Content',
@@ -55,8 +60,8 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
5560
const index = await fileFinder.find(['sub1']);
5661
expect(index).isTruthy();
5762
try {
58-
expect(index!.canonicalPath).endsWith(sep + join('sub1', 'index.htm'));
59-
expect(index!.negotiatedPath).endsWith(sep + join('sub1', 'index.htm'));
63+
expect(index!.canonicalFilename).equals('index.htm');
64+
expect(index!.filesystemPath).endsWith(sep + join('sub1', 'index.htm'));
6065
expect(index!.stats.size).equals(13);
6166
} finally {
6267
index?.handle.close();
@@ -69,16 +74,16 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
6974
expect(await fileExists(fileFinder, ['sub2'])).isFalse();
7075
});
7176

72-
it('serves the root index file if the root is requested', async ({ getTyped }) => {
73-
const fileFinder = await initialise(getTyped(TEST_DIR), {
77+
it('serves the root index file if the root is requested', async (ctx) => {
78+
const fileFinder = await initialise(ctx, {
7479
'index.htm': 'Index Content',
7580
});
7681

7782
const index = await fileFinder.find([]);
7883
expect(index).isTruthy();
7984
try {
80-
expect(index!.canonicalPath).endsWith(sep + join('index.htm'));
81-
expect(index!.negotiatedPath).endsWith(sep + join('index.htm'));
85+
expect(index!.canonicalFilename).equals('index.htm');
86+
expect(index!.filesystemPath).endsWith(sep + join('index.htm'));
8287
expect(index!.stats.size).equals(13);
8388
} finally {
8489
index?.handle.close();
@@ -88,18 +93,18 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
8893
expect(await fileExists(fileFinder, ['index.htm'])).isFalse();
8994
});
9095

91-
it('uses configured index files', async ({ getTyped }) => {
96+
it('uses configured index files', async (ctx) => {
9297
const fileFinder = await initialise(
93-
getTyped(TEST_DIR),
98+
ctx,
9499
{ sub: { 'index.htm': 'Index Content', 'custom.thing': 'Custom Content' } },
95100
{ indexFiles: ['custom.thing'] },
96101
);
97102

98103
const index = await fileFinder.find(['sub']);
99104
expect(index).isTruthy();
100105
try {
101-
expect(index!.canonicalPath).endsWith(sep + join('sub', 'custom.thing'));
102-
expect(index!.negotiatedPath).endsWith(sep + join('sub', 'custom.thing'));
106+
expect(index!.canonicalFilename).equals('custom.thing');
107+
expect(index!.filesystemPath).endsWith(sep + join('sub', 'custom.thing'));
103108
expect(index!.stats.size).equals(14);
104109
} finally {
105110
index?.handle.close();
@@ -109,27 +114,27 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
109114
expect(await fileExists(fileFinder, ['sub', 'custom.thing'])).isFalse();
110115
});
111116

112-
it('prioritises index files by their configured order', async ({ getTyped }) => {
117+
it('prioritises index files by their configured order', async (ctx) => {
113118
const fileFinder = await initialise(
114-
getTyped(TEST_DIR),
119+
ctx,
115120
{ a: 'nope', m: 'yep', z: 'nope' },
116121
{ indexFiles: ['m', 'a', 'z'] },
117122
);
118123

119124
const index = await fileFinder.find([]);
120125
expect(index).isTruthy();
121126
try {
122-
expect(index!.canonicalPath).endsWith(sep + 'm');
123-
expect(index!.negotiatedPath).endsWith(sep + 'm');
127+
expect(index!.canonicalFilename).equals('m');
128+
expect(index!.filesystemPath).endsWith(sep + 'm');
124129
expect(index!.stats.size).equals(3);
125130
} finally {
126131
index?.handle.close();
127132
}
128133
});
129134

130-
it('uses configured suffixes if the requested file does not exist', async ({ getTyped }) => {
135+
it('uses configured suffixes if the requested file does not exist', async (ctx) => {
131136
const fileFinder = await initialise(
132-
getTyped(TEST_DIR),
137+
ctx,
133138
{
134139
'file.foo': 'Foo',
135140
'other.bar': 'Bar',
@@ -144,8 +149,8 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
144149
const f1 = await fileFinder.find(['file']);
145150
expect(f1).isTruthy();
146151
try {
147-
expect(f1!.canonicalPath).endsWith(sep + 'file.foo');
148-
expect(f1!.negotiatedPath).endsWith(sep + 'file.foo');
152+
expect(f1!.canonicalFilename).equals('file.foo');
153+
expect(f1!.filesystemPath).endsWith(sep + 'file.foo');
149154
expect(f1!.stats.size).equals(3);
150155
} finally {
151156
f1?.handle.close();
@@ -154,8 +159,8 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
154159
const f2 = await fileFinder.find(['other']);
155160
expect(f2).isTruthy();
156161
try {
157-
expect(f2!.canonicalPath).endsWith(sep + 'other.bar');
158-
expect(f2!.negotiatedPath).endsWith(sep + 'other.bar');
162+
expect(f2!.canonicalFilename).equals('other.bar');
163+
expect(f2!.filesystemPath).endsWith(sep + 'other.bar');
159164
expect(f2!.stats.size).equals(3);
160165
} finally {
161166
f2?.handle.close();
@@ -164,8 +169,8 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
164169
const f3 = await fileFinder.find(['raw']);
165170
expect(f3).isTruthy();
166171
try {
167-
expect(f3!.canonicalPath).endsWith(sep + 'raw');
168-
expect(f3!.negotiatedPath).endsWith(sep + 'raw');
172+
expect(f3!.canonicalFilename).equals('raw');
173+
expect(f3!.filesystemPath).endsWith(sep + 'raw');
169174
expect(f3!.stats.size).equals(6);
170175
} finally {
171176
f3?.handle.close();
@@ -177,34 +182,34 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
177182
expect(await fileExists(fileFinder, ['sub', 'custom.thing'])).isFalse();
178183
});
179184

180-
it('prioritises suffixes by their configured order', async ({ getTyped }) => {
185+
it('prioritises suffixes by their configured order', async (ctx) => {
181186
const fileFinder = await initialise(
182-
getTyped(TEST_DIR),
187+
ctx,
183188
{ aa: 'nope', am: 'yep', az: 'nope' },
184189
{ implicitSuffixes: ['m', 'a', 'z'] },
185190
);
186191

187192
const index = await fileFinder.find(['a']);
188193
expect(index).isTruthy();
189194
try {
190-
expect(index!.canonicalPath).endsWith(sep + 'am');
191-
expect(index!.negotiatedPath).endsWith(sep + 'am');
195+
expect(index!.canonicalFilename).equals('am');
196+
expect(index!.filesystemPath).endsWith(sep + 'am');
192197
expect(index!.stats.size).equals(3);
193198
} finally {
194199
index?.handle.close();
195200
}
196201
});
197202

198-
it('does not allow access to files outside the directory', async (props) => {
199-
const dir = props.getTyped(TEST_DIR);
203+
it('does not allow access to files outside the directory', async (ctx) => {
204+
const dir = getRootDir(ctx);
200205
const fileFinder = await initialise(
201-
dir,
206+
ctx,
202207
{
203208
'one.txt': 'Blocked Content',
204209
sub: { 'ok.txt': 'Permitted Content' },
205210
},
206211
{},
207-
'sub',
212+
['sub'],
208213
);
209214

210215
expect(await fileExists(fileFinder, ['one.txt'])).isFalse();
@@ -221,9 +226,11 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
221226
}
222227
});
223228

224-
it('does not disclose the root path', async (props) => {
225-
const dir = props.getTyped(TEST_DIR);
226-
const fileFinder = await initialise(dir, { sub: { 'ok.txt': 'Permitted Content' } }, {}, 'sub');
229+
it('does not disclose the root path', async (ctx) => {
230+
const dir = getRootDir(ctx);
231+
const fileFinder = await initialise(ctx, { sub: { 'ok.txt': 'Permitted Content' } }, {}, [
232+
'sub',
233+
]);
227234

228235
expect(await fileExists(fileFinder, ['..', 'sub', 'ok.txt'])).isFalse();
229236
expect(await fileExists(fileFinder, ['../sub/ok.txt'])).isFalse();
@@ -233,8 +240,8 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
233240
expect(await fileExists(fileFinder, [join(dir, 'sub', 'ok.txt')])).isFalse();
234241
});
235242

236-
it('does not allow access to special files by default', async ({ getTyped }) => {
237-
const fileFinder = await initialise(getTyped(TEST_DIR), {
243+
it('does not allow access to special files by default', async (ctx) => {
244+
const fileFinder = await initialise(ctx, {
238245
'.dot': 'Blocked Content',
239246
'~tilde': 'Blocked Content',
240247
'tilde~': 'Blocked Content',
@@ -256,8 +263,8 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
256263
}
257264
});
258265

259-
it('is case sensitive by default', async ({ getTyped }) => {
260-
const fileFinder = await initialise(getTyped(TEST_DIR), {
266+
it('is case sensitive by default', async (ctx) => {
267+
const fileFinder = await initialise(ctx, {
261268
'one.txt': 'Content',
262269
'TWO.txt': 'Other',
263270
Sub: { 'three.txt': 'Nested Content' },
@@ -270,9 +277,9 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
270277
expect(await fileExists(fileFinder, ['Sub', 'three.txt'])).isTrue();
271278
});
272279

273-
it('forces all paths lowercase if configured', async ({ getTyped }) => {
280+
it('forces all paths lowercase if configured', async (ctx) => {
274281
const fileFinder = await initialise(
275-
getTyped(TEST_DIR),
282+
ctx,
276283
{
277284
'one.txt': 'Content',
278285
sub: { 'two.txt': 'Nested Content' },
@@ -286,9 +293,9 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
286293
expect(await fileExists(fileFinder, ['Sub', 'two.TXT'])).isTrue();
287294
});
288295

289-
it('returns specific file variants if negotiated', async ({ getTyped }) => {
296+
it('returns specific file variants if negotiated', async (ctx) => {
290297
const fileFinder = await initialise(
291-
getTyped(TEST_DIR),
298+
ctx,
292299
{
293300
'one.txt': 'Content',
294301
'one.txt.gz': 'Compressed Content',
@@ -308,8 +315,8 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
308315
expect(withGzip!.headers['content-language']).isUndefined();
309316
expect(withGzip!.headers['content-encoding']).equals('gzip');
310317
expect(withGzip!.headers['vary']).equals('accept-encoding');
311-
expect(withGzip!.canonicalPath).endsWith(sep + 'one.txt');
312-
expect(withGzip!.negotiatedPath).endsWith(sep + 'one.txt.gz');
318+
expect(withGzip!.canonicalFilename).equals('one.txt');
319+
expect(withGzip!.filesystemPath).endsWith(sep + 'one.txt.gz');
313320
expect(withGzip!.stats.size).equals(18);
314321
} finally {
315322
withGzip?.handle.close();
@@ -322,8 +329,8 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
322329
expect(withoutGzip!.headers['content-language']).isUndefined();
323330
expect(withoutGzip!.headers['content-encoding']).isUndefined();
324331
expect(withoutGzip!.headers['vary']).equals('accept-encoding');
325-
expect(withoutGzip!.canonicalPath).endsWith(sep + 'one.txt');
326-
expect(withoutGzip!.negotiatedPath).endsWith(sep + 'one.txt');
332+
expect(withoutGzip!.canonicalFilename).equals('one.txt');
333+
expect(withoutGzip!.filesystemPath).endsWith(sep + 'one.txt');
327334
expect(withoutGzip!.stats.size).equals(7);
328335
} finally {
329336
withoutGzip?.handle.close();
@@ -336,52 +343,16 @@ function fileFinderTestSuite(isPrecomputed: boolean) {
336343
expect(noGzip!.headers['content-language']).isUndefined();
337344
expect(noGzip!.headers['content-encoding']).isUndefined();
338345
expect(noGzip!.headers['vary']).equals('accept-encoding');
339-
expect(noGzip!.canonicalPath).endsWith(sep + 'two.txt');
340-
expect(noGzip!.negotiatedPath).endsWith(sep + 'two.txt');
346+
expect(noGzip!.canonicalFilename).equals('two.txt');
347+
expect(noGzip!.filesystemPath).endsWith(sep + 'two.txt');
341348
expect(noGzip!.stats.size).equals(7);
342349
} finally {
343350
noGzip?.handle.close();
344351
}
345352
});
346-
347-
describe('debugAllPaths', () => {
348-
it('returns a set of all recognised paths', async ({ getTyped }) => {
349-
const fileFinder = await initialise(getTyped(TEST_DIR), {
350-
'foo.txt': 'Hello',
351-
sub1: {
352-
'index.htm': 'Index Content',
353-
'foo.htm': 'Other Content',
354-
},
355-
sub2: { 'nope.htm': 'Nested Content' },
356-
});
357-
358-
const paths = await fileFinder.debugAllPaths();
359-
expect(paths).equals(new Set(['foo.txt', 'sub1', 'sub1/foo.htm', 'sub2/nope.htm']));
360-
});
361-
});
362-
363-
const TEST_DIR = makeTestTempDir('ff-');
364-
365-
async function initialise(
366-
dir: string,
367-
structure: FilesDefinition,
368-
options: FileFinderOptions = {},
369-
relativePath = '',
370-
) {
371-
await makeFileStructure(dir, structure);
372-
const fileFinder = await FileFinder.build(join(dir, relativePath), options);
373-
if (isPrecomputed) {
374-
return fileFinder.precompute();
375-
} else {
376-
return fileFinder;
377-
}
378-
}
379353
}
380354

381-
describe('FileFinder (Dynamic)', () => fileFinderTestSuite(false));
382-
describe('FileFinder (Precomputed)', () => fileFinderTestSuite(true));
383-
384-
async function fileExists(fileFinder: FileFinderCore, path: string[]) {
355+
async function fileExists(fileFinder: FileFinder, path: string[]) {
385356
const file = await fileFinder.find(path);
386357
const exists = Boolean(file);
387358
file?.handle.close();

0 commit comments

Comments
 (0)