feat: Move all metadata file knowledge to file mapper

This commit is contained in:
Joachim Van Herwegen 2021-07-20 15:58:45 +02:00
parent 6495d650c2
commit 57da67f9ee
11 changed files with 215 additions and 114 deletions

View File

@ -26,7 +26,6 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
private readonly templateFolder: string; private readonly templateFolder: string;
private readonly factory: FileIdentifierMapperFactory; private readonly factory: FileIdentifierMapperFactory;
private readonly engine: TemplateEngine; private readonly engine: TemplateEngine;
private readonly metaExtension = '.meta';
/** /**
* A mapper is needed to convert the template file paths to identifiers relative to the given base identifier. * A mapper is needed to convert the template file paths to identifiers relative to the given base identifier.
@ -91,9 +90,8 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
const links: Record<string, { link: ResourceLink; meta?: ResourceLink }> = { }; const links: Record<string, { link: ResourceLink; meta?: ResourceLink }> = { };
for await (const link of linkGen) { for await (const link of linkGen) {
const { path } = link.identifier; const { path } = link.identifier;
if (this.isMeta(path)) { if (link.isMetadata) {
const resourcePath = this.metaToResource(link.identifier).path; links[path] = Object.assign(links[path] || {}, { meta: link });
links[resourcePath] = Object.assign(links[resourcePath] || {}, { meta: link });
} else { } else {
links[path] = Object.assign(links[path] || {}, { link }); links[path] = Object.assign(links[path] || {}, { link });
} }
@ -135,11 +133,10 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
*/ */
private async generateMetadata(metaLink: ResourceLink, options: Dict<string>): private async generateMetadata(metaLink: ResourceLink, options: Dict<string>):
Promise<RepresentationMetadata> { Promise<RepresentationMetadata> {
const identifier = this.metaToResource(metaLink.identifier); const metadata = new RepresentationMetadata(metaLink.identifier);
const metadata = new RepresentationMetadata(identifier);
const data = await this.parseTemplate(metaLink.filePath, options); const data = await this.parseTemplate(metaLink.filePath, options);
const parser = new Parser({ format: metaLink.contentType, baseIRI: identifier.path }); const parser = new Parser({ format: metaLink.contentType, baseIRI: metaLink.identifier.path });
const quads = parser.parse(data); const quads = parser.parse(data);
metadata.addQuads(quads); metadata.addQuads(quads);
@ -153,18 +150,4 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
const raw = await fsPromises.readFile(filePath, 'utf8'); const raw = await fsPromises.readFile(filePath, 'utf8');
return this.engine.apply(raw, options); return this.engine.apply(raw, options);
} }
/**
* Verifies if the given path corresponds to a metadata file.
*/
private isMeta(path: string): boolean {
return path.endsWith(this.metaExtension);
}
/**
* Converts a generated metadata identifier to the identifier of its corresponding resource.
*/
private metaToResource(metaIdentifier: ResourceIdentifier): ResourceIdentifier {
return { path: metaIdentifier.path.slice(0, -this.metaExtension.length) };
}
} }

View File

@ -21,7 +21,7 @@ export class RootFilePathHandler extends VariableHandler {
public async handle({ identifier, settings }: { identifier: ResourceIdentifier; settings: PodSettings }): public async handle({ identifier, settings }: { identifier: ResourceIdentifier; settings: PodSettings }):
Promise<void> { Promise<void> {
const path = (await this.fileMapper.mapUrlToFilePath(identifier)).filePath; const path = (await this.fileMapper.mapUrlToFilePath(identifier, false)).filePath;
try { try {
// Even though we check if it already exists, there is still a potential race condition // Even though we check if it already exists, there is still a potential race condition
// in between this check and the store being created. // in between this check and the store being created.

View File

@ -5,7 +5,6 @@ import type { Quad } from 'rdf-js';
import type { Representation } from '../../ldp/representation/Representation'; import type { Representation } from '../../ldp/representation/Representation';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import { ConflictHttpError } from '../../util/errors/ConflictHttpError';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import { isSystemError } from '../../util/errors/SystemError'; import { isSystemError } from '../../util/errors/SystemError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError'; import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
@ -43,7 +42,7 @@ export class FileDataAccessor implements DataAccessor {
* Will throw NotFoundHttpError if the input is a container. * Will throw NotFoundHttpError if the input is a container.
*/ */
public async getData(identifier: ResourceIdentifier): Promise<Guarded<Readable>> { public async getData(identifier: ResourceIdentifier): Promise<Guarded<Readable>> {
const link = await this.resourceMapper.mapUrlToFilePath(identifier); const link = await this.resourceMapper.mapUrlToFilePath(identifier, false);
const stats = await this.getStats(link.filePath); const stats = await this.getStats(link.filePath);
if (stats.isFile()) { if (stats.isFile()) {
@ -58,7 +57,7 @@ export class FileDataAccessor implements DataAccessor {
* and adding file system specific metadata elements. * and adding file system specific metadata elements.
*/ */
public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> { public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> {
const link = await this.resourceMapper.mapUrlToFilePath(identifier); const link = await this.resourceMapper.mapUrlToFilePath(identifier, false);
const stats = await this.getStats(link.filePath); const stats = await this.getStats(link.filePath);
if (!isContainerIdentifier(identifier) && stats.isFile()) { if (!isContainerIdentifier(identifier) && stats.isFile()) {
return this.getFileMetadata(link, stats); return this.getFileMetadata(link, stats);
@ -70,7 +69,7 @@ export class FileDataAccessor implements DataAccessor {
} }
public async* getChildren(identifier: ResourceIdentifier): AsyncIterableIterator<RepresentationMetadata> { public async* getChildren(identifier: ResourceIdentifier): AsyncIterableIterator<RepresentationMetadata> {
const link = await this.resourceMapper.mapUrlToFilePath(identifier); const link = await this.resourceMapper.mapUrlToFilePath(identifier, false);
yield* this.getChildMetadata(link); yield* this.getChildMetadata(link);
} }
@ -80,10 +79,7 @@ export class FileDataAccessor implements DataAccessor {
*/ */
public async writeDocument(identifier: ResourceIdentifier, data: Guarded<Readable>, metadata: RepresentationMetadata): public async writeDocument(identifier: ResourceIdentifier, data: Guarded<Readable>, metadata: RepresentationMetadata):
Promise<void> { Promise<void> {
if (this.isMetadataPath(identifier.path)) { const link = await this.resourceMapper.mapUrlToFilePath(identifier, false, metadata.contentType);
throw new ConflictHttpError('Not allowed to create files with the metadata extension.');
}
const link = await this.resourceMapper.mapUrlToFilePath(identifier, metadata.contentType);
// Check if we already have a corresponding file with a different extension // Check if we already have a corresponding file with a different extension
await this.verifyExistingExtension(link); await this.verifyExistingExtension(link);
@ -95,7 +91,8 @@ export class FileDataAccessor implements DataAccessor {
} catch (error: unknown) { } catch (error: unknown) {
// Delete the metadata if there was an error writing the file // Delete the metadata if there was an error writing the file
if (wroteMetadata) { if (wroteMetadata) {
await fsPromises.unlink((await this.getMetadataLink(link.identifier)).filePath); const metaLink = await this.resourceMapper.mapUrlToFilePath(identifier, true);
await fsPromises.unlink(metaLink.filePath);
} }
throw error; throw error;
} }
@ -105,7 +102,7 @@ export class FileDataAccessor implements DataAccessor {
* Creates corresponding folder if necessary and writes metadata to metadata file if necessary. * Creates corresponding folder if necessary and writes metadata to metadata file if necessary.
*/ */
public async writeContainer(identifier: ResourceIdentifier, metadata: RepresentationMetadata): Promise<void> { public async writeContainer(identifier: ResourceIdentifier, metadata: RepresentationMetadata): Promise<void> {
const link = await this.resourceMapper.mapUrlToFilePath(identifier); const link = await this.resourceMapper.mapUrlToFilePath(identifier, false);
try { try {
await fsPromises.mkdir(link.filePath, { recursive: true }); await fsPromises.mkdir(link.filePath, { recursive: true });
} catch (error: unknown) { } catch (error: unknown) {
@ -122,11 +119,12 @@ export class FileDataAccessor implements DataAccessor {
* Removes the corresponding file/folder (and metadata file). * Removes the corresponding file/folder (and metadata file).
*/ */
public async deleteResource(identifier: ResourceIdentifier): Promise<void> { public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
const link = await this.resourceMapper.mapUrlToFilePath(identifier); const link = await this.resourceMapper.mapUrlToFilePath(identifier, false);
const stats = await this.getStats(link.filePath); const stats = await this.getStats(link.filePath);
try { try {
await fsPromises.unlink((await this.getMetadataLink(link.identifier)).filePath); const metaLink = await this.resourceMapper.mapUrlToFilePath(identifier, true);
await fsPromises.unlink(metaLink.filePath);
} catch (error: unknown) { } catch (error: unknown) {
// Ignore if it doesn't exist // Ignore if it doesn't exist
if (!isSystemError(error) || error.code !== 'ENOENT') { if (!isSystemError(error) || error.code !== 'ENOENT') {
@ -161,21 +159,6 @@ export class FileDataAccessor implements DataAccessor {
} }
} }
/**
* Generates ResourceLink that corresponds to the metadata resource of the given identifier.
*/
private async getMetadataLink(identifier: ResourceIdentifier): Promise<ResourceLink> {
const metaIdentifier = { path: `${identifier.path}.meta` };
return this.resourceMapper.mapUrlToFilePath(metaIdentifier);
}
/**
* Checks if the given path is a metadata path.
*/
private isMetadataPath(path: string): boolean {
return path.endsWith('.meta');
}
/** /**
* Reads and generates all metadata relevant for the given file, * Reads and generates all metadata relevant for the given file,
* ingesting it into a RepresentationMetadata object. * ingesting it into a RepresentationMetadata object.
@ -215,7 +198,7 @@ export class FileDataAccessor implements DataAccessor {
metadata.remove(RDF.type, LDP.terms.BasicContainer); metadata.remove(RDF.type, LDP.terms.BasicContainer);
metadata.removeAll(CONTENT_TYPE); metadata.removeAll(CONTENT_TYPE);
const quads = metadata.quads(); const quads = metadata.quads();
const metadataLink = await this.getMetadataLink(link.identifier); const metadataLink = await this.resourceMapper.mapUrlToFilePath(link.identifier, true);
let wroteMetadata: boolean; let wroteMetadata: boolean;
// Write metadata to file if there are quads remaining // Write metadata to file if there are quads remaining
@ -263,7 +246,7 @@ export class FileDataAccessor implements DataAccessor {
*/ */
private async getRawMetadata(identifier: ResourceIdentifier): Promise<Quad[]> { private async getRawMetadata(identifier: ResourceIdentifier): Promise<Quad[]> {
try { try {
const metadataLink = await this.getMetadataLink(identifier); const metadataLink = await this.resourceMapper.mapUrlToFilePath(identifier, true);
// Check if the metadata file exists first // Check if the metadata file exists first
await fsPromises.lstat(metadataLink.filePath); await fsPromises.lstat(metadataLink.filePath);
@ -290,10 +273,6 @@ export class FileDataAccessor implements DataAccessor {
// For every child in the container we want to generate specific metadata // For every child in the container we want to generate specific metadata
for await (const entry of dir) { for await (const entry of dir) {
const childName = entry.name; const childName = entry.name;
// Hide metadata files
if (this.isMetadataPath(childName)) {
continue;
}
// Ignore non-file/directory entries in the folder // Ignore non-file/directory entries in the folder
if (!entry.isFile() && !entry.isDirectory()) { if (!entry.isFile() && !entry.isDirectory()) {
@ -304,6 +283,11 @@ export class FileDataAccessor implements DataAccessor {
const childLink = await this.resourceMapper const childLink = await this.resourceMapper
.mapFilePathToUrl(joinFilePath(link.filePath, childName), entry.isDirectory()); .mapFilePathToUrl(joinFilePath(link.filePath, childName), entry.isDirectory());
// Hide metadata files
if (childLink.isMetadata) {
continue;
}
// Generate metadata of this specific child // Generate metadata of this specific child
const childStats = await fsPromises.lstat(joinFilePath(link.filePath, childName)); const childStats = await fsPromises.lstat(joinFilePath(link.filePath, childName));
const metadata = new RepresentationMetadata(childLink.identifier); const metadata = new RepresentationMetadata(childLink.identifier);
@ -340,7 +324,7 @@ export class FileDataAccessor implements DataAccessor {
private async verifyExistingExtension(link: ResourceLink): Promise<void> { private async verifyExistingExtension(link: ResourceLink): Promise<void> {
try { try {
// Delete the old file with the (now) wrong extension // Delete the old file with the (now) wrong extension
const oldLink = await this.resourceMapper.mapUrlToFilePath(link.identifier); const oldLink = await this.resourceMapper.mapUrlToFilePath(link.identifier, false);
if (oldLink.filePath !== link.filePath) { if (oldLink.filePath !== link.filePath) {
await fsPromises.unlink(oldLink.filePath); await fsPromises.unlink(oldLink.filePath);
} }

View File

@ -2,6 +2,7 @@ import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdenti
import { getLoggerFor } from '../../logging/LogUtil'; import { getLoggerFor } from '../../logging/LogUtil';
import { APPLICATION_OCTET_STREAM } from '../../util/ContentTypes'; import { APPLICATION_OCTET_STREAM } from '../../util/ContentTypes';
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
import { ConflictHttpError } from '../../util/errors/ConflictHttpError';
import { InternalServerError } from '../../util/errors/InternalServerError'; import { InternalServerError } from '../../util/errors/InternalServerError';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import { import {
@ -33,12 +34,23 @@ export class BaseFileIdentifierMapper implements FileIdentifierMapper {
* Determines the content type if none was provided. * Determines the content type if none was provided.
* For containers the content-type input is ignored. * For containers the content-type input is ignored.
* @param identifier - The input identifier. * @param identifier - The input identifier.
* @param isMetadata - If we need the data or metadata file path.
* @param contentType - The content-type provided with the request. * @param contentType - The content-type provided with the request.
* *
* @returns A ResourceLink with all the necessary metadata. * @returns A ResourceLink with all the necessary metadata.
*/ */
public async mapUrlToFilePath(identifier: ResourceIdentifier, contentType?: string): Promise<ResourceLink> { public async mapUrlToFilePath(identifier: ResourceIdentifier, isMetadata: boolean, contentType?: string):
const path = this.getRelativePath(identifier); Promise<ResourceLink> {
// Technically we could allow paths ending on .meta as long as we make sure there is never a mixup.
// But this can lead to potential issues.
// This also immediately stops users that expect they can update metadata like this.
if (this.isMetadataPath(identifier.path)) {
throw new ConflictHttpError('Not allowed to create files with the metadata extension.');
}
let path = this.getRelativePath(identifier);
if (isMetadata) {
path += '.meta';
}
this.validateRelativePath(path, identifier); this.validateRelativePath(path, identifier);
const filePath = this.getAbsolutePath(path); const filePath = this.getAbsolutePath(path);
@ -57,7 +69,7 @@ export class BaseFileIdentifierMapper implements FileIdentifierMapper {
*/ */
protected async mapUrlToContainerPath(identifier: ResourceIdentifier, filePath: string): Promise<ResourceLink> { protected async mapUrlToContainerPath(identifier: ResourceIdentifier, filePath: string): Promise<ResourceLink> {
this.logger.debug(`URL ${identifier.path} points to the container ${filePath}`); this.logger.debug(`URL ${identifier.path} points to the container ${filePath}`);
return { identifier, filePath }; return { identifier, filePath, isMetadata: this.isMetadataPath(filePath) };
} }
/** /**
@ -75,7 +87,7 @@ export class BaseFileIdentifierMapper implements FileIdentifierMapper {
Promise<ResourceLink> { Promise<ResourceLink> {
contentType = await this.getContentTypeFromUrl(identifier, contentType); contentType = await this.getContentTypeFromUrl(identifier, contentType);
this.logger.debug(`The path for ${identifier.path} is ${filePath}`); this.logger.debug(`The path for ${identifier.path} is ${filePath}`);
return { identifier, filePath, contentType }; return { identifier, filePath, contentType, isMetadata: this.isMetadataPath(filePath) };
} }
/** /**
@ -113,7 +125,11 @@ export class BaseFileIdentifierMapper implements FileIdentifierMapper {
this.logger.debug(`Document ${filePath} maps to URL ${url}`); this.logger.debug(`Document ${filePath} maps to URL ${url}`);
contentType = await this.getContentTypeFromPath(filePath); contentType = await this.getContentTypeFromPath(filePath);
} }
return { identifier: { path: url }, filePath, contentType }; const isMetadata = this.isMetadataPath(filePath);
if (isMetadata) {
url = url.slice(0, -'.meta'.length);
}
return { identifier: { path: url }, filePath, contentType, isMetadata };
} }
/** /**
@ -194,4 +210,11 @@ export class BaseFileIdentifierMapper implements FileIdentifierMapper {
throw new BadRequestHttpError('Disallowed /.. segment in URL'); throw new BadRequestHttpError('Disallowed /.. segment in URL');
} }
} }
/**
* Checks if the given path is a metadata path.
*/
protected isMetadataPath(path: string): boolean {
return path.endsWith('.meta');
}
} }

View File

@ -13,6 +13,10 @@ export interface ResourceLink {
* Content-type for a document (not defined for containers). * Content-type for a document (not defined for containers).
*/ */
contentType?: string; contentType?: string;
/**
* If the resource is a metadata file.
*/
isMetadata: boolean;
} }
/** /**
@ -33,11 +37,13 @@ export interface FileIdentifierMapper {
* If there is no corresponding file a file path will be generated. * If there is no corresponding file a file path will be generated.
* For containers the content-type input gets ignored. * For containers the content-type input gets ignored.
* @param identifier - The input identifier. * @param identifier - The input identifier.
* @param isMetadata - If we are mapping the metadata of the resource instead of its data.
* @param contentType - The (optional) content-type of the resource. * @param contentType - The (optional) content-type of the resource.
* *
* @returns A ResourceLink with all the necessary metadata. * @returns A ResourceLink with all the necessary metadata.
*/ */
mapUrlToFilePath: (identifier: ResourceIdentifier, contentType?: string) => Promise<ResourceLink>; mapUrlToFilePath: (identifier: ResourceIdentifier, isMetadata: boolean, contentType?: string) =>
Promise<ResourceLink>;
} }
/** /**

View File

@ -18,11 +18,16 @@ class DummyFactory implements FileIdentifierMapperFactory {
const trimRoot = trimTrailingSlashes(rootFilePath); const trimRoot = trimTrailingSlashes(rootFilePath);
return { return {
async mapFilePathToUrl(filePath: string, isContainer: boolean): Promise<ResourceLink> { async mapFilePathToUrl(filePath: string, isContainer: boolean): Promise<ResourceLink> {
const path = `${trimBase}${filePath.slice(trimRoot.length)}`; let path = `${trimBase}${filePath.slice(trimRoot.length)}`;
const isMetadata = filePath.endsWith('.meta');
if (isMetadata) {
path = path.slice(0, -'.meta'.length);
}
return { return {
identifier: { path: isContainer ? ensureTrailingSlash(path) : path }, identifier: { path: isContainer ? ensureTrailingSlash(path) : path },
filePath, filePath,
contentType: isContainer ? undefined : 'text/turtle', contentType: isContainer ? undefined : 'text/turtle',
isMetadata,
}; };
}, },
} as any; } as any;

View File

@ -23,6 +23,7 @@ describe('A RootFilePathHandler', (): void => {
mapUrlToFilePath: async(id): Promise<ResourceLink> => ({ mapUrlToFilePath: async(id): Promise<ResourceLink> => ({
identifier: id, identifier: id,
filePath: joinFilePath(rootFilePath, id.path.slice(baseUrl.length)), filePath: joinFilePath(rootFilePath, id.path.slice(baseUrl.length)),
isMetadata: false,
}), }),
mapFilePathToUrl: jest.fn(), mapFilePathToUrl: jest.fn(),
}); });

View File

@ -1,5 +1,6 @@
import { BaseFileIdentifierMapper } from '../../../../src/storage/mapping/BaseFileIdentifierMapper'; import { BaseFileIdentifierMapper } from '../../../../src/storage/mapping/BaseFileIdentifierMapper';
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError'; import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError';
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
import { trimTrailingSlashes } from '../../../../src/util/PathUtil'; import { trimTrailingSlashes } from '../../../../src/util/PathUtil';
@ -12,51 +13,71 @@ describe('An BaseFileIdentifierMapper', (): void => {
describe('mapUrlToFilePath', (): void => { describe('mapUrlToFilePath', (): void => {
it('throws 404 if the input path does not contain the base.', async(): Promise<void> => { it('throws 404 if the input path does not contain the base.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: 'invalid' })).rejects.toThrow(NotFoundHttpError); await expect(mapper.mapUrlToFilePath({ path: 'invalid' }, false)).rejects.toThrow(NotFoundHttpError);
}); });
it('throws 404 if the relative path does not start with a slash.', async(): Promise<void> => { it('throws 404 if the relative path does not start with a slash.', async(): Promise<void> => {
const result = mapper.mapUrlToFilePath({ path: `${trimTrailingSlashes(base)}test` }); const result = mapper.mapUrlToFilePath({ path: `${trimTrailingSlashes(base)}test` }, false);
await expect(result).rejects.toThrow(BadRequestHttpError); await expect(result).rejects.toThrow(BadRequestHttpError);
await expect(result).rejects.toThrow('URL needs a / after the base'); await expect(result).rejects.toThrow('URL needs a / after the base');
}); });
it('throws 400 if the input path contains relative parts.', async(): Promise<void> => { it('throws 400 if the input path contains relative parts.', async(): Promise<void> => {
const result = mapper.mapUrlToFilePath({ path: `${base}test/../test2` }); const result = mapper.mapUrlToFilePath({ path: `${base}test/../test2` }, false);
await expect(result).rejects.toThrow(BadRequestHttpError); await expect(result).rejects.toThrow(BadRequestHttpError);
await expect(result).rejects.toThrow('Disallowed /.. segment in URL'); await expect(result).rejects.toThrow('Disallowed /.. segment in URL');
}); });
it('returns the corresponding file path for container identifiers.', async(): Promise<void> => { it('returns the corresponding file path for container identifiers.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}container/` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}container/` }, false)).resolves.toEqual({
identifier: { path: `${base}container/` }, identifier: { path: `${base}container/` },
filePath: `${rootFilepath}container/`, filePath: `${rootFilepath}container/`,
isMetadata: false,
}); });
}); });
it('returns the default content-type.', async(): Promise<void> => { it('returns the default content-type.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test` }, false)).resolves.toEqual({
identifier: { path: `${base}test` }, identifier: { path: `${base}test` },
filePath: `${rootFilepath}test`, filePath: `${rootFilepath}test`,
contentType: 'application/octet-stream', contentType: 'application/octet-stream',
isMetadata: false,
}); });
await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, false)).resolves.toEqual({
identifier: { path: `${base}test.ttl` }, identifier: { path: `${base}test.ttl` },
filePath: `${rootFilepath}test.ttl`, filePath: `${rootFilepath}test.ttl`,
contentType: 'application/octet-stream', contentType: 'application/octet-stream',
isMetadata: false,
}); });
await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` }, false)).resolves.toEqual({
identifier: { path: `${base}test.txt` }, identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}test.txt`, filePath: `${rootFilepath}test.txt`,
contentType: 'application/octet-stream', contentType: 'application/octet-stream',
isMetadata: false,
}); });
}); });
it('generates a file path if supported content-type was provided.', async(): Promise<void> => { it('generates a file path if supported content-type was provided.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, 'text/turtle')).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, false, 'text/turtle')).resolves.toEqual({
identifier: { path: `${base}test.ttl` }, identifier: { path: `${base}test.ttl` },
filePath: `${rootFilepath}test.ttl`, filePath: `${rootFilepath}test.ttl`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
});
});
it('errors on metadata identifiers.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.meta` }, true)).rejects.toThrow(ConflictHttpError);
await expect(mapper.mapUrlToFilePath({ path: `${base}test.meta` }, true))
.rejects.toThrow('Not allowed to create files with the metadata extension.');
});
it('generates correct metadata file paths.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` }, true)).resolves.toEqual({
identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}test.txt.meta`,
contentType: 'application/octet-stream',
isMetadata: true,
}); });
}); });
}); });
@ -70,6 +91,7 @@ describe('An BaseFileIdentifierMapper', (): void => {
await expect(mapper.mapFilePathToUrl(`${rootFilepath}container/`, true)).resolves.toEqual({ await expect(mapper.mapFilePathToUrl(`${rootFilepath}container/`, true)).resolves.toEqual({
identifier: { path: `${base}container/` }, identifier: { path: `${base}container/` },
filePath: `${rootFilepath}container/`, filePath: `${rootFilepath}container/`,
isMetadata: false,
}); });
}); });
@ -78,16 +100,28 @@ describe('An BaseFileIdentifierMapper', (): void => {
identifier: { path: `${base}test` }, identifier: { path: `${base}test` },
filePath: `${rootFilepath}test`, filePath: `${rootFilepath}test`,
contentType: 'application/octet-stream', contentType: 'application/octet-stream',
isMetadata: false,
}); });
await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.ttl`, false)).resolves.toEqual({ await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.ttl`, false)).resolves.toEqual({
identifier: { path: `${base}test.ttl` }, identifier: { path: `${base}test.ttl` },
filePath: `${rootFilepath}test.ttl`, filePath: `${rootFilepath}test.ttl`,
contentType: 'application/octet-stream', contentType: 'application/octet-stream',
isMetadata: false,
}); });
await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.txt`, false)).resolves.toEqual({ await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.txt`, false)).resolves.toEqual({
identifier: { path: `${base}test.txt` }, identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}test.txt`, filePath: `${rootFilepath}test.txt`,
contentType: 'application/octet-stream', contentType: 'application/octet-stream',
isMetadata: false,
});
});
it('identifies metadata files.', async(): Promise<void> => {
await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.meta`, false)).resolves.toEqual({
identifier: { path: `${base}test` },
filePath: `${rootFilepath}test.meta`,
contentType: 'application/octet-stream',
isMetadata: true,
}); });
}); });
}); });

View File

@ -26,30 +26,31 @@ describe('An ExtensionBasedMapper', (): void => {
describe('mapUrlToFilePath', (): void => { describe('mapUrlToFilePath', (): void => {
it('throws 404 if the input path does not contain the base.', async(): Promise<void> => { it('throws 404 if the input path does not contain the base.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: 'invalid' })).rejects.toThrow(NotFoundHttpError); await expect(mapper.mapUrlToFilePath({ path: 'invalid' }, false)).rejects.toThrow(NotFoundHttpError);
}); });
it('throws 404 if the relative path does not start with a slash.', async(): Promise<void> => { it('throws 404 if the relative path does not start with a slash.', async(): Promise<void> => {
const result = mapper.mapUrlToFilePath({ path: `${trimTrailingSlashes(base)}test` }); const result = mapper.mapUrlToFilePath({ path: `${trimTrailingSlashes(base)}test` }, false);
await expect(result).rejects.toThrow(BadRequestHttpError); await expect(result).rejects.toThrow(BadRequestHttpError);
await expect(result).rejects.toThrow('URL needs a / after the base'); await expect(result).rejects.toThrow('URL needs a / after the base');
}); });
it('throws 400 if the input path contains relative parts.', async(): Promise<void> => { it('throws 400 if the input path contains relative parts.', async(): Promise<void> => {
const result = mapper.mapUrlToFilePath({ path: `${base}test/../test2` }); const result = mapper.mapUrlToFilePath({ path: `${base}test/../test2` }, false);
await expect(result).rejects.toThrow(BadRequestHttpError); await expect(result).rejects.toThrow(BadRequestHttpError);
await expect(result).rejects.toThrow('Disallowed /.. segment in URL'); await expect(result).rejects.toThrow('Disallowed /.. segment in URL');
}); });
it('returns the corresponding file path for container identifiers.', async(): Promise<void> => { it('returns the corresponding file path for container identifiers.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}container/` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}container/` }, false)).resolves.toEqual({
identifier: { path: `${base}container/` }, identifier: { path: `${base}container/` },
filePath: `${rootFilepath}container/`, filePath: `${rootFilepath}container/`,
isMetadata: false,
}); });
}); });
it('rejects URLs that end with "$.{extension}".', async(): Promise<void> => { it('rejects URLs that end with "$.{extension}".', async(): Promise<void> => {
const result = mapper.mapUrlToFilePath({ path: `${base}test$.txt` }); const result = mapper.mapUrlToFilePath({ path: `${base}test$.txt` }, false);
await expect(result).rejects.toThrow(NotImplementedHttpError); await expect(result).rejects.toThrow(NotImplementedHttpError);
await expect(result).rejects.toThrow('Identifiers cannot contain a dollar sign before their extension'); await expect(result).rejects.toThrow('Identifiers cannot contain a dollar sign before their extension');
}); });
@ -58,58 +59,74 @@ describe('An ExtensionBasedMapper', (): void => {
fsPromises.readdir.mockImplementation((): void => { fsPromises.readdir.mockImplementation((): void => {
throw new Error('does not exist'); throw new Error('does not exist');
}); });
await expect(mapper.mapUrlToFilePath({ path: `${base}no/test.txt` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}no/test.txt` }, false)).resolves.toEqual({
identifier: { path: `${base}no/test.txt` }, identifier: { path: `${base}no/test.txt` },
filePath: `${rootFilepath}no/test.txt`, filePath: `${rootFilepath}no/test.txt`,
contentType: 'text/plain', contentType: 'text/plain',
isMetadata: false,
}); });
}); });
it('determines content-type by extension when looking for a file that does not exist.', async(): Promise<void> => { it('determines content-type by extension when looking for a file that does not exist.', async(): Promise<void> => {
fsPromises.readdir.mockReturnValue([ 'test.ttl' ]); fsPromises.readdir.mockReturnValue([ 'test.ttl' ]);
await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` }, false)).resolves.toEqual({
identifier: { path: `${base}test.txt` }, identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}test.txt`, filePath: `${rootFilepath}test.txt`,
contentType: 'text/plain', contentType: 'text/plain',
isMetadata: false,
}); });
}); });
it('determines the content-type based on the extension.', async(): Promise<void> => { it('determines the content-type based on the extension.', async(): Promise<void> => {
fsPromises.readdir.mockReturnValue([ 'test.txt' ]); fsPromises.readdir.mockReturnValue([ 'test.txt' ]);
await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` }, false)).resolves.toEqual({
identifier: { path: `${base}test.txt` }, identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}test.txt`, filePath: `${rootFilepath}test.txt`,
contentType: 'text/plain', contentType: 'text/plain',
isMetadata: false,
});
});
it('determines the content-type correctly for metadata files.', async(): Promise<void> => {
fsPromises.readdir.mockReturnValue([ 'test.meta' ]);
await expect(mapper.mapUrlToFilePath({ path: `${base}test` }, true)).resolves.toEqual({
identifier: { path: `${base}test` },
filePath: `${rootFilepath}test.meta`,
contentType: 'text/turtle',
isMetadata: true,
}); });
}); });
it('matches even if the content-type does not match the extension.', async(): Promise<void> => { it('matches even if the content-type does not match the extension.', async(): Promise<void> => {
fsPromises.readdir.mockReturnValue([ 'test.txt$.ttl' ]); fsPromises.readdir.mockReturnValue([ 'test.txt$.ttl' ]);
await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` }, false)).resolves.toEqual({
identifier: { path: `${base}test.txt` }, identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}test.txt$.ttl`, filePath: `${rootFilepath}test.txt$.ttl`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });
it('generates a file path if the content-type was provided.', async(): Promise<void> => { it('generates a file path if the content-type was provided.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` }, 'text/plain')).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` }, false, 'text/plain')).resolves.toEqual({
identifier: { path: `${base}test.txt` }, identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}test.txt`, filePath: `${rootFilepath}test.txt`,
contentType: 'text/plain', contentType: 'text/plain',
isMetadata: false,
}); });
}); });
it('adds an extension if the given extension does not match the given content-type.', async(): Promise<void> => { it('adds an extension if the given extension does not match the given content-type.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` }, 'text/turtle')).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` }, false, 'text/turtle')).resolves.toEqual({
identifier: { path: `${base}test.txt` }, identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}test.txt$.ttl`, filePath: `${rootFilepath}test.txt$.ttl`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });
it('throws 501 if the given content-type is not recognized.', async(): Promise<void> => { it('throws 501 if the given content-type is not recognized.', async(): Promise<void> => {
const result = mapper.mapUrlToFilePath({ path: `${base}test.txt` }, 'fake/data'); const result = mapper.mapUrlToFilePath({ path: `${base}test.txt` }, false, 'fake/data');
await expect(result).rejects.toThrow(NotImplementedHttpError); await expect(result).rejects.toThrow(NotImplementedHttpError);
await expect(result).rejects.toThrow('Unsupported content type fake/data'); await expect(result).rejects.toThrow('Unsupported content type fake/data');
}); });
@ -124,6 +141,7 @@ describe('An ExtensionBasedMapper', (): void => {
await expect(mapper.mapFilePathToUrl(`${rootFilepath}container/`, true)).resolves.toEqual({ await expect(mapper.mapFilePathToUrl(`${rootFilepath}container/`, true)).resolves.toEqual({
identifier: { path: `${base}container/` }, identifier: { path: `${base}container/` },
filePath: `${rootFilepath}container/`, filePath: `${rootFilepath}container/`,
isMetadata: false,
}); });
}); });
@ -132,6 +150,16 @@ describe('An ExtensionBasedMapper', (): void => {
identifier: { path: `${base}test.txt` }, identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}test.txt`, filePath: `${rootFilepath}test.txt`,
contentType: 'text/plain', contentType: 'text/plain',
isMetadata: false,
});
});
it('returns a generated identifier for metadata files.', async(): Promise<void> => {
await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.meta`, false)).resolves.toEqual({
identifier: { path: `${base}test` },
filePath: `${rootFilepath}test.meta`,
contentType: 'text/turtle',
isMetadata: true,
}); });
}); });
@ -140,6 +168,7 @@ describe('An ExtensionBasedMapper', (): void => {
identifier: { path: `${base}test.txt` }, identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}test.txt$.ttl`, filePath: `${rootFilepath}test.txt$.ttl`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });
@ -148,6 +177,7 @@ describe('An ExtensionBasedMapper', (): void => {
identifier: { path: `${base}test` }, identifier: { path: `${base}test` },
filePath: `${rootFilepath}test`, filePath: `${rootFilepath}test`,
contentType: 'application/octet-stream', contentType: 'application/octet-stream',
isMetadata: false,
}); });
}); });
}); });

View File

@ -13,56 +13,61 @@ describe('An FixedContentTypeMapper', (): void => {
describe('mapUrlToFilePath', (): void => { describe('mapUrlToFilePath', (): void => {
it('throws 404 if the input path does not contain the base.', async(): Promise<void> => { it('throws 404 if the input path does not contain the base.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: 'invalid' })).rejects.toThrow(NotFoundHttpError); await expect(mapper.mapUrlToFilePath({ path: 'invalid' }, false)).rejects.toThrow(NotFoundHttpError);
}); });
it('throws 404 if the relative path does not start with a slash.', async(): Promise<void> => { it('throws 404 if the relative path does not start with a slash.', async(): Promise<void> => {
const result = mapper.mapUrlToFilePath({ path: `${trimTrailingSlashes(base)}test` }); const result = mapper.mapUrlToFilePath({ path: `${trimTrailingSlashes(base)}test` }, false);
await expect(result).rejects.toThrow(BadRequestHttpError); await expect(result).rejects.toThrow(BadRequestHttpError);
await expect(result).rejects.toThrow('URL needs a / after the base'); await expect(result).rejects.toThrow('URL needs a / after the base');
}); });
it('throws 400 if the input path contains relative parts.', async(): Promise<void> => { it('throws 400 if the input path contains relative parts.', async(): Promise<void> => {
const result = mapper.mapUrlToFilePath({ path: `${base}test/../test2` }); const result = mapper.mapUrlToFilePath({ path: `${base}test/../test2` }, false);
await expect(result).rejects.toThrow(BadRequestHttpError); await expect(result).rejects.toThrow(BadRequestHttpError);
await expect(result).rejects.toThrow('Disallowed /.. segment in URL'); await expect(result).rejects.toThrow('Disallowed /.. segment in URL');
}); });
it('returns the corresponding file path for container identifiers.', async(): Promise<void> => { it('returns the corresponding file path for container identifiers.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}container/` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}container/` }, false)).resolves.toEqual({
identifier: { path: `${base}container/` }, identifier: { path: `${base}container/` },
filePath: `${rootFilepath}container/`, filePath: `${rootFilepath}container/`,
isMetadata: false,
}); });
}); });
it('always returns the configured content-type.', async(): Promise<void> => { it('always returns the configured content-type.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test` }, false)).resolves.toEqual({
identifier: { path: `${base}test` }, identifier: { path: `${base}test` },
filePath: `${rootFilepath}test`, filePath: `${rootFilepath}test`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, false)).resolves.toEqual({
identifier: { path: `${base}test.ttl` }, identifier: { path: `${base}test.ttl` },
filePath: `${rootFilepath}test.ttl`, filePath: `${rootFilepath}test.ttl`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` }, false)).resolves.toEqual({
identifier: { path: `${base}test.txt` }, identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}test.txt`, filePath: `${rootFilepath}test.txt`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });
it('generates a file path if supported content-type was provided.', async(): Promise<void> => { it('generates a file path if supported content-type was provided.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, 'text/turtle')).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, false, 'text/turtle')).resolves.toEqual({
identifier: { path: `${base}test.ttl` }, identifier: { path: `${base}test.ttl` },
filePath: `${rootFilepath}test.ttl`, filePath: `${rootFilepath}test.ttl`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });
it('throws 400 if the given content-type is not supported.', async(): Promise<void> => { it('throws 400 if the given content-type is not supported.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, 'application/n-quads')).rejects await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, false, 'application/n-quads')).rejects
.toThrow( .toThrow(
new BadRequestHttpError(`Unsupported content type application/n-quads, only text/turtle is allowed`), new BadRequestHttpError(`Unsupported content type application/n-quads, only text/turtle is allowed`),
); );
@ -78,6 +83,7 @@ describe('An FixedContentTypeMapper', (): void => {
await expect(mapper.mapFilePathToUrl(`${rootFilepath}container/`, true)).resolves.toEqual({ await expect(mapper.mapFilePathToUrl(`${rootFilepath}container/`, true)).resolves.toEqual({
identifier: { path: `${base}container/` }, identifier: { path: `${base}container/` },
filePath: `${rootFilepath}container/`, filePath: `${rootFilepath}container/`,
isMetadata: false,
}); });
}); });
@ -86,16 +92,19 @@ describe('An FixedContentTypeMapper', (): void => {
identifier: { path: `${base}test` }, identifier: { path: `${base}test` },
filePath: `${rootFilepath}test`, filePath: `${rootFilepath}test`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.ttl`, false)).resolves.toEqual({ await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.ttl`, false)).resolves.toEqual({
identifier: { path: `${base}test.ttl` }, identifier: { path: `${base}test.ttl` },
filePath: `${rootFilepath}test.ttl`, filePath: `${rootFilepath}test.ttl`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.txt`, false)).resolves.toEqual({ await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.txt`, false)).resolves.toEqual({
identifier: { path: `${base}test.txt` }, identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}test.txt`, filePath: `${rootFilepath}test.txt`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });
}); });
@ -107,40 +116,45 @@ describe('An FixedContentTypeMapper', (): void => {
describe('mapUrlToFilePath', (): void => { describe('mapUrlToFilePath', (): void => {
it('returns the corresponding file path for container identifiers.', async(): Promise<void> => { it('returns the corresponding file path for container identifiers.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}container/` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}container/` }, false)).resolves.toEqual({
identifier: { path: `${base}container/` }, identifier: { path: `${base}container/` },
filePath: `${rootFilepath}container/`, filePath: `${rootFilepath}container/`,
isMetadata: false,
}); });
}); });
it('always returns the configured content-type.', async(): Promise<void> => { it('always returns the configured content-type.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test` }, false)).resolves.toEqual({
identifier: { path: `${base}test` }, identifier: { path: `${base}test` },
filePath: `${rootFilepath}test.ttl`, filePath: `${rootFilepath}test.ttl`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, false)).resolves.toEqual({
identifier: { path: `${base}test.ttl` }, identifier: { path: `${base}test.ttl` },
filePath: `${rootFilepath}test.ttl.ttl`, filePath: `${rootFilepath}test.ttl.ttl`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt` }, false)).resolves.toEqual({
identifier: { path: `${base}test.txt` }, identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}test.txt.ttl`, filePath: `${rootFilepath}test.txt.ttl`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });
it('generates a file path if supported content-type was provided.', async(): Promise<void> => { it('generates a file path if supported content-type was provided.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, 'text/turtle')).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, false, 'text/turtle')).resolves.toEqual({
identifier: { path: `${base}test.ttl` }, identifier: { path: `${base}test.ttl` },
filePath: `${rootFilepath}test.ttl.ttl`, filePath: `${rootFilepath}test.ttl.ttl`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });
it('throws 400 if the given content-type is not supported.', async(): Promise<void> => { it('throws 400 if the given content-type is not supported.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, 'application/n-quads')).rejects await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, false, 'application/n-quads')).rejects
.toThrow( .toThrow(
new BadRequestHttpError(`Unsupported content type application/n-quads, only text/turtle is allowed`), new BadRequestHttpError(`Unsupported content type application/n-quads, only text/turtle is allowed`),
); );
@ -152,6 +166,7 @@ describe('An FixedContentTypeMapper', (): void => {
await expect(mapper.mapFilePathToUrl(`${rootFilepath}container/`, true)).resolves.toEqual({ await expect(mapper.mapFilePathToUrl(`${rootFilepath}container/`, true)).resolves.toEqual({
identifier: { path: `${base}container/` }, identifier: { path: `${base}container/` },
filePath: `${rootFilepath}container/`, filePath: `${rootFilepath}container/`,
isMetadata: false,
}); });
}); });
@ -160,6 +175,7 @@ describe('An FixedContentTypeMapper', (): void => {
identifier: { path: `${base}test` }, identifier: { path: `${base}test` },
filePath: `${rootFilepath}test.ttl`, filePath: `${rootFilepath}test.ttl`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });
@ -176,42 +192,46 @@ describe('An FixedContentTypeMapper', (): void => {
describe('mapUrlToFilePath', (): void => { describe('mapUrlToFilePath', (): void => {
it('returns the corresponding file path for container identifiers.', async(): Promise<void> => { it('returns the corresponding file path for container identifiers.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}container/` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}container/` }, false)).resolves.toEqual({
identifier: { path: `${base}container/` }, identifier: { path: `${base}container/` },
filePath: `${rootFilepath}container/`, filePath: `${rootFilepath}container/`,
isMetadata: false,
}); });
}); });
it('always returns the configured content-type.', async(): Promise<void> => { it('always returns the configured content-type.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, false)).resolves.toEqual({
identifier: { path: `${base}test.ttl` }, identifier: { path: `${base}test.ttl` },
filePath: `${rootFilepath}test`, filePath: `${rootFilepath}test`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt.ttl` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.txt.ttl` }, false)).resolves.toEqual({
identifier: { path: `${base}test.txt.ttl` }, identifier: { path: `${base}test.txt.ttl` },
filePath: `${rootFilepath}test.txt`, filePath: `${rootFilepath}test.txt`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });
it('generates a file path if supported content-type was provided.', async(): Promise<void> => { it('generates a file path if supported content-type was provided.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, 'text/turtle')).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, false, 'text/turtle')).resolves.toEqual({
identifier: { path: `${base}test.ttl` }, identifier: { path: `${base}test.ttl` },
filePath: `${rootFilepath}test`, filePath: `${rootFilepath}test`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });
it('throws 404 if the url does not end with the suffix.', async(): Promise<void> => { it('throws 404 if the url does not end with the suffix.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.nq` }, 'text/turtle')).rejects await expect(mapper.mapUrlToFilePath({ path: `${base}test.nq` }, false, 'text/turtle')).rejects
.toThrow(NotFoundHttpError); .toThrow(NotFoundHttpError);
await expect(mapper.mapUrlToFilePath({ path: `${base}test` }, 'text/turtle')).rejects await expect(mapper.mapUrlToFilePath({ path: `${base}test` }, false, 'text/turtle')).rejects
.toThrow(NotFoundHttpError); .toThrow(NotFoundHttpError);
}); });
it('throws 400 if the given content-type is not supported.', async(): Promise<void> => { it('throws 400 if the given content-type is not supported.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, 'application/n-quads')).rejects await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, false, 'application/n-quads')).rejects
.toThrow( .toThrow(
new BadRequestHttpError(`Unsupported content type application/n-quads, only text/turtle is allowed`), new BadRequestHttpError(`Unsupported content type application/n-quads, only text/turtle is allowed`),
); );
@ -223,6 +243,7 @@ describe('An FixedContentTypeMapper', (): void => {
await expect(mapper.mapFilePathToUrl(`${rootFilepath}container/`, true)).resolves.toEqual({ await expect(mapper.mapFilePathToUrl(`${rootFilepath}container/`, true)).resolves.toEqual({
identifier: { path: `${base}container/` }, identifier: { path: `${base}container/` },
filePath: `${rootFilepath}container/`, filePath: `${rootFilepath}container/`,
isMetadata: false,
}); });
}); });
@ -231,16 +252,19 @@ describe('An FixedContentTypeMapper', (): void => {
identifier: { path: `${base}test.ttl` }, identifier: { path: `${base}test.ttl` },
filePath: `${rootFilepath}test`, filePath: `${rootFilepath}test`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.ttl`, false)).resolves.toEqual({ await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.ttl`, false)).resolves.toEqual({
identifier: { path: `${base}test.ttl.ttl` }, identifier: { path: `${base}test.ttl.ttl` },
filePath: `${rootFilepath}test.ttl`, filePath: `${rootFilepath}test.ttl`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.txt`, false)).resolves.toEqual({ await expect(mapper.mapFilePathToUrl(`${rootFilepath}test.txt`, false)).resolves.toEqual({
identifier: { path: `${base}test.txt.ttl` }, identifier: { path: `${base}test.txt.ttl` },
filePath: `${rootFilepath}test.txt`, filePath: `${rootFilepath}test.txt`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });
}); });
@ -252,24 +276,26 @@ describe('An FixedContentTypeMapper', (): void => {
describe('mapUrlToFilePath', (): void => { describe('mapUrlToFilePath', (): void => {
it('returns the corresponding file path for container identifiers.', async(): Promise<void> => { it('returns the corresponding file path for container identifiers.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}container/` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}container/` }, false)).resolves.toEqual({
identifier: { path: `${base}container/` }, identifier: { path: `${base}container/` },
filePath: `${rootFilepath}container/`, filePath: `${rootFilepath}container/`,
isMetadata: false,
}); });
}); });
it('always returns the configured content-type.', async(): Promise<void> => { it('always returns the configured content-type.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` })).resolves.toEqual({ await expect(mapper.mapUrlToFilePath({ path: `${base}test.ttl` }, false)).resolves.toEqual({
identifier: { path: `${base}test.ttl` }, identifier: { path: `${base}test.ttl` },
filePath: `${rootFilepath}test.nq`, filePath: `${rootFilepath}test.nq`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });
it('throws 404 if the url does not end with the suffix.', async(): Promise<void> => { it('throws 404 if the url does not end with the suffix.', async(): Promise<void> => {
await expect(mapper.mapUrlToFilePath({ path: `${base}test.nq` }, 'text/turtle')).rejects await expect(mapper.mapUrlToFilePath({ path: `${base}test.nq` }, false, 'text/turtle')).rejects
.toThrow(NotFoundHttpError); .toThrow(NotFoundHttpError);
await expect(mapper.mapUrlToFilePath({ path: `${base}test` }, 'text/turtle')).rejects await expect(mapper.mapUrlToFilePath({ path: `${base}test` }, false, 'text/turtle')).rejects
.toThrow(NotFoundHttpError); .toThrow(NotFoundHttpError);
}); });
}); });
@ -279,6 +305,7 @@ describe('An FixedContentTypeMapper', (): void => {
await expect(mapper.mapFilePathToUrl(`${rootFilepath}container/`, true)).resolves.toEqual({ await expect(mapper.mapFilePathToUrl(`${rootFilepath}container/`, true)).resolves.toEqual({
identifier: { path: `${base}container/` }, identifier: { path: `${base}container/` },
filePath: `${rootFilepath}container/`, filePath: `${rootFilepath}container/`,
isMetadata: false,
}); });
}); });
@ -287,6 +314,7 @@ describe('An FixedContentTypeMapper', (): void => {
identifier: { path: `${base}test.ttl` }, identifier: { path: `${base}test.ttl` },
filePath: `${rootFilepath}test.nq`, filePath: `${rootFilepath}test.nq`,
contentType: 'text/turtle', contentType: 'text/turtle',
isMetadata: false,
}); });
}); });

View File

@ -15,39 +15,42 @@ describe('A SubdomainExtensionBasedMapper', (): void => {
describe('mapUrlToFilePath', (): void => { describe('mapUrlToFilePath', (): void => {
it('converts file paths to identifiers with a subdomain.', async(): Promise<void> => { it('converts file paths to identifiers with a subdomain.', async(): Promise<void> => {
const identifier = { path: `${getSubdomain('alice')}test.txt` }; const identifier = { path: `${getSubdomain('alice')}test.txt` };
await expect(mapper.mapUrlToFilePath(identifier, 'text/plain')).resolves.toEqual({ await expect(mapper.mapUrlToFilePath(identifier, false, 'text/plain')).resolves.toEqual({
identifier, identifier,
filePath: `${rootFilepath}alice/test.txt`, filePath: `${rootFilepath}alice/test.txt`,
contentType: 'text/plain', contentType: 'text/plain',
isMetadata: false,
}); });
}); });
it('adds the default subdomain to the file path for root identifiers.', async(): Promise<void> => { it('adds the default subdomain to the file path for root identifiers.', async(): Promise<void> => {
const identifier = { path: `${base}test.txt` }; const identifier = { path: `${base}test.txt` };
await expect(mapper.mapUrlToFilePath(identifier, 'text/plain')).resolves.toEqual({ await expect(mapper.mapUrlToFilePath(identifier, false, 'text/plain')).resolves.toEqual({
identifier, identifier,
filePath: `${rootFilepath}www/test.txt`, filePath: `${rootFilepath}www/test.txt`,
contentType: 'text/plain', contentType: 'text/plain',
isMetadata: false,
}); });
}); });
it('decodes punycode when generating a file path.', async(): Promise<void> => { it('decodes punycode when generating a file path.', async(): Promise<void> => {
const identifier = { path: `${getSubdomain('xn--c1yn36f')}t%20est.txt` }; const identifier = { path: `${getSubdomain('xn--c1yn36f')}t%20est.txt` };
await expect(mapper.mapUrlToFilePath(identifier, 'text/plain')).resolves.toEqual({ await expect(mapper.mapUrlToFilePath(identifier, false, 'text/plain')).resolves.toEqual({
identifier, identifier,
filePath: `${rootFilepath}點看/t est.txt`, filePath: `${rootFilepath}點看/t est.txt`,
contentType: 'text/plain', contentType: 'text/plain',
isMetadata: false,
}); });
}); });
it('errors if the path is invalid.', async(): Promise<void> => { it('errors if the path is invalid.', async(): Promise<void> => {
const identifier = { path: `veryinvalidpath` }; const identifier = { path: `veryinvalidpath` };
await expect(mapper.mapUrlToFilePath(identifier, 'text/plain')).rejects.toThrow(NotFoundHttpError); await expect(mapper.mapUrlToFilePath(identifier, false, 'text/plain')).rejects.toThrow(NotFoundHttpError);
}); });
it('errors if the subdomain matches the default one.', async(): Promise<void> => { it('errors if the subdomain matches the default one.', async(): Promise<void> => {
const identifier = { path: `${getSubdomain('www')}test.txt` }; const identifier = { path: `${getSubdomain('www')}test.txt` };
await expect(mapper.mapUrlToFilePath(identifier, 'text/plain')).rejects.toThrow(ForbiddenHttpError); await expect(mapper.mapUrlToFilePath(identifier, false, 'text/plain')).rejects.toThrow(ForbiddenHttpError);
}); });
}); });
@ -57,6 +60,7 @@ describe('A SubdomainExtensionBasedMapper', (): void => {
identifier: { path: `${getSubdomain('alice')}test.txt` }, identifier: { path: `${getSubdomain('alice')}test.txt` },
filePath: `${rootFilepath}alice/test.txt`, filePath: `${rootFilepath}alice/test.txt`,
contentType: 'text/plain', contentType: 'text/plain',
isMetadata: false,
}); });
}); });
@ -64,6 +68,7 @@ describe('A SubdomainExtensionBasedMapper', (): void => {
await expect(mapper.mapFilePathToUrl(`${rootFilepath}alice/test.txt`, true)).resolves.toEqual({ await expect(mapper.mapFilePathToUrl(`${rootFilepath}alice/test.txt`, true)).resolves.toEqual({
identifier: { path: `${getSubdomain('alice')}test.txt/` }, identifier: { path: `${getSubdomain('alice')}test.txt/` },
filePath: `${rootFilepath}alice/test.txt`, filePath: `${rootFilepath}alice/test.txt`,
isMetadata: false,
}); });
}); });
@ -72,6 +77,7 @@ describe('A SubdomainExtensionBasedMapper', (): void => {
identifier: { path: `${base}test.txt` }, identifier: { path: `${base}test.txt` },
filePath: `${rootFilepath}www/test.txt`, filePath: `${rootFilepath}www/test.txt`,
contentType: 'text/plain', contentType: 'text/plain',
isMetadata: false,
}); });
}); });
@ -80,6 +86,7 @@ describe('A SubdomainExtensionBasedMapper', (): void => {
identifier: { path: `${getSubdomain('xn--c1yn36f')}t%20est.txt` }, identifier: { path: `${getSubdomain('xn--c1yn36f')}t%20est.txt` },
filePath: `${rootFilepath}點看/t est.txt`, filePath: `${rootFilepath}點看/t est.txt`,
contentType: 'text/plain', contentType: 'text/plain',
isMetadata: false,
}); });
}); });