mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Create RootInitializer to set up root resources
This commit is contained in:
parent
00f086fa79
commit
c2ad892020
@ -8,6 +8,4 @@ This is the entry point to the main server setup.
|
|||||||
|
|
||||||
## Init
|
## Init
|
||||||
Contains a list of initializer that need to be run when starting the server.
|
Contains a list of initializer that need to be run when starting the server.
|
||||||
For example, when acl authorization is used,
|
* *default*: The default setup that makes sure the root container has the necessary resources.
|
||||||
an initializer will be added that makes sure there is an acl file in the root.
|
|
||||||
* *default*: The default setup that makes sure the root container is marked as pim:storage.
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||||
"import": [
|
"import": [
|
||||||
"files-scs:config/app/init/base/init.json",
|
"files-scs:config/app/init/base/init.json",
|
||||||
"files-scs:config/app/init/initializers/root-container.json"
|
"files-scs:config/app/init/initializers/root.json"
|
||||||
],
|
],
|
||||||
"@graph": [
|
"@graph": [
|
||||||
{
|
{
|
||||||
@ -10,7 +10,7 @@
|
|||||||
"@id": "urn:solid-server:default:ParallelInitializer",
|
"@id": "urn:solid-server:default:ParallelInitializer",
|
||||||
"@type": "ParallelHandler",
|
"@type": "ParallelHandler",
|
||||||
"handlers": [
|
"handlers": [
|
||||||
{ "@id": "urn:solid-server:default:RootContainerInitializer" }
|
{ "@id": "urn:solid-server:default:RootInitializer" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
|
||||||
"@graph": [
|
|
||||||
{
|
|
||||||
"comment": "Makes sure the root container exists and marks it as pim:Storage.",
|
|
||||||
"@id": "urn:solid-server:default:RootContainerInitializer",
|
|
||||||
"@type": "RootContainerInitializer",
|
|
||||||
"settings_store": { "@id": "urn:solid-server:default:ResourceStore" },
|
|
||||||
"settings_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
18
config/app/init/initializers/root.json
Normal file
18
config/app/init/initializers/root.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.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": "RootInitializer",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
|
"store": { "@id": "urn:solid-server:default:ResourceStore" },
|
||||||
|
"generator": {
|
||||||
|
"@type": "TemplatedResourcesGenerator",
|
||||||
|
"templateFolder": "$PACKAGE_ROOT/templates/root",
|
||||||
|
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
||||||
|
"templateEngine": { "@type": "HandlebarsTemplateEngine" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -8,7 +8,7 @@
|
|||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"StaticAssetHandler:_assets_key": "/favicon.ico",
|
"StaticAssetHandler:_assets_key": "/favicon.ico",
|
||||||
"StaticAssetHandler:_assets_value": "$PACKAGE_ROOT/templates/root/favicon.ico"
|
"StaticAssetHandler:_assets_value": "$PACKAGE_ROOT/templates/images/favicon.ico"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"StaticAssetHandler:_assets_key": "/.well_known/css/styles/",
|
"StaticAssetHandler:_assets_key": "/.well_known/css/styles/",
|
||||||
|
@ -17,20 +17,6 @@
|
|||||||
},
|
},
|
||||||
{ "@id": "urn:solid-server:default:WebAclAuthorizer" }
|
{ "@id": "urn:solid-server:default:WebAclAuthorizer" }
|
||||||
]
|
]
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"comment": "Add to the list of initializers.",
|
|
||||||
"@id": "urn:solid-server:default:ParallelInitializer",
|
|
||||||
"ParallelHandler:_handlers": [
|
|
||||||
{
|
|
||||||
"comment": "Makes sure there is a root acl document. This is necessary for acl authorization.",
|
|
||||||
"@type": "AclInitializer",
|
|
||||||
"settings_store": { "@id": "urn:solid-server:default:ResourceStore" },
|
|
||||||
"settings_aclStrategy": { "@id": "urn:solid-server:default:AclStrategy" },
|
|
||||||
"settings_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -66,13 +66,12 @@ export * from './init/final/Finalizable';
|
|||||||
export * from './init/final/ParallelFinalizer';
|
export * from './init/final/ParallelFinalizer';
|
||||||
|
|
||||||
// Init
|
// Init
|
||||||
export * from './init/AclInitializer';
|
|
||||||
export * from './init/App';
|
export * from './init/App';
|
||||||
export * from './init/AppRunner';
|
export * from './init/AppRunner';
|
||||||
export * from './init/ConfigPodInitializer';
|
export * from './init/ConfigPodInitializer';
|
||||||
export * from './init/Initializer';
|
export * from './init/Initializer';
|
||||||
export * from './init/LoggerInitializer';
|
export * from './init/LoggerInitializer';
|
||||||
export * from './init/RootContainerInitializer';
|
export * from './init/RootInitializer';
|
||||||
export * from './init/ServerInitializer';
|
export * from './init/ServerInitializer';
|
||||||
|
|
||||||
// LDP/Authorization
|
// LDP/Authorization
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
import { createReadStream } from 'fs';
|
|
||||||
import type { AuxiliaryIdentifierStrategy } from '../ldp/auxiliary/AuxiliaryIdentifierStrategy';
|
|
||||||
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
|
|
||||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
|
||||||
import type { ResourceStore } from '../storage/ResourceStore';
|
|
||||||
import { TEXT_TURTLE } from '../util/ContentTypes';
|
|
||||||
import { createErrorMessage } from '../util/errors/ErrorUtil';
|
|
||||||
import { InternalServerError } from '../util/errors/InternalServerError';
|
|
||||||
import { ensureTrailingSlash, joinFilePath } from '../util/PathUtil';
|
|
||||||
import { Initializer } from './Initializer';
|
|
||||||
|
|
||||||
const DEFAULT_ACL_PATH = joinFilePath(__dirname, '../../templates/root/.acl');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensures that a root ACL is present.
|
|
||||||
*/
|
|
||||||
export class AclInitializer extends Initializer {
|
|
||||||
protected readonly logger = getLoggerFor(this);
|
|
||||||
private readonly store: ResourceStore;
|
|
||||||
private readonly aclStrategy: AuxiliaryIdentifierStrategy;
|
|
||||||
private readonly root: ResourceIdentifier;
|
|
||||||
private readonly aclPath: string;
|
|
||||||
|
|
||||||
public constructor(settings: {
|
|
||||||
store: ResourceStore;
|
|
||||||
aclStrategy: AuxiliaryIdentifierStrategy;
|
|
||||||
baseUrl: string;
|
|
||||||
aclPath?: string;
|
|
||||||
}) {
|
|
||||||
super();
|
|
||||||
this.store = settings.store;
|
|
||||||
this.aclStrategy = settings.aclStrategy;
|
|
||||||
this.root = { path: ensureTrailingSlash(settings.baseUrl) };
|
|
||||||
this.aclPath = settings.aclPath ?? DEFAULT_ACL_PATH;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Solid, §4.1: "The root container (pim:Storage) MUST have an ACL auxiliary resource directly associated to it.
|
|
||||||
// The associated ACL document MUST include an authorization policy with acl:Control access privilege."
|
|
||||||
// https://solid.github.io/specification/protocol#storage
|
|
||||||
public async handle(): Promise<void> {
|
|
||||||
const rootAcl = this.aclStrategy.getAuxiliaryIdentifier(this.root);
|
|
||||||
if (await this.store.resourceExists(rootAcl)) {
|
|
||||||
this.logger.debug(`Existing root ACL document found at ${rootAcl.path}`);
|
|
||||||
} else {
|
|
||||||
this.logger.debug(`Installing root ACL document at ${rootAcl.path}`);
|
|
||||||
const aclDocument = createReadStream(this.aclPath, 'utf8');
|
|
||||||
try {
|
|
||||||
await this.store.setRepresentation(rootAcl, new BasicRepresentation(aclDocument, rootAcl, TEXT_TURTLE));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const message = `Issue initializing the root ACL resource: ${createErrorMessage(error)}`;
|
|
||||||
this.logger.error(message);
|
|
||||||
throw new InternalServerError(message, { cause: error });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
|
|
||||||
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
|
|
||||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
|
||||||
import type { ResourceStore } from '../storage/ResourceStore';
|
|
||||||
import { TEXT_TURTLE } from '../util/ContentTypes';
|
|
||||||
import { ensureTrailingSlash } from '../util/PathUtil';
|
|
||||||
import { addResourceMetadata } from '../util/ResourceUtil';
|
|
||||||
import { PIM, RDF } from '../util/Vocabularies';
|
|
||||||
import { Initializer } from './Initializer';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes ResourceStores by creating a root container if it didn't exist yet.
|
|
||||||
*
|
|
||||||
* Solid, §4.1: "When a server supports a data pod, it MUST provide one or more storages (pim:Storage) –
|
|
||||||
* a space of URIs in which data can be accessed. A storage is the root container for all of its contained resources."
|
|
||||||
* https://solid.github.io/specification/protocol#storage
|
|
||||||
*/
|
|
||||||
export class RootContainerInitializer extends Initializer {
|
|
||||||
protected readonly logger = getLoggerFor(this);
|
|
||||||
private readonly store: ResourceStore;
|
|
||||||
private readonly baseId: ResourceIdentifier;
|
|
||||||
|
|
||||||
public constructor(settings: { store: ResourceStore; baseUrl: string }) {
|
|
||||||
super();
|
|
||||||
this.store = settings.store;
|
|
||||||
this.baseId = { path: ensureTrailingSlash(settings.baseUrl) };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handle(): Promise<void> {
|
|
||||||
this.logger.debug(`Checking for root container at ${this.baseId.path}`);
|
|
||||||
if (!await this.store.resourceExists(this.baseId)) {
|
|
||||||
await this.createRootContainer();
|
|
||||||
} else {
|
|
||||||
this.logger.debug(`Existing root container found at ${this.baseId.path}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a root container in a ResourceStore.
|
|
||||||
*/
|
|
||||||
protected async createRootContainer(): Promise<void> {
|
|
||||||
const metadata = new RepresentationMetadata(this.baseId, TEXT_TURTLE);
|
|
||||||
addResourceMetadata(metadata, true);
|
|
||||||
|
|
||||||
// Make sure the root container is a pim:Storage
|
|
||||||
// This prevents deletion of the root container as storage root containers can not be deleted
|
|
||||||
// Solid, §4.1: "Servers exposing the storage resource MUST advertise by including the HTTP Link header
|
|
||||||
// with rel="type" targeting http://www.w3.org/ns/pim/space#Storage when responding to storage’s request URI."
|
|
||||||
// https://solid.github.io/specification/protocol#storage
|
|
||||||
metadata.add(RDF.type, PIM.terms.Storage);
|
|
||||||
|
|
||||||
this.logger.debug(`Creating root container at ${this.baseId.path}`);
|
|
||||||
await this.store.setRepresentation(this.baseId, new BasicRepresentation([], metadata));
|
|
||||||
}
|
|
||||||
}
|
|
73
src/init/RootInitializer.ts
Normal file
73
src/init/RootInitializer.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||||
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
|
import type { ResourcesGenerator } from '../pods/generate/ResourcesGenerator';
|
||||||
|
import type { ResourceStore } from '../storage/ResourceStore';
|
||||||
|
import { createErrorMessage } from '../util/errors/ErrorUtil';
|
||||||
|
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
||||||
|
import { ensureTrailingSlash } from '../util/PathUtil';
|
||||||
|
import { PIM, RDF } from '../util/Vocabularies';
|
||||||
|
import { Initializer } from './Initializer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializer that sets up the root container.
|
||||||
|
* Will copy all the files and folders in the given path to the corresponding documents and containers.
|
||||||
|
* This will always happen when the server starts unless the following 2 conditions are both fulfilled:
|
||||||
|
* * The container already exists.
|
||||||
|
* * The container has metadata indicating it is a pim:Storage.
|
||||||
|
*
|
||||||
|
* It is important that the ResourcesGenerator generates a `<> a pim:Storage` triple for the root container:
|
||||||
|
* this prevents deletion of the root container as storage root containers can not be deleted.
|
||||||
|
* Solid, §4.1: "Servers exposing the storage resource MUST advertise by including the HTTP Link header
|
||||||
|
* with rel="type" targeting http://www.w3.org/ns/pim/space#Storage when responding to storage’s request URI."
|
||||||
|
* https://solid.github.io/specification/protocol#storage
|
||||||
|
*/
|
||||||
|
export class RootInitializer extends Initializer {
|
||||||
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
|
private readonly store: ResourceStore;
|
||||||
|
private readonly baseId: ResourceIdentifier;
|
||||||
|
private readonly generator: ResourcesGenerator;
|
||||||
|
|
||||||
|
public constructor(baseUrl: string, store: ResourceStore, generator: ResourcesGenerator) {
|
||||||
|
super();
|
||||||
|
this.baseId = { path: ensureTrailingSlash(baseUrl) };
|
||||||
|
this.store = store;
|
||||||
|
this.generator = generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handle(): Promise<void> {
|
||||||
|
this.logger.debug(`Checking for valid root container at ${this.baseId.path}`);
|
||||||
|
if (!await this.rootContainerIsValid()) {
|
||||||
|
this.logger.info(`Root container not found; initializing it.`);
|
||||||
|
const resources = this.generator.generate(this.baseId, {});
|
||||||
|
let count = 0;
|
||||||
|
for await (const { identifier: resourceId, representation } of resources) {
|
||||||
|
try {
|
||||||
|
await this.store.setRepresentation(resourceId, representation);
|
||||||
|
count += 1;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.warn(`Failed to create resource ${resourceId.path}: ${createErrorMessage(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.info(`Initialized root container with ${count} resources.`);
|
||||||
|
} else {
|
||||||
|
this.logger.debug(`Valid root container found at ${this.baseId.path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies if the root container already exists and has the pim:Storage type.
|
||||||
|
*/
|
||||||
|
private async rootContainerIsValid(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const representation = await this.store.getRepresentation(this.baseId, {});
|
||||||
|
representation.data.destroy();
|
||||||
|
return representation.metadata.getAll(RDF.terms.type).some((term): boolean => term.equals(PIM.terms.Storage));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (NotFoundHttpError.isInstance(error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
|
||||||
import type { ResourceStore } from '../../storage/ResourceStore';
|
import type { ResourceStore } from '../../storage/ResourceStore';
|
||||||
import type { PodSettings } from '../settings/PodSettings';
|
|
||||||
import type { ResourcesGenerator } from './ResourcesGenerator';
|
import type { ResourcesGenerator } from './ResourcesGenerator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -12,7 +11,7 @@ import type { ResourcesGenerator } from './ResourcesGenerator';
|
|||||||
*
|
*
|
||||||
* @returns The amount of resources that were added.
|
* @returns The amount of resources that were added.
|
||||||
*/
|
*/
|
||||||
export async function addGeneratedResources(identifier: ResourceIdentifier, settings: PodSettings,
|
export async function addGeneratedResources(identifier: ResourceIdentifier, settings: NodeJS.Dict<string>,
|
||||||
generator: ResourcesGenerator, store: ResourceStore): Promise<number> {
|
generator: ResourcesGenerator, store: ResourceStore): Promise<number> {
|
||||||
const resources = generator.generate(identifier, settings);
|
const resources = generator.generate(identifier, settings);
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
@ -9,6 +9,7 @@ import type {
|
|||||||
FileIdentifierMapperFactory,
|
FileIdentifierMapperFactory,
|
||||||
ResourceLink,
|
ResourceLink,
|
||||||
} from '../../storage/mapping/FileIdentifierMapper';
|
} from '../../storage/mapping/FileIdentifierMapper';
|
||||||
|
import { TEXT_TURTLE } from '../../util/ContentTypes';
|
||||||
import { guardStream } from '../../util/GuardedStream';
|
import { guardStream } from '../../util/GuardedStream';
|
||||||
import type { Guarded } from '../../util/GuardedStream';
|
import type { Guarded } from '../../util/GuardedStream';
|
||||||
import { joinFilePath, isContainerIdentifier, resolveAssetPath } from '../../util/PathUtil';
|
import { joinFilePath, isContainerIdentifier, resolveAssetPath } from '../../util/PathUtil';
|
||||||
@ -130,7 +131,10 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
|
|||||||
const metadata = new RepresentationMetadata(link.identifier);
|
const metadata = new RepresentationMetadata(link.identifier);
|
||||||
|
|
||||||
// Read file if it is not a container
|
// Read file if it is not a container
|
||||||
if (!isContainerIdentifier(link.identifier)) {
|
if (isContainerIdentifier(link.identifier)) {
|
||||||
|
// Containers need to be an RDF type
|
||||||
|
metadata.contentType = TEXT_TURTLE;
|
||||||
|
} else {
|
||||||
data = await this.processFile(link, options);
|
data = await this.processFile(link, options);
|
||||||
metadata.contentType = link.contentType;
|
metadata.contentType = link.contentType;
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
7
templates/root/.meta
Normal file
7
templates/root/.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
@prefix pim: <http://www.w3.org/ns/pim/space#>.
|
||||||
|
|
||||||
|
# It is imperative the root container is marked as a pim:Storage :
|
||||||
|
# Solid, §4.1: "Servers exposing the storage resource MUST advertise by including the HTTP Link header
|
||||||
|
# with rel="type" targeting http://www.w3.org/ns/pim/space#Storage when responding to storage’s request URI."
|
||||||
|
# https://solid.github.io/specification/protocol#storage
|
||||||
|
<> a pim:Storage.
|
@ -58,12 +58,10 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
|
|||||||
await app.stop();
|
await app.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can read a container listing.', async(): Promise<void> => {
|
it('can read the root container index page.', async(): Promise<void> => {
|
||||||
const response = await getResource(baseUrl);
|
const response = await getResource(baseUrl, { contentType: 'text/html' });
|
||||||
|
|
||||||
await expectQuads(response, [
|
await expect(response.text()).resolves.toContain('Welcome to the Community Solid Server');
|
||||||
quad(namedNode(baseUrl), RDF.terms.type, LDP.terms.Container),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// This is only here because we're accessing the root container
|
// This is only here because we're accessing the root container
|
||||||
expect(response.headers.get('link')).toContain(`<${PIM.Storage}>; rel="type"`);
|
expect(response.headers.get('link')).toContain(`<${PIM.Storage}>; rel="type"`);
|
||||||
@ -71,7 +69,7 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
|
|||||||
|
|
||||||
it('can read a container listing with a query string.', async(): Promise<void> => {
|
it('can read a container listing with a query string.', async(): Promise<void> => {
|
||||||
// Helper functions would fail due to query params
|
// Helper functions would fail due to query params
|
||||||
const response = await fetch(`${baseUrl}?abc=def&xyz`);
|
const response = await fetch(`${baseUrl}?abc=def&xyz`, { headers: { accept: 'text/turtle' }});
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.headers.get('content-type')).toBe('text/turtle');
|
expect(response.headers.get('content-type')).toBe('text/turtle');
|
||||||
expect(response.headers.get('link')).toContain(`<${LDP.Container}>; rel="type"`);
|
expect(response.headers.get('link')).toContain(`<${LDP.Container}>; rel="type"`);
|
||||||
@ -124,7 +122,12 @@ describe.each(stores)('An LDP handler allowing all requests %s', (name, { storeC
|
|||||||
await putResource(containerUrl, { contentType: 'text/turtle' });
|
await putResource(containerUrl, { contentType: 'text/turtle' });
|
||||||
|
|
||||||
// GET
|
// GET
|
||||||
await getResource(containerUrl);
|
const response = await getResource(containerUrl);
|
||||||
|
|
||||||
|
// Verify container listing
|
||||||
|
await expectQuads(response, [
|
||||||
|
quad(namedNode(containerUrl), RDF.terms.type, LDP.terms.Container),
|
||||||
|
]);
|
||||||
|
|
||||||
// DELETE
|
// DELETE
|
||||||
expect(await deleteResource(containerUrl)).toBeUndefined();
|
expect(await deleteResource(containerUrl)).toBeUndefined();
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { RootContainerInitializer } from '../../src/init/RootContainerInitializer';
|
|
||||||
import { RoutingAuxiliaryStrategy } from '../../src/ldp/auxiliary/RoutingAuxiliaryStrategy';
|
import { RoutingAuxiliaryStrategy } from '../../src/ldp/auxiliary/RoutingAuxiliaryStrategy';
|
||||||
import { BasicRepresentation } from '../../src/ldp/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../src/ldp/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../src/ldp/representation/Representation';
|
import type { Representation } from '../../src/ldp/representation/Representation';
|
||||||
|
import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
|
||||||
import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor';
|
import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor';
|
||||||
import { DataAccessorBasedStore } from '../../src/storage/DataAccessorBasedStore';
|
import { DataAccessorBasedStore } from '../../src/storage/DataAccessorBasedStore';
|
||||||
import { LockingResourceStore } from '../../src/storage/LockingResourceStore';
|
import { LockingResourceStore } from '../../src/storage/LockingResourceStore';
|
||||||
import type { ResourceStore } from '../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../src/storage/ResourceStore';
|
||||||
import { APPLICATION_OCTET_STREAM } from '../../src/util/ContentTypes';
|
import { APPLICATION_OCTET_STREAM, TEXT_TURTLE } from '../../src/util/ContentTypes';
|
||||||
import { InternalServerError } from '../../src/util/errors/InternalServerError';
|
import { InternalServerError } from '../../src/util/errors/InternalServerError';
|
||||||
import { SingleRootIdentifierStrategy } from '../../src/util/identifiers/SingleRootIdentifierStrategy';
|
import { SingleRootIdentifierStrategy } from '../../src/util/identifiers/SingleRootIdentifierStrategy';
|
||||||
import { EqualReadWriteLocker } from '../../src/util/locking/EqualReadWriteLocker';
|
import { EqualReadWriteLocker } from '../../src/util/locking/EqualReadWriteLocker';
|
||||||
@ -15,6 +15,7 @@ import type { ReadWriteLocker } from '../../src/util/locking/ReadWriteLocker';
|
|||||||
import { SingleThreadedResourceLocker } from '../../src/util/locking/SingleThreadedResourceLocker';
|
import { SingleThreadedResourceLocker } from '../../src/util/locking/SingleThreadedResourceLocker';
|
||||||
import { WrappedExpiringReadWriteLocker } from '../../src/util/locking/WrappedExpiringReadWriteLocker';
|
import { WrappedExpiringReadWriteLocker } from '../../src/util/locking/WrappedExpiringReadWriteLocker';
|
||||||
import { guardedStreamFrom } from '../../src/util/StreamUtil';
|
import { guardedStreamFrom } from '../../src/util/StreamUtil';
|
||||||
|
import { PIM, RDF } from '../../src/util/Vocabularies';
|
||||||
jest.useFakeTimers('legacy');
|
jest.useFakeTimers('legacy');
|
||||||
|
|
||||||
describe('A LockingResourceStore', (): void => {
|
describe('A LockingResourceStore', (): void => {
|
||||||
@ -41,8 +42,9 @@ describe('A LockingResourceStore', (): void => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Initialize store
|
// Initialize store
|
||||||
const initializer = new RootContainerInitializer({ store: source, baseUrl: base });
|
const metadata = new RepresentationMetadata({ path: base }, TEXT_TURTLE);
|
||||||
await initializer.handleSafe();
|
metadata.add(RDF.type, PIM.terms.Storage);
|
||||||
|
await source.setRepresentation({ path: base }, new BasicRepresentation([], metadata));
|
||||||
|
|
||||||
locker = new EqualReadWriteLocker(new SingleThreadedResourceLocker());
|
locker = new EqualReadWriteLocker(new SingleThreadedResourceLocker());
|
||||||
expiringLocker = new WrappedExpiringReadWriteLocker(locker, 1000);
|
expiringLocker = new WrappedExpiringReadWriteLocker(locker, 1000);
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import { AclInitializer } from '../../../src/init/AclInitializer';
|
|
||||||
import type { AuxiliaryIdentifierStrategy } from '../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
|
|
||||||
import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation';
|
|
||||||
import type { ResourceStore } from '../../../src/storage/ResourceStore';
|
|
||||||
import { InternalServerError } from '../../../src/util/errors/InternalServerError';
|
|
||||||
import { joinFilePath } from '../../../src/util/PathUtil';
|
|
||||||
|
|
||||||
const createReadStream = jest.spyOn(fs, 'createReadStream').mockReturnValue('file contents' as any);
|
|
||||||
|
|
||||||
jest.mock('../../../src/ldp/representation/BasicRepresentation');
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
const RepresentationMock: jest.Mock<BasicRepresentation> = BasicRepresentation as any;
|
|
||||||
|
|
||||||
describe('AclInitializer', (): void => {
|
|
||||||
const store: jest.Mocked<ResourceStore> = {
|
|
||||||
setRepresentation: jest.fn(),
|
|
||||||
resourceExists: jest.fn().mockImplementation((): any => false),
|
|
||||||
} as any;
|
|
||||||
const aclIdentifier = { path: 'http://test.com/.acl' };
|
|
||||||
const aclStrategy: jest.Mocked<AuxiliaryIdentifierStrategy> = {
|
|
||||||
getAuxiliaryIdentifier: jest.fn().mockReturnValue(aclIdentifier),
|
|
||||||
} as any;
|
|
||||||
const baseUrl = 'http://localhost:3000/';
|
|
||||||
|
|
||||||
afterEach((): void => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets the default ACL when none exists already.', async(): Promise<void> => {
|
|
||||||
const initializer = new AclInitializer({ baseUrl, store, aclStrategy });
|
|
||||||
await initializer.handle();
|
|
||||||
|
|
||||||
expect(aclStrategy.getAuxiliaryIdentifier).toHaveBeenCalledWith({ path: baseUrl });
|
|
||||||
expect(store.resourceExists).toHaveBeenCalledTimes(1);
|
|
||||||
expect(store.resourceExists).toHaveBeenCalledWith(aclIdentifier);
|
|
||||||
expect(store.setRepresentation).toHaveBeenCalledTimes(1);
|
|
||||||
expect(store.setRepresentation).toHaveBeenCalledWith(
|
|
||||||
{ path: 'http://test.com/.acl' }, RepresentationMock.mock.instances[0],
|
|
||||||
);
|
|
||||||
expect(createReadStream).toHaveBeenCalledTimes(1);
|
|
||||||
expect(createReadStream).toHaveBeenCalledWith(joinFilePath(__dirname, '../../../templates/root/.acl'), 'utf8');
|
|
||||||
expect(RepresentationMock).toHaveBeenCalledWith('file contents', aclIdentifier, 'text/turtle');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets the specific ACL when one was specified.', async(): Promise<void> => {
|
|
||||||
const initializer = new AclInitializer({ baseUrl, store, aclStrategy, aclPath: '/path/doc.acl' });
|
|
||||||
await initializer.handle();
|
|
||||||
|
|
||||||
expect(aclStrategy.getAuxiliaryIdentifier).toHaveBeenCalledWith({ path: baseUrl });
|
|
||||||
expect(store.resourceExists).toHaveBeenCalledTimes(1);
|
|
||||||
expect(store.resourceExists).toHaveBeenCalledWith(aclIdentifier);
|
|
||||||
expect(store.setRepresentation).toHaveBeenCalledTimes(1);
|
|
||||||
expect(store.setRepresentation).toHaveBeenCalledWith(
|
|
||||||
{ path: 'http://test.com/.acl' }, RepresentationMock.mock.instances[0],
|
|
||||||
);
|
|
||||||
expect(createReadStream).toHaveBeenCalledTimes(1);
|
|
||||||
expect(createReadStream).toHaveBeenCalledWith('/path/doc.acl', 'utf8');
|
|
||||||
expect(RepresentationMock).toHaveBeenCalledWith('file contents', aclIdentifier, 'text/turtle');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not invoke ACL initialization when a root ACL already exists.', async(): Promise<void> => {
|
|
||||||
store.resourceExists.mockResolvedValueOnce(true);
|
|
||||||
|
|
||||||
const initializer = new AclInitializer({ baseUrl, store, aclStrategy });
|
|
||||||
await initializer.handle();
|
|
||||||
|
|
||||||
expect(aclStrategy.getAuxiliaryIdentifier).toHaveBeenCalledWith({ path: baseUrl });
|
|
||||||
expect(store.resourceExists).toHaveBeenCalledTimes(1);
|
|
||||||
expect(store.resourceExists).toHaveBeenCalledWith(aclIdentifier);
|
|
||||||
expect(store.setRepresentation).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors when the root ACL check errors.', async(): Promise<void> => {
|
|
||||||
store.setRepresentation.mockRejectedValueOnce(new Error('Fatal'));
|
|
||||||
|
|
||||||
const initializer = new AclInitializer({ baseUrl, store, aclStrategy });
|
|
||||||
const prom = initializer.handle();
|
|
||||||
await expect(prom).rejects.toThrow('Issue initializing the root ACL resource: Fatal');
|
|
||||||
await expect(prom).rejects.toThrow(InternalServerError);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,44 +0,0 @@
|
|||||||
import { RootContainerInitializer } from '../../../src/init/RootContainerInitializer';
|
|
||||||
import type { ResourceStore } from '../../../src/storage/ResourceStore';
|
|
||||||
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
|
|
||||||
|
|
||||||
describe('A RootContainerInitializer', (): void => {
|
|
||||||
const baseUrl = 'http://test.com/';
|
|
||||||
const store: jest.Mocked<ResourceStore> = {
|
|
||||||
getRepresentation: jest.fn().mockRejectedValue(new NotFoundHttpError()),
|
|
||||||
setRepresentation: jest.fn(),
|
|
||||||
resourceExists: jest.fn(),
|
|
||||||
} as any;
|
|
||||||
const initializer = new RootContainerInitializer({ store, baseUrl });
|
|
||||||
|
|
||||||
afterEach((): void => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('invokes ResourceStore initialization.', async(): Promise<void> => {
|
|
||||||
store.resourceExists.mockResolvedValueOnce(false);
|
|
||||||
await initializer.handle();
|
|
||||||
|
|
||||||
expect(store.resourceExists).toHaveBeenCalledTimes(1);
|
|
||||||
expect(store.resourceExists).toHaveBeenCalledWith({ path: baseUrl });
|
|
||||||
expect(store.setRepresentation).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not invoke ResourceStore initialization when a root container already exists.', async(): Promise<void> => {
|
|
||||||
store.resourceExists.mockResolvedValueOnce(true);
|
|
||||||
store.getRepresentation.mockReturnValueOnce(Promise.resolve({
|
|
||||||
data: { destroy: jest.fn() },
|
|
||||||
} as any));
|
|
||||||
|
|
||||||
await initializer.handle();
|
|
||||||
|
|
||||||
expect(store.resourceExists).toHaveBeenCalledTimes(1);
|
|
||||||
expect(store.resourceExists).toHaveBeenCalledWith({ path: 'http://test.com/' });
|
|
||||||
expect(store.setRepresentation).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors when the store errors writing the root container.', async(): Promise<void> => {
|
|
||||||
store.resourceExists.mockRejectedValueOnce(new Error('Fatal'));
|
|
||||||
await expect(initializer.handle()).rejects.toThrow('Fatal');
|
|
||||||
});
|
|
||||||
});
|
|
82
test/unit/init/RootInitializer.test.ts
Normal file
82
test/unit/init/RootInitializer.test.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { RootInitializer } from '../../../src/init/RootInitializer';
|
||||||
|
import { BasicRepresentation } from '../../../src/ldp/representation/BasicRepresentation';
|
||||||
|
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
|
||||||
|
import type { Logger } from '../../../src/logging/Logger';
|
||||||
|
import { getLoggerFor } from '../../../src/logging/LogUtil';
|
||||||
|
import type { Resource, ResourcesGenerator } from '../../../src/pods/generate/ResourcesGenerator';
|
||||||
|
import type { ResourceStore } from '../../../src/storage/ResourceStore';
|
||||||
|
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
|
||||||
|
import { PIM, RDF } from '../../../src/util/Vocabularies';
|
||||||
|
|
||||||
|
jest.mock('../../../src/logging/LogUtil', (): any => {
|
||||||
|
const logger: Logger = { warn: jest.fn(), debug: jest.fn(), info: jest.fn() } as any;
|
||||||
|
return { getLoggerFor: (): Logger => logger };
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('A RootInitializer', (): void => {
|
||||||
|
const baseUrl = 'http://test.com/foo/';
|
||||||
|
let store: jest.Mocked<ResourceStore>;
|
||||||
|
let generatorData: Resource[];
|
||||||
|
let generator: jest.Mocked<ResourcesGenerator>;
|
||||||
|
let initializer: RootInitializer;
|
||||||
|
let logger: jest.Mocked<Logger>;
|
||||||
|
|
||||||
|
beforeEach(async(): Promise<void> => {
|
||||||
|
store = {
|
||||||
|
getRepresentation: jest.fn().mockRejectedValue(new NotFoundHttpError()),
|
||||||
|
setRepresentation: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
generatorData = [
|
||||||
|
{ identifier: { path: '/.acl' }, representation: '/.acl' as any },
|
||||||
|
{ identifier: { path: '/container/' }, representation: '/container/' as any },
|
||||||
|
];
|
||||||
|
generator = {
|
||||||
|
generate: jest.fn(async function* (): any {
|
||||||
|
yield* generatorData;
|
||||||
|
}),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
initializer = new RootInitializer(baseUrl, store, generator);
|
||||||
|
logger = getLoggerFor(initializer) as any;
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing is the root container already has pim:Storage metadata.', async(): Promise<void> => {
|
||||||
|
const metadata = new RepresentationMetadata({ path: baseUrl }, { [RDF.type]: PIM.terms.Storage });
|
||||||
|
store.getRepresentation.mockResolvedValueOnce(new BasicRepresentation('data', metadata));
|
||||||
|
|
||||||
|
await expect(initializer.handle()).resolves.toBeUndefined();
|
||||||
|
expect(generator.generate).toHaveBeenCalledTimes(0);
|
||||||
|
expect(store.setRepresentation).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes new resources if the container does not exist yet.', async(): Promise<void> => {
|
||||||
|
await expect(initializer.handle()).resolves.toBeUndefined();
|
||||||
|
expect(generator.generate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(store.setRepresentation).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes new resources if the container is not a pim:Storage.', async(): Promise<void> => {
|
||||||
|
store.getRepresentation.mockResolvedValueOnce(new BasicRepresentation('data', 'text/string'));
|
||||||
|
|
||||||
|
await expect(initializer.handle()).resolves.toBeUndefined();
|
||||||
|
expect(generator.generate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(store.setRepresentation).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if there is a problem accessing the root container.', async(): Promise<void> => {
|
||||||
|
store.getRepresentation.mockRejectedValueOnce(new Error('bad data'));
|
||||||
|
await expect(initializer.handle()).rejects.toThrow('bad data');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs warnings if there was a problem creating a resource.', async(): Promise<void> => {
|
||||||
|
store.setRepresentation.mockRejectedValueOnce(new Error('bad input'));
|
||||||
|
|
||||||
|
await expect(initializer.handle()).resolves.toBeUndefined();
|
||||||
|
expect(generator.generate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(store.setRepresentation).toHaveBeenCalledTimes(2);
|
||||||
|
expect(logger.warn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(logger.warn).toHaveBeenLastCalledWith('Failed to create resource /.acl: bad input');
|
||||||
|
});
|
||||||
|
});
|
@ -111,16 +111,18 @@ describe('A TemplatedResourcesGenerator', (): void => {
|
|||||||
{ path: `${location.path}container/` },
|
{ path: `${location.path}container/` },
|
||||||
{ path: `${location.path}container/template` },
|
{ path: `${location.path}container/template` },
|
||||||
]);
|
]);
|
||||||
// Root has the 1 raw metadata triple (with <> changed to its identifier)
|
// Root has the 1 raw metadata triple (with <> changed to its identifier) and content-type
|
||||||
const rootMetadata = result[0].representation.metadata;
|
const rootMetadata = result[0].representation.metadata;
|
||||||
expect(rootMetadata.identifier.value).toBe(location.path);
|
expect(rootMetadata.identifier.value).toBe(location.path);
|
||||||
expect(rootMetadata.quads()).toHaveLength(1);
|
expect(rootMetadata.quads()).toHaveLength(2);
|
||||||
expect(rootMetadata.get('pre:has')?.value).toBe('metadata');
|
expect(rootMetadata.get('pre:has')?.value).toBe('metadata');
|
||||||
|
expect(rootMetadata.contentType).toBe('text/turtle');
|
||||||
|
|
||||||
// Container has no metadata triples
|
// Container has no metadata triples besides content-type
|
||||||
const contMetadata = result[1].representation.metadata;
|
const contMetadata = result[1].representation.metadata;
|
||||||
expect(contMetadata.identifier.value).toBe(`${location.path}container/`);
|
expect(contMetadata.identifier.value).toBe(`${location.path}container/`);
|
||||||
expect(contMetadata.quads()).toHaveLength(0);
|
expect(contMetadata.quads()).toHaveLength(1);
|
||||||
|
expect(contMetadata.contentType).toBe('text/turtle');
|
||||||
|
|
||||||
// Document has the 1 raw metadata triple (with <> changed to its identifier) and content-type
|
// Document has the 1 raw metadata triple (with <> changed to its identifier) and content-type
|
||||||
const docMetadata = result[2].representation.metadata;
|
const docMetadata = result[2].representation.metadata;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user