mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Cache static assets.
Closes https://github.com/solid/community-server/issues/861
This commit is contained in:
parent
7983170795
commit
745eef798a
@ -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",
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user