mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Generalize RootInitializer to ContainerInitializer
The initializer can now be used for any container. The initializer also does not handle the repeat check anymore, this is now configured with a ConditionalHandler.
This commit is contained in:
parent
bb7e88b137
commit
9968f2ae5b
@ -4,14 +4,23 @@
|
|||||||
{
|
{
|
||||||
"comment": "Makes sure the root container exists and contains the necessary resources.",
|
"comment": "Makes sure the root container exists and contains the necessary resources.",
|
||||||
"@id": "urn:solid-server:default:RootInitializer",
|
"@id": "urn:solid-server:default:RootInitializer",
|
||||||
"@type": "RootInitializer",
|
"@type": "ConditionalHandler",
|
||||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
"storageKey": "rootInitialized",
|
||||||
"store": { "@id": "urn:solid-server:default:ResourceStore" },
|
"storageValue": true,
|
||||||
"generator": {
|
"storage": { "@id": "urn:solid-server:default:SetupStorage" },
|
||||||
"@type": "TemplatedResourcesGenerator",
|
"source": {
|
||||||
"templateFolder": "@css:templates/root/prefilled",
|
"@type": "ContainerInitializer",
|
||||||
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
"templateEngine": { "@type": "HandlebarsTemplateEngine" }
|
"args_path": "/",
|
||||||
|
"args_store": { "@id": "urn:solid-server:default:ResourceStore" },
|
||||||
|
"args_generator": {
|
||||||
|
"@type": "TemplatedResourcesGenerator",
|
||||||
|
"templateFolder": "@css:templates/root/prefilled",
|
||||||
|
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
||||||
|
"templateEngine": { "@type": "HandlebarsTemplateEngine" }
|
||||||
|
},
|
||||||
|
"args_storageKey": "rootInitialized",
|
||||||
|
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -4,14 +4,23 @@
|
|||||||
{
|
{
|
||||||
"comment": "Makes sure the root container exists and contains the necessary resources.",
|
"comment": "Makes sure the root container exists and contains the necessary resources.",
|
||||||
"@id": "urn:solid-server:default:RootInitializer",
|
"@id": "urn:solid-server:default:RootInitializer",
|
||||||
"@type": "RootInitializer",
|
"@type": "ConditionalHandler",
|
||||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
"storageKey": "rootInitialized",
|
||||||
"store": { "@id": "urn:solid-server:default:ResourceStore" },
|
"storageValue": true,
|
||||||
"generator": {
|
"storage": { "@id": "urn:solid-server:default:SetupStorage" },
|
||||||
"@type": "TemplatedResourcesGenerator",
|
"source": {
|
||||||
"templateFolder": "@css:templates/root/empty",
|
"@type": "ContainerInitializer",
|
||||||
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
"templateEngine": { "@type": "HandlebarsTemplateEngine" }
|
"args_path": "/",
|
||||||
|
"args_store": { "@id": "urn:solid-server:default:ResourceStore" },
|
||||||
|
"args_generator": {
|
||||||
|
"@type": "TemplatedResourcesGenerator",
|
||||||
|
"templateFolder": "@css:templates/root/empty",
|
||||||
|
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
||||||
|
"templateEngine": { "@type": "HandlebarsTemplateEngine" }
|
||||||
|
},
|
||||||
|
"args_storageKey": "rootInitialized",
|
||||||
|
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -82,9 +82,9 @@ export * from './init/setup/SetupHttpHandler';
|
|||||||
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/ContainerInitializer';
|
||||||
export * from './init/Initializer';
|
export * from './init/Initializer';
|
||||||
export * from './init/LoggerInitializer';
|
export * from './init/LoggerInitializer';
|
||||||
export * from './init/RootInitializer';
|
|
||||||
export * from './init/ServerInitializer';
|
export * from './init/ServerInitializer';
|
||||||
|
|
||||||
// LDP/Authorization
|
// LDP/Authorization
|
||||||
|
76
src/init/ContainerInitializer.ts
Normal file
76
src/init/ContainerInitializer.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||||
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
|
import type { ResourcesGenerator } from '../pods/generate/ResourcesGenerator';
|
||||||
|
import type { KeyValueStorage } from '../storage/keyvalue/KeyValueStorage';
|
||||||
|
import type { ResourceStore } from '../storage/ResourceStore';
|
||||||
|
import { createErrorMessage } from '../util/errors/ErrorUtil';
|
||||||
|
import { ensureTrailingSlash, joinUrl } from '../util/PathUtil';
|
||||||
|
import { Initializer } from './Initializer';
|
||||||
|
|
||||||
|
export interface ContainerInitializerArgs {
|
||||||
|
/**
|
||||||
|
* Base URL of the server.
|
||||||
|
*/
|
||||||
|
baseUrl: string;
|
||||||
|
/**
|
||||||
|
* Relative path of the container.
|
||||||
|
*/
|
||||||
|
path: string;
|
||||||
|
/**
|
||||||
|
* ResourceStore where the container should be stored.
|
||||||
|
*/
|
||||||
|
store: ResourceStore;
|
||||||
|
/**
|
||||||
|
* Generator that should be used to generate container contents.
|
||||||
|
*/
|
||||||
|
generator: ResourcesGenerator;
|
||||||
|
/**
|
||||||
|
* Key that is used to store the boolean in the storage indicating the container is initialized.
|
||||||
|
*/
|
||||||
|
storageKey: string;
|
||||||
|
/**
|
||||||
|
* Used to store initialization status.
|
||||||
|
*/
|
||||||
|
storage: KeyValueStorage<string, boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializer that sets up a container.
|
||||||
|
* Will copy all the files and folders in the given path to the corresponding documents and containers.
|
||||||
|
*/
|
||||||
|
export class ContainerInitializer extends Initializer {
|
||||||
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
|
private readonly store: ResourceStore;
|
||||||
|
private readonly containerId: ResourceIdentifier;
|
||||||
|
private readonly generator: ResourcesGenerator;
|
||||||
|
private readonly storageKey: string;
|
||||||
|
private readonly storage: KeyValueStorage<string, boolean>;
|
||||||
|
|
||||||
|
public constructor(args: ContainerInitializerArgs) {
|
||||||
|
super();
|
||||||
|
this.containerId = { path: ensureTrailingSlash(joinUrl(args.baseUrl, args.path)) };
|
||||||
|
this.store = args.store;
|
||||||
|
this.generator = args.generator;
|
||||||
|
this.storageKey = args.storageKey;
|
||||||
|
this.storage = args.storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handle(): Promise<void> {
|
||||||
|
this.logger.info(`Initializing container ${this.containerId.path}`);
|
||||||
|
const resources = this.generator.generate(this.containerId, {});
|
||||||
|
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 container ${this.containerId.path} with ${count} resources.`);
|
||||||
|
|
||||||
|
// Mark the initialization as finished
|
||||||
|
await this.storage.set(this.storageKey, true);
|
||||||
|
}
|
||||||
|
}
|
@ -1,73 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
73
test/unit/init/ContainerInitializer.test.ts
Normal file
73
test/unit/init/ContainerInitializer.test.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { ContainerInitializer } from '../../../src/init/ContainerInitializer';
|
||||||
|
import type { Logger } from '../../../src/logging/Logger';
|
||||||
|
import { getLoggerFor } from '../../../src/logging/LogUtil';
|
||||||
|
import type { Resource, ResourcesGenerator } from '../../../src/pods/generate/ResourcesGenerator';
|
||||||
|
import type { KeyValueStorage } from '../../../src/storage/keyvalue/KeyValueStorage';
|
||||||
|
import type { ResourceStore } from '../../../src/storage/ResourceStore';
|
||||||
|
|
||||||
|
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 ContainerInitializer', (): void => {
|
||||||
|
const baseUrl = 'http://test.com/';
|
||||||
|
const path = 'foo/';
|
||||||
|
let store: jest.Mocked<ResourceStore>;
|
||||||
|
let generatorData: Resource[];
|
||||||
|
let generator: jest.Mocked<ResourcesGenerator>;
|
||||||
|
const storageKey = 'done';
|
||||||
|
let storage: jest.Mocked<KeyValueStorage<string, boolean>>;
|
||||||
|
let initializer: ContainerInitializer;
|
||||||
|
let logger: jest.Mocked<Logger>;
|
||||||
|
|
||||||
|
beforeEach(async(): Promise<void> => {
|
||||||
|
store = {
|
||||||
|
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;
|
||||||
|
|
||||||
|
const map = new Map();
|
||||||
|
storage = {
|
||||||
|
get: jest.fn((id: string): any => map.get(id)),
|
||||||
|
set: jest.fn((id: string, value: any): any => map.set(id, value)),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
initializer = new ContainerInitializer({
|
||||||
|
baseUrl,
|
||||||
|
path,
|
||||||
|
store,
|
||||||
|
generator,
|
||||||
|
storageKey,
|
||||||
|
storage,
|
||||||
|
});
|
||||||
|
logger = getLoggerFor(initializer) as any;
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes resources and sets the storage value to true.', async(): Promise<void> => {
|
||||||
|
await expect(initializer.handle()).resolves.toBeUndefined();
|
||||||
|
expect(generator.generate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(store.setRepresentation).toHaveBeenCalledTimes(2);
|
||||||
|
expect(storage.get(storageKey)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
@ -1,82 +0,0 @@
|
|||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
x
Reference in New Issue
Block a user