feat: Cache static assets.

Closes https://github.com/solid/community-server/issues/861
This commit is contained in:
Ruben Verborgh 2021-07-27 23:20:08 +02:00
parent 7983170795
commit 745eef798a
3 changed files with 35 additions and 2 deletions

View File

@ -5,6 +5,7 @@
"comment": "Servers static files on fixed URLs.",
"@id": "urn:solid-server:default:StaticAssetHandler",
"@type": "StaticAssetHandler",
"options_expires": 86400,
"assets": [
{
"StaticAssetHandler:_assets_key": "/favicon.ico",

View File

@ -20,20 +20,23 @@ import type { HttpRequest } from '../HttpRequest';
export class StaticAssetHandler extends HttpHandler {
private readonly mappings: Record<string, string>;
private readonly pathMatcher: RegExp;
private readonly expires: number;
private readonly logger = getLoggerFor(this);
/**
* Creates a handler for the provided static resources.
* @param assets - A mapping from URL paths to paths,
* where URL paths ending in a slash are interpreted as entire folders.
* @param options - Cache expiration time in seconds.
*/
public constructor(assets: Record<string, string>) {
public constructor(assets: Record<string, string>, options: { expires?: number } = {}) {
super();
this.mappings = {};
for (const [ url, path ] of Object.entries(assets)) {
this.mappings[url] = resolveAssetPath(path);
}
this.pathMatcher = this.createPathMatcher(assets);
this.expires = Number.isInteger(options.expires) ? Math.max(0, options.expires!) : 0;
}
/**
@ -90,7 +93,10 @@ export class StaticAssetHandler extends HttpHandler {
// Write a 200 response when the asset becomes readable
asset.once('readable', (): void => {
const contentType = mime.lookup(filePath) || APPLICATION_OCTET_STREAM;
response.writeHead(200, { 'content-type': contentType });
response.writeHead(200, {
'content-type': contentType,
...this.getCacheHeaders(),
});
// With HEAD, only write the headers
if (request.method === 'HEAD') {
@ -120,4 +126,13 @@ export class StaticAssetHandler extends HttpHandler {
});
});
}
private getCacheHeaders(): Record<string, string> {
return this.expires <= 0 ?
{} :
{
'cache-control': `max-age=${this.expires}`,
expires: new Date(Date.now() + (this.expires * 1000)).toUTCString(),
};
}
}

View File

@ -213,4 +213,21 @@ describe('A StaticAssetHandler', (): void => {
const response = createResponse({ eventEmitter: EventEmitter });
await expect(handler.canHandle({ request, response } as any)).rejects.toThrow();
});
it('caches responses when the expires option is set.', async(): Promise<void> => {
jest.spyOn(Date, 'now').mockReturnValue(0);
const cachedHandler = new StaticAssetHandler({
'/foo/bar/style': '/assets/styles/bar.css',
}, {
expires: 86400,
});
const request = { method: 'GET', url: '/foo/bar/style' };
const response = createResponse();
await cachedHandler.handleSafe({ request, response } as any);
jest.restoreAllMocks();
expect(response.statusCode).toBe(200);
expect(response.getHeaders()).toHaveProperty('cache-control', 'max-age=86400');
expect(response.getHeaders()).toHaveProperty('expires', 'Fri, 02 Jan 1970 00:00:00 GMT');
});
});