refactor: Clean up utility functions

This commit is contained in:
Joachim Van Herwegen
2020-11-18 15:52:07 +01:00
parent 82f3aa0cd8
commit 1073c2ff4c
54 changed files with 482 additions and 646 deletions

View File

@@ -5,12 +5,12 @@ import type { PermissionSet } from '../ldp/permissions/PermissionSet';
import type { Representation } from '../ldp/representation/Representation';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../logging/LogUtil';
import type { ContainerManager } from '../storage/ContainerManager';
import type { ResourceStore } from '../storage/ResourceStore';
import { INTERNAL_QUADS } from '../util/ContentTypes';
import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError';
import { getParentContainer } from '../util/PathUtil';
import { ACL, FOAF } from '../util/UriConstants';
import type { AclManager } from './AclManager';
import type { AuthorizerArgs } from './Authorizer';
@@ -25,13 +25,11 @@ export class WebAclAuthorizer extends Authorizer {
protected readonly logger = getLoggerFor(this);
private readonly aclManager: AclManager;
private readonly containerManager: ContainerManager;
private readonly resourceStore: ResourceStore;
public constructor(aclManager: AclManager, containerManager: ContainerManager, resourceStore: ResourceStore) {
public constructor(aclManager: AclManager, resourceStore: ResourceStore) {
super();
this.aclManager = aclManager;
this.containerManager = containerManager;
this.resourceStore = resourceStore;
}
@@ -134,7 +132,7 @@ export class WebAclAuthorizer extends Authorizer {
}
this.logger.debug(`Traversing to the parent of ${id.path}`);
const parent = await this.containerManager.getContainer(id);
const parent = getParentContainer(id);
return this.getAclRecursive(parent, true);
}

View File

@@ -2,7 +2,7 @@ import { getLoggerFor } from '../../logging/LogUtil';
import type { HttpResponse } from '../../server/HttpResponse';
import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { pipeSafe } from '../../util/Util';
import { pipeSafely } from '../../util/StreamUtil';
import type { MetadataWriter } from './metadata/MetadataWriter';
import type { ResponseDescription } from './response/ResponseDescription';
import { ResponseWriter } from './ResponseWriter';
@@ -34,7 +34,7 @@ export class BasicResponseWriter extends ResponseWriter {
input.response.writeHead(input.result.statusCode);
if (input.result.data) {
const pipe = pipeSafe(input.result.data, input.response);
const pipe = pipeSafely(input.result.data, input.response);
pipe.on('error', (error): void => {
this.logger.error(`Writing to HttpResponse failed with message ${error.message}`);
});

View File

@@ -1,7 +1,7 @@
import type { TLSSocket } from 'tls';
import { getLoggerFor } from '../../logging/LogUtil';
import type { HttpRequest } from '../../server/HttpRequest';
import { toCanonicalUriPath } from '../../util/Util';
import { toCanonicalUriPath } from '../../util/PathUtil';
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
import { TargetExtractor } from './TargetExtractor';

View File

@@ -5,7 +5,7 @@ import { getLoggerFor } from '../../logging/LogUtil';
import { APPLICATION_SPARQL_UPDATE } from '../../util/ContentTypes';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
import { pipeSafe, readableToString } from '../../util/Util';
import { pipeSafely, readableToString } from '../../util/StreamUtil';
import type { BodyParserArgs } from './BodyParser';
import { BodyParser } from './BodyParser';
import type { SparqlUpdatePatch } from './SparqlUpdatePatch';
@@ -29,8 +29,8 @@ export class SparqlUpdateBodyParser extends BodyParser {
// Note that readableObjectMode is only defined starting from Node 12
// It is impossible to check if object mode is enabled in Node 10 (without accessing private variables)
const options = { objectMode: request.readableObjectMode };
const toAlgebraStream = pipeSafe(request, new PassThrough(options));
const dataCopy = pipeSafe(request, new PassThrough(options));
const toAlgebraStream = pipeSafely(request, new PassThrough(options));
const dataCopy = pipeSafely(request, new PassThrough(options));
let algebra: Algebra.Operation;
try {
const sparql = await readableToString(toAlgebraStream);

View File

@@ -1,5 +1,5 @@
import type { HttpResponse } from '../../../server/HttpResponse';
import { addHeader } from '../../../util/Util';
import { addHeader } from '../../../util/HeaderUtil';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { MetadataWriter } from './MetadataWriter';

View File

@@ -1,5 +1,5 @@
import type { HttpResponse } from '../../../server/HttpResponse';
import { addHeader } from '../../../util/Util';
import { addHeader } from '../../../util/HeaderUtil';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { MetadataWriter } from './MetadataWriter';

View File

@@ -1,16 +0,0 @@
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
/**
* Handles the identification of containers in which a resource is contained.
*/
export interface ContainerManager {
/**
* Finds the corresponding container.
* Should throw an error if there is no such container (in the case of root).
*
* @param id - Identifier to find container of.
*
* @returns The identifier of the container this resource is in.
*/
getContainer: (id: ResourceIdentifier) => Promise<ResourceIdentifier>;
}

View File

@@ -12,11 +12,11 @@ import { MethodNotAllowedHttpError } from '../util/errors/MethodNotAllowedHttpEr
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { NotImplementedError } from '../util/errors/NotImplementedError';
import { UnsupportedHttpError } from '../util/errors/UnsupportedHttpError';
import type { MetadataController } from '../util/MetadataController';
import { ensureTrailingSlash, getParentContainer, trimTrailingSlashes } from '../util/PathUtil';
import { parseQuads } from '../util/QuadUtil';
import { generateResourceQuads } from '../util/ResourceUtil';
import { CONTENT_TYPE, HTTP, LDP, RDF } from '../util/UriConstants';
import { ensureTrailingSlash, trimTrailingSlashes } from '../util/Util';
import type { DataAccessor } from './accessors/DataAccessor';
import type { ContainerManager } from './ContainerManager';
import type { ResourceStore } from './ResourceStore';
/**
@@ -45,15 +45,10 @@ import type { ResourceStore } from './ResourceStore';
export class DataAccessorBasedStore implements ResourceStore {
private readonly accessor: DataAccessor;
private readonly base: string;
private readonly metadataController: MetadataController;
private readonly containerManager: ContainerManager;
public constructor(accessor: DataAccessor, base: string, metadataController: MetadataController,
containerManager: ContainerManager) {
public constructor(accessor: DataAccessor, base: string) {
this.accessor = accessor;
this.base = ensureTrailingSlash(base);
this.metadataController = metadataController;
this.containerManager = containerManager;
}
public async getRepresentation(identifier: ResourceIdentifier): Promise<Representation> {
@@ -219,13 +214,13 @@ export class DataAccessorBasedStore implements ResourceStore {
}
if (createContainers) {
await this.createRecursiveContainers(await this.containerManager.getContainer(identifier));
await this.createRecursiveContainers(getParentContainer(identifier));
}
// Make sure the metadata has the correct identifier and correct type quads
const { metadata } = representation;
metadata.identifier = DataFactory.namedNode(identifier.path);
metadata.addQuads(this.metadataController.generateResourceQuads(metadata.identifier, isContainer));
metadata.addQuads(generateResourceQuads(metadata.identifier, isContainer));
await (isContainer ?
this.accessor.writeContainer(identifier, representation.metadata) :
@@ -241,7 +236,7 @@ export class DataAccessorBasedStore implements ResourceStore {
protected async handleContainerData(representation: Representation): Promise<void> {
let quads: Quad[];
try {
quads = await this.metadataController.parseQuads(representation.data);
quads = await parseQuads(representation.data);
} catch (error: unknown) {
if (error instanceof Error) {
throw new UnsupportedHttpError(`Can only create containers with RDF data. ${error.message}`);
@@ -349,7 +344,7 @@ export class DataAccessorBasedStore implements ResourceStore {
} catch (error: unknown) {
if (error instanceof NotFoundHttpError) {
// Make sure the parent exists first
await this.createRecursiveContainers(await this.containerManager.getContainer(container));
await this.createRecursiveContainers(getParentContainer(container));
await this.writeData(container, this.getEmptyContainerRepresentation(container), true);
} else {
throw error;

View File

@@ -11,7 +11,7 @@ import {
encodeUriPathComponents,
ensureTrailingSlash,
trimTrailingSlashes,
} from '../util/Util';
} from '../util/PathUtil';
import type { FileIdentifierMapper, ResourceLink } from './FileIdentifierMapper';
const { join: joinPath, normalize: normalizePath } = posix;

View File

@@ -1,34 +0,0 @@
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { ensureTrailingSlash } from '../util/Util';
import type { ContainerManager } from './ContainerManager';
/**
* Determines containers based on URL decomposition.
*/
export class UrlContainerManager implements ContainerManager {
private readonly base: string;
public constructor(base: string) {
this.base = base;
}
public async getContainer(id: ResourceIdentifier): Promise<ResourceIdentifier> {
const path = this.canonicalUrl(id.path);
if (this.base === path) {
throw new Error('Root does not have a container');
}
const parentPath = new URL('..', path).toString();
// This probably means there is an issue with the root
if (parentPath === path) {
throw new Error('URL root reached');
}
return { path: parentPath };
}
private canonicalUrl(path: string): string {
return ensureTrailingSlash(path.toString());
}
}

View File

@@ -12,10 +12,10 @@ import { ConflictHttpError } from '../../util/errors/ConflictHttpError';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import { isSystemError } from '../../util/errors/SystemError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
import type { MetadataController } from '../../util/MetadataController';
import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil';
import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
import { CONTENT_TYPE, DCTERMS, POSIX, RDF, XSD } from '../../util/UriConstants';
import { toNamedNode, toTypedLiteral } from '../../util/UriUtil';
import { pushQuad } from '../../util/Util';
import type { FileIdentifierMapper, ResourceLink } from '../FileIdentifierMapper';
import type { DataAccessor } from './DataAccessor';
@@ -26,11 +26,9 @@ const { join: joinPath } = posix;
*/
export class FileDataAccessor implements DataAccessor {
private readonly resourceMapper: FileIdentifierMapper;
private readonly metadataController: MetadataController;
public constructor(resourceMapper: FileIdentifierMapper, metadataController: MetadataController) {
public constructor(resourceMapper: FileIdentifierMapper) {
this.resourceMapper = resourceMapper;
this.metadataController = metadataController;
}
/**
@@ -218,7 +216,7 @@ export class FileDataAccessor implements DataAccessor {
// Write metadata to file if there are quads remaining
if (quads.length > 0) {
const serializedMetadata = this.metadataController.serializeQuads(quads);
const serializedMetadata = serializeQuads(quads);
await this.writeDataFile(metadataPath, serializedMetadata);
wroteMetadata = true;
@@ -247,7 +245,7 @@ export class FileDataAccessor implements DataAccessor {
Promise<RepresentationMetadata> {
const metadata = new RepresentationMetadata(link.identifier.path)
.addQuads(await this.getRawMetadata(link.identifier));
metadata.addQuads(this.metadataController.generateResourceQuads(metadata.identifier as NamedNode, isContainer));
metadata.addQuads(generateResourceQuads(metadata.identifier as NamedNode, isContainer));
metadata.addQuads(this.generatePosixQuads(metadata.identifier as NamedNode, stats));
return metadata;
}
@@ -266,7 +264,7 @@ export class FileDataAccessor implements DataAccessor {
await fsPromises.lstat(metadataPath);
const readMetadataStream = createReadStream(metadataPath);
return await this.metadataController.parseQuads(readMetadataStream);
return await parseQuads(readMetadataStream);
} catch (error: unknown) {
// Metadata file doesn't exist so lets keep `rawMetaData` an empty array.
if (!isSystemError(error) || error.code !== 'ENOENT') {
@@ -306,13 +304,13 @@ export class FileDataAccessor implements DataAccessor {
// Generate metadata of this specific child
const subject = DataFactory.namedNode(childLink.identifier.path);
quads.push(...this.metadataController.generateResourceQuads(subject, childStats.isDirectory()));
quads.push(...generateResourceQuads(subject, childStats.isDirectory()));
quads.push(...this.generatePosixQuads(subject, childStats));
childURIs.push(childLink.identifier.path);
}
// Generate containment metadata
const containsQuads = this.metadataController.generateContainerContainsResourceQuads(
const containsQuads = generateContainmentQuads(
DataFactory.namedNode(link.identifier.path), childURIs,
);

View File

@@ -5,8 +5,8 @@ import type { NamedNode } from 'rdf-js';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import type { MetadataController } from '../../util/MetadataController';
import { ensureTrailingSlash } from '../../util/Util';
import { ensureTrailingSlash } from '../../util/PathUtil';
import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
import type { DataAccessor } from './DataAccessor';
interface DataEntry {
@@ -43,14 +43,12 @@ class ArrayReadable extends Readable {
export class InMemoryDataAccessor implements DataAccessor {
private readonly base: string;
private readonly store: ContainerEntry;
private readonly metadataController: MetadataController;
public constructor(base: string, metadataController: MetadataController) {
public constructor(base: string) {
this.base = ensureTrailingSlash(base);
this.metadataController = metadataController;
const metadata = new RepresentationMetadata(this.base);
metadata.addQuads(this.metadataController.generateResourceQuads(DataFactory.namedNode(this.base), true));
metadata.addQuads(generateResourceQuads(DataFactory.namedNode(this.base), true));
this.store = { entries: {}, metadata };
}
@@ -161,8 +159,7 @@ export class InMemoryDataAccessor implements DataAccessor {
if (!this.isDataEntry(entry)) {
const childNames = Object.keys(entry.entries).map((name): string =>
`${identifier.path}${name}${this.isDataEntry(entry.entries[name]) ? '' : '/'}`);
const quads = this.metadataController
.generateContainerContainsResourceQuads(metadata.identifier as NamedNode, childNames);
const quads = generateContainmentQuads(metadata.identifier as NamedNode, childNames);
metadata.addQuads(quads);
}
return metadata;

View File

@@ -23,11 +23,10 @@ import { ConflictHttpError } from '../../util/errors/ConflictHttpError';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
import type { MetadataController } from '../../util/MetadataController';
import { ensureTrailingSlash, getParentContainer } from '../../util/PathUtil';
import { generateResourceQuads } from '../../util/ResourceUtil';
import { CONTENT_TYPE, LDP } from '../../util/UriConstants';
import { toNamedNode } from '../../util/UriUtil';
import { ensureTrailingSlash } from '../../util/Util';
import type { ContainerManager } from '../ContainerManager';
import type { DataAccessor } from './DataAccessor';
const { defaultGraph, namedNode, quad, variable } = DataFactory;
@@ -48,17 +47,12 @@ export class SparqlDataAccessor implements DataAccessor {
protected readonly logger = getLoggerFor(this);
private readonly endpoint: string;
private readonly base: string;
private readonly containerManager: ContainerManager;
private readonly metadataController: MetadataController;
private readonly fetcher: SparqlEndpointFetcher;
private readonly generator: SparqlGenerator;
public constructor(endpoint: string, base: string, containerManager: ContainerManager,
metadataController: MetadataController) {
public constructor(endpoint: string, base: string) {
this.endpoint = endpoint;
this.base = ensureTrailingSlash(base);
this.containerManager = containerManager;
this.metadataController = metadataController;
this.fetcher = new SparqlEndpointFetcher();
this.generator = new Generator();
}
@@ -103,7 +97,7 @@ export class SparqlDataAccessor implements DataAccessor {
// Need to generate type metadata for the root container since it's not stored
if (identifier.path === this.base) {
metadata.addQuads(this.metadataController.generateResourceQuads(name, true));
metadata.addQuads(generateResourceQuads(name, true));
}
return metadata;
@@ -113,7 +107,7 @@ export class SparqlDataAccessor implements DataAccessor {
* Writes the given metadata for the container.
*/
public async writeContainer(identifier: ResourceIdentifier, metadata: RepresentationMetadata): Promise<void> {
const { name, parent } = await this.getRelatedNames(identifier);
const { name, parent } = this.getRelatedNames(identifier);
return this.sendSparqlUpdate(this.sparqlInsert(name, parent, metadata));
}
@@ -125,7 +119,7 @@ export class SparqlDataAccessor implements DataAccessor {
if (this.isMetadataIdentifier(identifier)) {
throw new ConflictHttpError('Not allowed to create NamedNodes with the metadata extension.');
}
const { name, parent } = await this.getRelatedNames(identifier);
const { name, parent } = this.getRelatedNames(identifier);
const triples = await arrayifyStream(data) as Quad[];
const def = defaultGraph();
@@ -143,15 +137,15 @@ export class SparqlDataAccessor implements DataAccessor {
* Removes all graph data relevant to the given identifier.
*/
public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
const { name, parent } = await this.getRelatedNames(identifier);
const { name, parent } = this.getRelatedNames(identifier);
return this.sendSparqlUpdate(this.sparqlDelete(name, parent));
}
/**
* Helper function to get named nodes corresponding to the identifier and its parent container.
*/
private async getRelatedNames(identifier: ResourceIdentifier): Promise<{ name: NamedNode; parent: NamedNode }> {
const parentIdentifier = await this.containerManager.getContainer(identifier);
private getRelatedNames(identifier: ResourceIdentifier): { name: NamedNode; parent: NamedNode } {
const parentIdentifier = getParentContainer(identifier);
const name = namedNode(identifier.path);
const parent = namedNode(parentIdentifier.path);
return { name, parent };

View File

@@ -1,7 +1,6 @@
import type { Representation } from '../../ldp/representation/Representation';
import { getLoggerFor } from '../../logging/LogUtil';
import { matchingMediaType } from '../../util/Util';
import { checkRequest } from './ConversionUtil';
import { validateRequestArgs, matchingMediaType } from './ConversionUtil';
import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
@@ -48,7 +47,7 @@ export class ChainedConverter extends TypedRepresentationConverter {
// So we only check if the input can be parsed and the preferred type can be written
const inTypes = this.filterTypes(await this.first.getInputTypes());
const outTypes = this.filterTypes(await this.last.getOutputTypes());
checkRequest(input, inTypes, outTypes);
validateRequestArgs(input, inTypes, outTypes);
}
private filterTypes(typeVals: Record<string, number>): string[] {

View File

@@ -3,7 +3,6 @@ import type { RepresentationPreferences } from '../../ldp/representation/Represe
import { INTERNAL_ALL } from '../../util/ContentTypes';
import { InternalServerError } from '../../util/errors/InternalServerError';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { matchingMediaType } from '../../util/Util';
import type { RepresentationConverterArgs } from './RepresentationConverter';
/**
@@ -57,6 +56,33 @@ RepresentationPreference[] => {
return weightedSupported.filter((preference): boolean => preference.weight !== 0);
};
/**
* Checks if the given two media types/ranges match each other.
* Takes wildcards into account.
* @param mediaA - Media type to match.
* @param mediaB - Media type to match.
*
* @returns True if the media type patterns can match each other.
*/
export const matchingMediaType = (mediaA: string, mediaB: string): boolean => {
if (mediaA === mediaB) {
return true;
}
const [ typeA, subTypeA ] = mediaA.split('/');
const [ typeB, subTypeB ] = mediaB.split('/');
if (typeA === '*' || typeB === '*') {
return true;
}
if (typeA !== typeB) {
return false;
}
if (subTypeA === '*' || subTypeB === '*') {
return true;
}
return subTypeA === subTypeB;
};
/**
* Runs some standard checks on the input request:
* - Checks if there is a content type for the input.
@@ -66,8 +92,8 @@ RepresentationPreference[] => {
* @param supportedIn - Media types that can be parsed by the converter.
* @param supportedOut - Media types that can be produced by the converter.
*/
export const checkRequest = (request: RepresentationConverterArgs, supportedIn: string[], supportedOut: string[]):
void => {
export const validateRequestArgs = (request: RepresentationConverterArgs, supportedIn: string[],
supportedOut: string[]): void => {
const inType = request.representation.metadata.contentType;
if (!inType) {
throw new UnsupportedHttpError('Input type required for conversion.');

View File

@@ -5,7 +5,7 @@ import { RepresentationMetadata } from '../../ldp/representation/RepresentationM
import type { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences';
import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { CONTENT_TYPE } from '../../util/UriConstants';
import { checkRequest, matchingTypes } from './ConversionUtil';
import { validateRequestArgs, matchingTypes } from './ConversionUtil';
import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
@@ -22,7 +22,7 @@ export class QuadToRdfConverter extends TypedRepresentationConverter {
}
public async canHandle(input: RepresentationConverterArgs): Promise<void> {
checkRequest(input, [ INTERNAL_QUADS ], await rdfSerializer.getContentTypes());
validateRequestArgs(input, [ INTERNAL_QUADS ], await rdfSerializer.getContentTypes());
}
public async handle(input: RepresentationConverterArgs): Promise<Representation> {

View File

@@ -4,9 +4,9 @@ import type { Representation } from '../../ldp/representation/Representation';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { pipeSafely } from '../../util/StreamUtil';
import { CONTENT_TYPE } from '../../util/UriConstants';
import { pipeSafe } from '../../util/Util';
import { checkRequest } from './ConversionUtil';
import { validateRequestArgs } from './ConversionUtil';
import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
@@ -23,7 +23,7 @@ export class RdfToQuadConverter extends TypedRepresentationConverter {
}
public async canHandle(input: RepresentationConverterArgs): Promise<void> {
checkRequest(input, await rdfParser.getContentTypes(), [ INTERNAL_QUADS ]);
validateRequestArgs(input, await rdfParser.getContentTypes(), [ INTERNAL_QUADS ]);
}
public async handle(input: RepresentationConverterArgs): Promise<Representation> {
@@ -40,7 +40,7 @@ export class RdfToQuadConverter extends TypedRepresentationConverter {
// Wrap the stream such that errors are transformed
// (Node 10 requires both writableObjectMode and readableObjectMode)
const pass = new PassThrough({ writableObjectMode: true, readableObjectMode: true });
const data = pipeSafe(rawQuads, pass, (error): Error => new UnsupportedHttpError(error.message));
const data = pipeSafely(rawQuads, pass, (error): Error => new UnsupportedHttpError(error.message));
return {
binary: false,

View File

@@ -1,7 +1,7 @@
import type { Representation } from '../../ldp/representation/Representation';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { trimTrailingSlashes } from '../../util/Util';
import { trimTrailingSlashes } from '../../util/PathUtil';
import type { ResourceStore } from '../ResourceStore';
import { RouterRule } from './RouterRule';

View File

@@ -1,4 +1,5 @@
import { getLoggerFor } from '../logging/LogUtil';
import type { HttpResponse } from '../server/HttpResponse';
import { UnsupportedHttpError } from './errors/UnsupportedHttpError';
const logger = getLoggerFor('HeaderUtil');
@@ -355,3 +356,25 @@ export const parseAcceptLanguage = (input: string): AcceptLanguage[] => {
});
return results;
};
/**
* Adds a header value without overriding previous values.
*/
export const addHeader = (response: HttpResponse, name: string, value: string | string[]): void => {
let allValues: string[] = [];
if (response.hasHeader(name)) {
let oldValues = response.getHeader(name)!;
if (typeof oldValues === 'string') {
oldValues = [ oldValues ];
} else if (typeof oldValues === 'number') {
oldValues = [ `${oldValues}` ];
}
allValues = oldValues;
}
if (Array.isArray(value)) {
allValues.push(...value);
} else {
allValues.push(value);
}
response.setHeader(name, allValues.length === 1 ? allValues[0] : allValues);
};

View File

@@ -1,61 +0,0 @@
import type { Readable } from 'stream';
import arrayifyStream from 'arrayify-stream';
import { DataFactory, StreamParser, StreamWriter } from 'n3';
import type { NamedNode, Quad } from 'rdf-js';
import streamifyArray from 'streamify-array';
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
import { TEXT_TURTLE } from './ContentTypes';
import { LDP, RDF } from './UriConstants';
import { toNamedNode } from './UriUtil';
import { pipeSafe, pushQuad } from './Util';
export class MetadataController {
/**
* Helper function to generate type quads for a Container or Resource.
* @param subject - Subject for the new quads.
* @param isContainer - If the identifier corresponds to a container.
*
* @returns The generated quads.
*/
public generateResourceQuads(subject: NamedNode, isContainer: boolean): Quad[] {
const quads: Quad[] = [];
if (isContainer) {
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Container));
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.BasicContainer));
}
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Resource));
return quads;
}
/**
* Helper function to generate the quads describing that the resource URIs are children of the container URI.
* @param containerURI - The URI of the container.
* @param childURIs - The URI of the child resources.
*
* @returns The generated quads.
*/
public generateContainerContainsResourceQuads(containerURI: NamedNode, childURIs: string[]): Quad[] {
return new RepresentationMetadata(containerURI, { [LDP.contains]: childURIs.map(DataFactory.namedNode) }).quads();
}
/**
* Helper function for serializing an array of quads, with as result a Readable object.
* @param quads - The array of quads.
*
* @returns The Readable object.
*/
public serializeQuads(quads: Quad[]): Readable {
return pipeSafe(streamifyArray(quads), new StreamWriter({ format: TEXT_TURTLE }));
}
/**
* Helper function to convert a Readable into an array of quads.
* @param readable - The readable object.
*
* @returns A promise containing the array of quads.
*/
public async parseQuads(readable: Readable): Promise<Quad[]> {
return await arrayifyStream(pipeSafe(readable, new StreamParser({ format: TEXT_TURTLE })));
}
}

60
src/util/PathUtil.ts Normal file
View File

@@ -0,0 +1,60 @@
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { InternalServerError } from './errors/InternalServerError';
/**
* Makes sure the input path has exactly 1 slash at the end.
* Multiple slashes will get merged into one.
* If there is no slash it will be added.
*
* @param path - Path to check.
*
* @returns The potentially changed path.
*/
export const ensureTrailingSlash = (path: string): string => path.replace(/\/*$/u, '/');
/**
* Makes sure the input path has no slashes at the end.
*
* @param path - Path to check.
*
* @returns The potentially changed path.
*/
export const trimTrailingSlashes = (path: string): string => path.replace(/\/+$/u, '');
/**
* Converts a URI path to the canonical version by splitting on slashes,
* decoding any percent-based encodings,
* and then encoding any special characters.
*/
export const toCanonicalUriPath = (path: string): string => path.split('/').map((part): string =>
encodeURIComponent(decodeURIComponent(part))).join('/');
/**
* Decodes all components of a URI path.
*/
export const decodeUriPathComponents = (path: string): string => path.split('/').map(decodeURIComponent).join('/');
/**
* Encodes all (non-slash) special characters in a URI path.
*/
export const encodeUriPathComponents = (path: string): string => path.split('/').map(encodeURIComponent).join('/');
/**
* Finds the container containing the given resource.
* This does not ensure either the container or resource actually exist.
*
* @param id - Identifier to find container of.
*
* @returns The identifier of the container this resource is in.
*/
export const getParentContainer = (id: ResourceIdentifier): ResourceIdentifier => {
// Trailing slash is necessary for URL library
const parentPath = new URL('..', ensureTrailingSlash(id.path)).toString();
// This probably means there is an issue with the root
if (parentPath === id.path) {
throw new InternalServerError('URL root reached');
}
return { path: parentPath };
};

32
src/util/QuadUtil.ts Normal file
View File

@@ -0,0 +1,32 @@
import type { Readable } from 'stream';
import arrayifyStream from 'arrayify-stream';
import { DataFactory, StreamParser, StreamWriter } from 'n3';
import type { Literal, NamedNode, Quad } from 'rdf-js';
import streamifyArray from 'streamify-array';
import { TEXT_TURTLE } from './ContentTypes';
import { pipeSafely } from './StreamUtil';
/**
* Generates a quad with the given subject/predicate/object and pushes it to the given array.
*/
export const pushQuad =
(quads: Quad[], subject: NamedNode, predicate: NamedNode, object: NamedNode | Literal): number =>
quads.push(DataFactory.quad(subject, predicate, object));
/**
* Helper function for serializing an array of quads, with as result a Readable object.
* @param quads - The array of quads.
*
* @returns The Readable object.
*/
export const serializeQuads = (quads: Quad[]): Readable =>
pipeSafely(streamifyArray(quads), new StreamWriter({ format: TEXT_TURTLE }));
/**
* Helper function to convert a Readable into an array of quads.
* @param readable - The readable object.
*
* @returns A promise containing the array of quads.
*/
export const parseQuads = async(readable: Readable): Promise<Quad[]> =>
arrayifyStream(pipeSafely(readable, new StreamParser({ format: TEXT_TURTLE })));

34
src/util/ResourceUtil.ts Normal file
View File

@@ -0,0 +1,34 @@
import { DataFactory } from 'n3';
import type { NamedNode, Quad } from 'rdf-js';
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
import { pushQuad } from './QuadUtil';
import { LDP, RDF } from './UriConstants';
import { toNamedNode } from './UriUtil';
/**
* Helper function to generate type quads for a Container or Resource.
* @param subject - Subject for the new quads.
* @param isContainer - If the identifier corresponds to a container.
*
* @returns The generated quads.
*/
export const generateResourceQuads = (subject: NamedNode, isContainer: boolean): Quad[] => {
const quads: Quad[] = [];
if (isContainer) {
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Container));
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.BasicContainer));
}
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Resource));
return quads;
};
/**
* Helper function to generate the quads describing that the resource URIs are children of the container URI.
* @param containerURI - The URI of the container.
* @param childURIs - The URI of the child resources.
*
* @returns The generated quads.
*/
export const generateContainmentQuads = (containerURI: NamedNode, childURIs: string[]): Quad[] =>
new RepresentationMetadata(containerURI, { [LDP.contains]: childURIs.map(DataFactory.namedNode) }).quads();

38
src/util/StreamUtil.ts Normal file
View File

@@ -0,0 +1,38 @@
import type { Readable, Writable } from 'stream';
import arrayifyStream from 'arrayify-stream';
import { getLoggerFor } from '../logging/LogUtil';
const logger = getLoggerFor('StreamUtil');
/**
* Joins all strings of a stream.
* @param stream - Stream of strings.
*
* @returns The joined string.
*/
export const readableToString = async(stream: Readable): Promise<string> => (await arrayifyStream(stream)).join('');
/**
* Pipes one stream into another and emits errors of the first stream with the second.
* In case of an error in the first stream the second one will be destroyed with the given error.
* @param readable - Initial readable stream.
* @param destination - The destination for writing data.
* @param mapError - Optional function that takes the error and converts it to a new error.
*
* @returns The destination stream.
*/
export const pipeSafely = <T extends Writable>(readable: NodeJS.ReadableStream, destination: T,
mapError?: (error: Error) => Error): T => {
// Not using `stream.pipeline` since the result there only emits an error event if the last stream has the error
readable.pipe(destination);
readable.on('error', (error): void => {
logger.warn(`Piped stream errored with ${error.message}`);
// From https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options :
// "One important caveat is that if the Readable stream emits an error during processing, the Writable destination
// is not closed automatically. If an error occurs, it will be necessary to manually close each stream
// in order to prevent memory leaks."
destination.destroy(mapError ? mapError(error) : error);
});
return destination;
};

View File

@@ -1,131 +0,0 @@
import type { Readable, Writable } from 'stream';
import arrayifyStream from 'arrayify-stream';
import { DataFactory } from 'n3';
import type { Literal, NamedNode, Quad } from 'rdf-js';
import { getLoggerFor } from '../logging/LogUtil';
import type { HttpResponse } from '../server/HttpResponse';
const logger = getLoggerFor('Util');
/**
* Makes sure the input path has exactly 1 slash at the end.
* Multiple slashes will get merged into one.
* If there is no slash it will be added.
*
* @param path - Path to check.
*
* @returns The potentially changed path.
*/
export const ensureTrailingSlash = (path: string): string => path.replace(/\/*$/u, '/');
/**
* Joins all strings of a stream.
* @param stream - Stream of strings.
*
* @returns The joined string.
*/
export const readableToString = async(stream: Readable): Promise<string> => (await arrayifyStream(stream)).join('');
/**
* Makes sure the input path has no slashes at the end.
*
* @param path - Path to check.
*
* @returns The potentially changed path.
*/
export const trimTrailingSlashes = (path: string): string => path.replace(/\/+$/u, '');
/**
* Checks if the given two media types/ranges match each other.
* Takes wildcards into account.
* @param mediaA - Media type to match.
* @param mediaB - Media type to match.
*
* @returns True if the media type patterns can match each other.
*/
export const matchingMediaType = (mediaA: string, mediaB: string): boolean => {
const [ typeA, subTypeA ] = mediaA.split('/');
const [ typeB, subTypeB ] = mediaB.split('/');
if (typeA === '*' || typeB === '*') {
return true;
}
if (typeA !== typeB) {
return false;
}
if (subTypeA === '*' || subTypeB === '*') {
return true;
}
return subTypeA === subTypeB;
};
/**
* Pipes one stream into another and emits errors of the first stream with the second.
* In case of an error in the first stream the second one will be destroyed with the given error.
* @param readable - Initial readable stream.
* @param destination - The destination for writing data.
* @param mapError - Optional function that takes the error and converts it to a new error.
*
* @returns The destination stream.
*/
export const pipeSafe = <T extends Writable>(readable: NodeJS.ReadableStream, destination: T,
mapError?: (error: Error) => Error): T => {
// Not using `stream.pipeline` since the result there only emits an error event if the last stream has the error
readable.pipe(destination);
readable.on('error', (error): void => {
logger.warn(`Piped stream errored with ${error.message}`);
// From https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options :
// "One important caveat is that if the Readable stream emits an error during processing, the Writable destination
// is not closed automatically. If an error occurs, it will be necessary to manually close each stream
// in order to prevent memory leaks."
destination.destroy(mapError ? mapError(error) : error);
});
return destination;
};
/**
* Converts a URI path to the canonical version by splitting on slashes,
* decoding any percent-based encodings,
* and then encoding any special characters.
*/
export const toCanonicalUriPath = (path: string): string => path.split('/').map((part): string =>
encodeURIComponent(decodeURIComponent(part))).join('/');
/**
* Decodes all components of a URI path.
*/
export const decodeUriPathComponents = (path: string): string => path.split('/').map(decodeURIComponent).join('/');
/**
* Encodes all (non-slash) special characters in a URI path.
*/
export const encodeUriPathComponents = (path: string): string => path.split('/').map(encodeURIComponent).join('/');
/**
* Generates a quad with the given subject/predicate/object and pushes it to the given array.
*/
export const pushQuad =
(quads: Quad[], subject: NamedNode, predicate: NamedNode, object: NamedNode | Literal): number =>
quads.push(DataFactory.quad(subject, predicate, object));
/**
* Adds a header value without overriding previous values.
*/
export const addHeader = (response: HttpResponse, name: string, value: string | string[]): void => {
let allValues: string[] = [];
if (response.hasHeader(name)) {
let oldValues = response.getHeader(name)!;
if (typeof oldValues === 'string') {
oldValues = [ oldValues ];
} else if (typeof oldValues === 'number') {
oldValues = [ `${oldValues}` ];
}
allValues = oldValues;
}
if (Array.isArray(value)) {
allValues.push(...value);
} else {
allValues.push(value);
}
response.setHeader(name, allValues.length === 1 ? allValues[0] : allValues);
};