mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Support $MODULE_PATH in StaticAssetHandler
This commit is contained in:
parent
0a420847dc
commit
e9917322e3
@ -51,7 +51,7 @@
|
|||||||
"StaticAssetHandler:_assets": [
|
"StaticAssetHandler:_assets": [
|
||||||
{
|
{
|
||||||
"StaticAssetHandler:_assets_key": "/favicon.ico",
|
"StaticAssetHandler:_assets_key": "/favicon.ico",
|
||||||
"StaticAssetHandler:_assets_value": "./templates/root/favicon.ico"
|
"StaticAssetHandler:_assets_value": "$PACKAGE_ROOT/templates/root/favicon.ico"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { getLoggerFor } from '../../logging/LogUtil';
|
|||||||
import { APPLICATION_OCTET_STREAM } from '../../util/ContentTypes';
|
import { APPLICATION_OCTET_STREAM } from '../../util/ContentTypes';
|
||||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||||
import { joinFilePath } from '../../util/PathUtil';
|
import { joinFilePath, resolveAssetPath } from '../../util/PathUtil';
|
||||||
import { pipeSafely } from '../../util/StreamUtil';
|
import { pipeSafely } from '../../util/StreamUtil';
|
||||||
import type { HttpHandlerInput } from '../HttpHandler';
|
import type { HttpHandlerInput } from '../HttpHandler';
|
||||||
import { HttpHandler } from '../HttpHandler';
|
import { HttpHandler } from '../HttpHandler';
|
||||||
@ -13,6 +13,9 @@ import type { HttpRequest } from '../HttpRequest';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler that serves static resources on specific paths.
|
* Handler that serves static resources on specific paths.
|
||||||
|
* Relative file paths are assumed to be relative to cwd.
|
||||||
|
* Relative file paths can be preceded by $PACKAGE_ROOT/, e.g. $PACKAGE_ROOT/foo/bar,
|
||||||
|
* in case they need to be relative to the module root.
|
||||||
*/
|
*/
|
||||||
export class StaticAssetHandler extends HttpHandler {
|
export class StaticAssetHandler extends HttpHandler {
|
||||||
private readonly mappings: Record<string, string>;
|
private readonly mappings: Record<string, string>;
|
||||||
@ -26,7 +29,10 @@ export class StaticAssetHandler extends HttpHandler {
|
|||||||
*/
|
*/
|
||||||
public constructor(assets: Record<string, string>) {
|
public constructor(assets: Record<string, string>) {
|
||||||
super();
|
super();
|
||||||
this.mappings = { ...assets };
|
this.mappings = {};
|
||||||
|
for (const [ url, path ] of Object.entries(assets)) {
|
||||||
|
this.mappings[url] = resolveAssetPath(path);
|
||||||
|
}
|
||||||
this.pathMatcher = this.createPathMatcher(assets);
|
this.pathMatcher = this.createPathMatcher(assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,3 +165,23 @@ export function createSubdomainRegexp(baseUrl: string): RegExp {
|
|||||||
const { scheme, rest } = extractScheme(baseUrl);
|
const { scheme, rest } = extractScheme(baseUrl);
|
||||||
return new RegExp(`^${scheme}(?:([^/]+)\\.)?${rest}`, 'u');
|
return new RegExp(`^${scheme}(?:([^/]+)\\.)?${rest}`, 'u');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the folder corresponding to the root of the Community Solid Server module
|
||||||
|
*/
|
||||||
|
export function getModuleRoot(): string {
|
||||||
|
return joinFilePath(__dirname, '../../');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts file path inputs into absolute paths.
|
||||||
|
* Works similar to `absoluteFilePath` but paths that start with '$PACKAGE_ROOT/'
|
||||||
|
* will be relative to the module directory instead of the cwd.
|
||||||
|
*/
|
||||||
|
export function resolveAssetPath(path: string): string {
|
||||||
|
const modulePath = '$PACKAGE_ROOT/';
|
||||||
|
if (path.startsWith(modulePath)) {
|
||||||
|
return joinFilePath(getModuleRoot(), path.slice(modulePath.length));
|
||||||
|
}
|
||||||
|
return absoluteFilePath(path);
|
||||||
|
}
|
||||||
|
@ -6,15 +6,18 @@ import streamifyArray from 'streamify-array';
|
|||||||
import { StaticAssetHandler } from '../../../../src/server/middleware/StaticAssetHandler';
|
import { StaticAssetHandler } from '../../../../src/server/middleware/StaticAssetHandler';
|
||||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
||||||
import type { SystemError } from '../../../../src/util/errors/SystemError';
|
import type { SystemError } from '../../../../src/util/errors/SystemError';
|
||||||
|
import { getModuleRoot, joinFilePath } from '../../../../src/util/PathUtil';
|
||||||
|
|
||||||
const createReadStream = jest.spyOn(fs, 'createReadStream')
|
const createReadStream = jest.spyOn(fs, 'createReadStream')
|
||||||
.mockImplementation((): any => streamifyArray([ 'file contents' ]));
|
.mockImplementation((): any => streamifyArray([ 'file contents' ]));
|
||||||
|
|
||||||
describe('a StaticAssetHandler', (): void => {
|
describe('A StaticAssetHandler', (): void => {
|
||||||
const handler = new StaticAssetHandler({
|
const handler = new StaticAssetHandler({
|
||||||
'/foo/bar/style': '/assets/styles/bar.css',
|
'/foo/bar/style': '/assets/styles/bar.css',
|
||||||
'/foo/bar/main': '/assets/scripts/bar.js',
|
'/foo/bar/main': '/assets/scripts/bar.js',
|
||||||
'/foo/bar/unknown': '/assets/bar.unknown',
|
'/foo/bar/unknown': '/assets/bar.unknown',
|
||||||
|
'/foo/bar/cwd': 'paths/cwd.txt',
|
||||||
|
'/foo/bar/module': '$PACKAGE_ROOT/paths/module.txt',
|
||||||
'/foo/bar/folder1/': '/assets/folders/1/',
|
'/foo/bar/folder1/': '/assets/folders/1/',
|
||||||
'/foo/bar/folder2/': '/assets/folders/2',
|
'/foo/bar/folder2/': '/assets/folders/2',
|
||||||
'/foo/bar/folder2/subfolder/': '/assets/folders/3',
|
'/foo/bar/folder2/subfolder/': '/assets/folders/3',
|
||||||
@ -92,6 +95,30 @@ describe('a StaticAssetHandler', (): void => {
|
|||||||
expect(createReadStream).toHaveBeenCalledWith('/assets/bar.unknown');
|
expect(createReadStream).toHaveBeenCalledWith('/assets/bar.unknown');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles a request to a known URL with a relative file path.', async(): Promise<void> => {
|
||||||
|
const request = { method: 'GET', url: '/foo/bar/cwd' };
|
||||||
|
const response = createResponse({ eventEmitter: EventEmitter });
|
||||||
|
await handler.handleSafe({ request, response } as any);
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.getHeaders()).toHaveProperty('content-type', 'text/plain');
|
||||||
|
|
||||||
|
expect(createReadStream).toHaveBeenCalledTimes(1);
|
||||||
|
expect(createReadStream).toHaveBeenCalledWith(joinFilePath(process.cwd(), '/paths/cwd.txt'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles a request to a known URL with a relative to module file path.', async(): Promise<void> => {
|
||||||
|
const request = { method: 'GET', url: '/foo/bar/module' };
|
||||||
|
const response = createResponse({ eventEmitter: EventEmitter });
|
||||||
|
await handler.handleSafe({ request, response } as any);
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.getHeaders()).toHaveProperty('content-type', 'text/plain');
|
||||||
|
|
||||||
|
expect(createReadStream).toHaveBeenCalledTimes(1);
|
||||||
|
expect(createReadStream).toHaveBeenCalledWith(joinFilePath(getModuleRoot(), '/paths/module.txt'));
|
||||||
|
});
|
||||||
|
|
||||||
it('throws a 404 when the asset does not exist.', async(): Promise<void> => {
|
it('throws a 404 when the asset does not exist.', async(): Promise<void> => {
|
||||||
const request = { method: 'GET', url: '/foo/bar/main' };
|
const request = { method: 'GET', url: '/foo/bar/main' };
|
||||||
const response = createResponse({ eventEmitter: EventEmitter });
|
const response = createResponse({ eventEmitter: EventEmitter });
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
import { existsSync } from 'fs';
|
||||||
import {
|
import {
|
||||||
absoluteFilePath, createSubdomainRegexp,
|
absoluteFilePath, createSubdomainRegexp,
|
||||||
decodeUriPathComponents,
|
decodeUriPathComponents,
|
||||||
encodeUriPathComponents,
|
encodeUriPathComponents,
|
||||||
ensureTrailingSlash, extractScheme, getExtension, isContainerIdentifier, isContainerPath,
|
ensureTrailingSlash, extractScheme, getExtension, getModuleRoot, isContainerIdentifier, isContainerPath,
|
||||||
joinFilePath,
|
joinFilePath,
|
||||||
normalizeFilePath,
|
normalizeFilePath, resolveAssetPath,
|
||||||
toCanonicalUriPath, trimTrailingSlashes,
|
toCanonicalUriPath, trimTrailingSlashes,
|
||||||
} from '../../../src/util/PathUtil';
|
} from '../../../src/util/PathUtil';
|
||||||
|
|
||||||
@ -129,4 +130,35 @@ describe('PathUtil', (): void => {
|
|||||||
expect(regex.exec('http://alicetest.com/foo/')).toBeNull();
|
expect(regex.exec('http://alicetest.com/foo/')).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#getModuleRoot', (): void => {
|
||||||
|
it('returns the root folder of the module.', async(): Promise<void> => {
|
||||||
|
// Note that this test only makes sense as long as the dist folder is on the same level as the src folder
|
||||||
|
const root = getModuleRoot();
|
||||||
|
const packageJson = joinFilePath(root, 'package.json');
|
||||||
|
expect(existsSync(packageJson)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#resolvePathInput', (): void => {
|
||||||
|
it('interprets paths relative to the module root when starting with $PACKAGE_ROOT/.', async(): Promise<void> => {
|
||||||
|
expect(resolveAssetPath('$PACKAGE_ROOT/foo/bar')).toBe(joinFilePath(getModuleRoot(), '/foo/bar'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles ../ paths with $PACKAGE_ROOT/.', async(): Promise<void> => {
|
||||||
|
expect(resolveAssetPath('$PACKAGE_ROOT/foo/bar/../baz')).toBe(joinFilePath(getModuleRoot(), '/foo/baz'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves absolute paths as they are.', async(): Promise<void> => {
|
||||||
|
expect(resolveAssetPath('/foo/bar/')).toBe('/foo/bar/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles other paths relative to the cwd.', async(): Promise<void> => {
|
||||||
|
expect(resolveAssetPath('foo/bar/')).toBe(joinFilePath(process.cwd(), 'foo/bar/'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles other paths with ../.', async(): Promise<void> => {
|
||||||
|
expect(resolveAssetPath('foo/bar/../baz')).toBe(joinFilePath(process.cwd(), 'foo/baz'));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user