Skip to content

Commit

Permalink
feat: Initialize root containers with RootContainerInitializer
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Dec 18, 2020
1 parent a08b7e9 commit 231349b
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 0 deletions.
9 changes: 9 additions & 0 deletions config/presets/init.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions src/init/RootContainerInitializer.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
if (!await this.hasRootContainer()) {
await this.createRootContainer();
}
}

/**
* Verify if a root container already exists in a ResourceStore.
*/
protected async hasRootContainer(): Promise<boolean> {
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<void> {
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,
});
}
}
5 changes: 5 additions & 0 deletions src/util/UriConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
41 changes: 41 additions & 0 deletions test/unit/init/RootContainerInitializer.test.ts
Original file line number Diff line number Diff line change
@@ -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<ResourceStore> = {
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<void> => {
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<void> => {
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<void> => {
store.getRepresentation.mockRejectedValueOnce(new Error('Fatal'));
await expect(initializer.handle()).rejects.toThrow('Fatal');
});
});

0 comments on commit 231349b

Please sign in to comment.