feat: Update StaticAssetHandler to allow for easier overrides

This commit is contained in:
Joachim Van Herwegen 2023-07-17 09:38:13 +02:00
parent a8b5d5eb45
commit ea83ea59a1
18 changed files with 155 additions and 74 deletions

View File

@ -2,7 +2,7 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/init/default.json",
"css:config/app/init/initializers/prefilled-root.json"
"css:config/app/init/initializers/root.json"
],
"@graph": [
{
@ -12,6 +12,11 @@
"handlers": [
{ "@id": "urn:solid-server:default:RootInitializer" }
]
},
{
"@id": "urn:solid-server:default:RootFolderGenerator",
"@type": "StaticFolderGenerator",
"templateFolder": "@css:templates/root/prefilled"
}
]
}

View File

@ -12,6 +12,11 @@
"handlers": [
{ "@id": "urn:solid-server:default:RootInitializer" }
]
},
{
"@id": "urn:solid-server:default:RootFolderGenerator",
"@type": "StaticFolderGenerator",
"templateFolder": "@css:templates/root/empty"
}
]
}

View File

@ -1,26 +0,0 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Makes sure the root container exists and contains the necessary resources.",
"@id": "urn:solid-server:default:RootInitializer",
"@type": "ConditionalHandler",
"storageKey": "rootInitialized",
"storageValue": true,
"storage": { "@id": "urn:solid-server:default:SetupStorage" },
"source": {
"@type": "ContainerInitializer",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_path": "/",
"args_store": { "@id": "urn:solid-server:default:ResourceStore" },
"args_generator": {
"@type": "StaticFolderGenerator",
"templateFolder": "@css:templates/root/prefilled",
"resourcesGenerator": { "@id": "urn:solid-server:default:TemplatedResourcesGenerator" }
},
"args_storageKey": "rootInitialized",
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
}
}
]
}

View File

@ -9,13 +9,14 @@
"storageValue": true,
"storage": { "@id": "urn:solid-server:default:SetupStorage" },
"source": {
"@id": "urn:solid-server:default:RootContainerInitializer",
"@type": "ContainerInitializer",
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_path": "/",
"args_store": { "@id": "urn:solid-server:default:ResourceStore" },
"args_generator": {
"@id": "urn:solid-server:default:RootFolderGenerator",
"@type": "StaticFolderGenerator",
"templateFolder": "@css:templates/root/empty",
"resourcesGenerator": { "@id": "urn:solid-server:default:TemplatedResourcesGenerator" }
},
"args_storageKey": "rootInitialized",

View File

@ -10,8 +10,10 @@
"@type": "StaticAssetHandler",
"assets": [
{
"StaticAssetHandler:_assets_key": "/",
"StaticAssetHandler:_assets_value": "@css:templates/root/prefilled/base/index.html"
"@id": "urn:solid-server:default:RootStaticAsset",
"@type": "StaticAssetEntry",
"relativeUrl": "/",
"filePath": "@css:templates/root/static/index.html"
}
]
}

View File

@ -2,7 +2,7 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
"css:config/app/init/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",

View File

@ -2,7 +2,7 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
"css:config/app/init/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",

View File

@ -2,7 +2,7 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
"css:config/app/init/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",

View File

@ -2,7 +2,7 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
"css:config/app/init/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",

View File

@ -9,26 +9,35 @@
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"assets": [
{
"StaticAssetHandler:_assets_key": "/favicon.ico",
"StaticAssetHandler:_assets_value": "@css:templates/images/favicon.ico"
"@id": "urn:solid-server:default:FaviconStaticAsset",
"@type": "StaticAssetEntry",
"relativeUrl": "/favicon.ico",
"filePath": "@css:templates/images/favicon.ico"
},
{
"StaticAssetHandler:_assets_key": "/.well-known/css/styles/",
"StaticAssetHandler:_assets_value": "@css:templates/styles/"
"@id": "urn:solid-server:default:StylesStaticAsset",
"@type": "StaticAssetEntry",
"relativeUrl": "/.well-known/css/styles/",
"filePath": "@css:templates/styles/"
},
{
"StaticAssetHandler:_assets_key": "/.well-known/css/fonts/",
"StaticAssetHandler:_assets_value": "@css:templates/fonts/"
"@id": "urn:solid-server:default:FontsStaticAsset",
"@type": "StaticAssetEntry",
"relativeUrl": "/.well-known/css/fonts/",
"filePath": "@css:templates/fonts/"
},
{
"StaticAssetHandler:_assets_key": "/.well-known/css/images/",
"StaticAssetHandler:_assets_value": "@css:templates/images/"
"@id": "urn:solid-server:default:ImagesStaticAsset",
"@type": "StaticAssetEntry",
"relativeUrl": "/.well-known/css/images/",
"filePath": "@css:templates/images/"
},
{
"StaticAssetHandler:_assets_key": "/.well-known/css/scripts/",
"StaticAssetHandler:_assets_value": "@css:templates/scripts/"
"@id": "urn:solid-server:default:ScriptsStaticAsset",
"@type": "StaticAssetEntry",
"relativeUrl": "/.well-known/css/scripts/",
"filePath": "@css:templates/scripts/"
}
]
}
]

View File

@ -2,7 +2,7 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
"css:config/app/init/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",

View File

@ -2,7 +2,7 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
"css:config/app/init/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",

View File

@ -2,7 +2,7 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
"css:config/app/init/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/disabled.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",

View File

@ -2,7 +2,7 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
"css:config/app/init/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",

View File

@ -2,7 +2,7 @@
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
"import": [
"css:config/app/main/default.json",
"css:config/app/init/default.json",
"css:config/app/init/static-root.json",
"css:config/app/setup/required.json",
"css:config/app/variables/default.json",
"css:config/http/handler/default.json",

View File

@ -12,9 +12,20 @@ import type { HttpHandlerInput } from '../HttpHandler';
import { HttpHandler } from '../HttpHandler';
import type { HttpRequest } from '../HttpRequest';
/**
* Used to link file paths with relative URLs.
* By using a separate class instead of a key/value map it is easier to replace values in Components.js.
*/
export class StaticAssetEntry {
public constructor(
public readonly relativeUrl: string,
public readonly filePath: string,
) { }
}
/**
* Handler that serves static resources on specific paths.
* Relative file paths are assumed to be relative to cwd.
* Relative file paths are assumed to be relative to the current working directory.
* Relative file paths can be preceded by `@css:`, e.g. `@css:foo/bar`,
* in case they need to be relative to the module root.
* File paths ending in a slash assume the target is a folder and map all of its contents.
@ -27,18 +38,17 @@ export class StaticAssetHandler extends HttpHandler {
/**
* 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 assets - A list of {@link StaticAssetEntry}.
* @param baseUrl - The base URL of the server.
* @param options - Cache expiration time in seconds.
*/
public constructor(assets: Record<string, string>, baseUrl: string, options: { expires?: number } = {}) {
public constructor(assets: StaticAssetEntry[], baseUrl: string, options: { expires?: number } = {}) {
super();
this.mappings = {};
const rootPath = ensureTrailingSlash(new URL(baseUrl).pathname);
for (const [ url, path ] of Object.entries(assets)) {
this.mappings[trimLeadingSlashes(url)] = resolveAssetPath(path);
for (const { relativeUrl, filePath } of assets) {
this.mappings[trimLeadingSlashes(relativeUrl)] = resolveAssetPath(filePath);
}
this.pathMatcher = this.createPathMatcher(rootPath);
this.expires = Number.isInteger(options.expires) ? Math.max(0, options.expires!) : 0;

View File

@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Community Solid Server</title>
<link rel="stylesheet" href="/.well-known/css/styles/main.css" type="text/css">
</head>
<body>
<header>
<a href=".."><img src="/.well-known/css/images/solid.svg" alt="[Solid logo]" /></a>
<h1>Community Solid Server</h1>
</header>
<main>
<h1>Welcome to Solid</h1>
<p>
This server implements
the <a href="https://solid.github.io/specification/protocol">Solid protocol</a>
so you can create your own <a href="https://solidproject.org/about">Solid Pod</a>
and identity.
</p>
<h2 id="users">Getting started as a <em>user</em></h2>
<p>
<a href="./idp/register/">Sign up for an account</a>
to get started with your own Pod and WebID.
</p>
<p>
The default configuration stores data only in memory.
If you want to keep data permanently,
choose a configuration that saves data to disk instead.
</p>
<p>
To learn more about how this server can be used,
have a look at the
<a href="https://github.com/CommunitySolidServer/tutorials/blob/main/getting-started.md">getting started tutorial</a>.
</p>
<h2 id="developers">Getting started as a <em>developer</em></h2>
<p>
The default configuration includes
the <strong>ready-to-use root Pod</strong> you're currently looking at.
<br>
Besides the provided configurations,
you can also fine-tune your own custom configuration using the
<a href="https://communitysolidserver.github.io/configuration-generator/">configuration generator</a>.
</p>
<p>
You can easily choose any folder on your disk
to expose as the root Pod.
<br>
Use the <code>--help</code> switch to learn more.
</p>
<h2>Have a wonderful Solid experience</h2>
<p>
<strong>Learn more about Solid
at <a href="https://solidproject.org/">solidproject.org</a>.</strong>
</p>
<p>
You are warmly invited
to <a href="https://github.com/CommunitySolidServer/CommunitySolidServer/discussions">share your experiences</a>
and to <a href="https://github.com/CommunitySolidServer/CommunitySolidServer/issues">report any bugs</a> you encounter.
</p>
</main>
<footer>
<p>
©20192023 <a href="https://inrupt.com/">Inrupt Inc.</a>
and <a href="https://www.imec-int.com/">imec</a>
</p>
</footer>
</body>
</html>

View File

@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
import fs from 'fs';
import { PassThrough, Readable } from 'stream';
import { createResponse } from 'node-mocks-http';
import { StaticAssetHandler } from '../../../../src/server/middleware/StaticAssetHandler';
import { StaticAssetEntry, StaticAssetHandler } from '../../../../src/server/middleware/StaticAssetHandler';
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
import type { SystemError } from '../../../../src/util/errors/SystemError';
@ -12,17 +12,19 @@ const createReadStream = jest.spyOn(fs, 'createReadStream')
.mockImplementation((): any => Readable.from([ 'file contents' ]));
describe('A StaticAssetHandler', (): void => {
const handler = new StaticAssetHandler({
'/': '/assets/README.md',
'/foo/bar/style': '/assets/styles/bar.css',
'/foo/bar/main': '/assets/scripts/bar.js',
'/foo/bar/unknown': '/assets/bar.unknown',
'/foo/bar/cwd': 'paths/cwd.txt',
'/foo/bar/module': '@css:paths/module.txt',
'/foo/bar/document/': '/assets/document.txt',
'/foo/bar/folder/': '/assets/folders/1/',
'/foo/bar/folder/subfolder/': '/assets/folders/2/',
}, 'http://localhost:3000');
const assets = [
new StaticAssetEntry('/', '/assets/README.md'),
new StaticAssetEntry('/foo/bar/style', '/assets/styles/bar.css'),
new StaticAssetEntry('/foo/bar/main', '/assets/scripts/bar.js'),
new StaticAssetEntry('/foo/bar/unknown', '/assets/bar.unknown'),
new StaticAssetEntry('/foo/bar/cwd', 'paths/cwd.txt'),
new StaticAssetEntry('/foo/bar/module', '@css:paths/module.txt'),
new StaticAssetEntry('/foo/bar/document/', '/assets/document.txt'),
new StaticAssetEntry('/foo/bar/folder/', '/assets/folders/1/'),
new StaticAssetEntry('/foo/bar/folder/subfolder/', '/assets/folders/2/'),
];
const handler = new StaticAssetHandler(assets, 'http://localhost:3000');
afterEach(jest.clearAllMocks);
@ -177,7 +179,7 @@ describe('A StaticAssetHandler', (): void => {
});
it('requires folders to be linked to URLs ending on a slash.', async(): Promise<void> => {
expect((): StaticAssetHandler => new StaticAssetHandler({ '/foo': '/bar/' }, 'http://example.com/'))
expect((): StaticAssetHandler => new StaticAssetHandler([ new StaticAssetEntry('/foo', '/bar/') ], 'http://example.com/'))
.toThrow(InternalServerError);
});
@ -225,11 +227,11 @@ describe('A StaticAssetHandler', (): void => {
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',
}, 'http://localhost:3000', {
expires: 86400,
});
const cachedHandler = new StaticAssetHandler(
[ new StaticAssetEntry('/foo/bar/style', '/assets/styles/bar.css') ],
'http://localhost:3000',
{ expires: 86400 },
);
const request = { method: 'GET', url: '/foo/bar/style' };
const response = createResponse();
await cachedHandler.handleSafe({ request, response } as any);