From 231349b30d1de5fd97ac14540ede945f1d4d9295 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Mon, 14 Dec 2020 14:13:08 +0100 Subject: [PATCH] feat: Initialize root containers with RootContainerInitializer --- config/presets/init.json | 9 +++ src/index.ts | 1 + src/init/RootContainerInitializer.ts | 72 +++++++++++++++++++ src/util/UriConstants.ts | 5 ++ .../init/RootContainerInitializer.test.ts | 41 +++++++++++ 5 files changed, 128 insertions(+) create mode 100644 src/init/RootContainerInitializer.ts create mode 100644 test/unit/init/RootContainerInitializer.test.ts diff --git a/config/presets/init.json b/config/presets/init.json index a7aff9a01..901aaa480 100644 --- a/config/presets/init.json +++ b/config/presets/init.json @@ -11,6 +11,15 @@ "@id": "urn:solid-server:default:LoggerFactory" } }, + { + "@type": "RootContainerInitializer", + "RootContainerInitializer:_baseUrl": { + "@id": "urn:solid-server:default:variable:baseUrl" + }, + "RootContainerInitializer:_store": { + "@id": "urn:solid-server:default:ResourceStore" + } + }, { "@type": "AclInitializer", "AclInitializer:_baseUrl": { diff --git a/src/index.ts b/src/index.ts index 38d8bfd36..a81b04b3d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ export * from './init/AclInitializer'; export * from './init/CliRunner'; export * from './init/Initializer'; export * from './init/LoggerInitializer'; +export * from './init/RootContainerInitializer'; export * from './init/ServerInitializer'; // LDP/HTTP/Metadata diff --git a/src/init/RootContainerInitializer.ts b/src/init/RootContainerInitializer.ts new file mode 100644 index 000000000..b9ba9a8f5 --- /dev/null +++ b/src/init/RootContainerInitializer.ts @@ -0,0 +1,72 @@ +import { DataFactory } from 'n3'; +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 { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; +import { ensureTrailingSlash } from '../util/PathUtil'; +import { generateResourceQuads } from '../util/ResourceUtil'; +import { guardedStreamFrom } from '../util/StreamUtil'; +import { PIM, RDF } from '../util/UriConstants'; +import { toNamedNode } from '../util/UriUtil'; +import { Initializer } from './Initializer'; +import namedNode = DataFactory.namedNode; + +/** + * Initializes ResourceStores by creating a root container if it didn't exist yet. + */ +export class RootContainerInitializer extends Initializer { + protected readonly logger = getLoggerFor(this); + private readonly baseId: ResourceIdentifier; + private readonly store: ResourceStore; + + public constructor(baseUrl: string, store: ResourceStore) { + super(); + this.baseId = { path: ensureTrailingSlash(baseUrl) }; + this.store = store; + } + + public async handle(): Promise { + if (!await this.hasRootContainer()) { + await this.createRootContainer(); + } + } + + /** + * Verify if a root container already exists in a ResourceStore. + */ + protected async hasRootContainer(): Promise { + try { + const result = await this.store.getRepresentation(this.baseId, {}); + this.logger.debug(`Existing root container found at ${this.baseId.path}`); + result.data.destroy(); + return true; + } catch (error: unknown) { + if (!(error instanceof NotFoundHttpError)) { + throw error; + } + } + return false; + } + + /** + * Create a root container in a ResourceStore. + */ + protected async createRootContainer(): Promise { + const metadata = new RepresentationMetadata(this.baseId); + metadata.addQuads(generateResourceQuads(namedNode(this.baseId.path), 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 + metadata.add(RDF.type, toNamedNode(PIM.Storage)); + + metadata.contentType = TEXT_TURTLE; + + await this.store.setRepresentation(this.baseId, { + binary: true, + data: guardedStreamFrom([]), + metadata, + }); + } +} diff --git a/src/util/UriConstants.ts b/src/util/UriConstants.ts index 861ba1e91..58058f887 100644 --- a/src/util/UriConstants.ts +++ b/src/util/UriConstants.ts @@ -46,6 +46,11 @@ export const MA = { format: MA_PREFIX('format'), }; +const PIM_PREFIX = createNamespace('http://www.w3.org/ns/pim/space#'); +export const PIM = { + Storage: PIM_PREFIX('Storage'), +}; + const POSIX_PREFIX = createNamespace('http://www.w3.org/ns/posix/stat#'); export const POSIX = { mtime: POSIX_PREFIX('mtime'), diff --git a/test/unit/init/RootContainerInitializer.test.ts b/test/unit/init/RootContainerInitializer.test.ts new file mode 100644 index 000000000..ba7942f82 --- /dev/null +++ b/test/unit/init/RootContainerInitializer.test.ts @@ -0,0 +1,41 @@ +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 = { + getRepresentation: jest.fn().mockRejectedValue(new NotFoundHttpError()), + setRepresentation: jest.fn(), + } as any; + const initializer = new RootContainerInitializer(baseUrl, store); + + afterEach((): void => { + jest.clearAllMocks(); + }); + + it('invokes ResourceStore initialization.', async(): Promise => { + await initializer.handle(); + + expect(store.getRepresentation).toHaveBeenCalledTimes(1); + expect(store.getRepresentation).toHaveBeenCalledWith({ path: baseUrl }, {}); + expect(store.setRepresentation).toHaveBeenCalledTimes(1); + }); + + it('does not invoke ResourceStore initialization when a root container already exists.', async(): Promise => { + store.getRepresentation.mockReturnValueOnce(Promise.resolve({ + data: { destroy: jest.fn() }, + } as any)); + + await initializer.handle(); + + expect(store.getRepresentation).toHaveBeenCalledTimes(1); + expect(store.getRepresentation).toHaveBeenCalledWith({ path: 'http://test.com/' }, {}); + expect(store.setRepresentation).toHaveBeenCalledTimes(0); + }); + + it('errors when the store errors writing the root container.', async(): Promise => { + store.getRepresentation.mockRejectedValueOnce(new Error('Fatal')); + await expect(initializer.handle()).rejects.toThrow('Fatal'); + }); +});