fix: Integrate wrapStreamError to prevent uncaught errors

This commit is contained in:
Joachim Van Herwegen 2020-11-17 13:06:29 +01:00
parent 1a30b51461
commit e4183333fd
24 changed files with 112 additions and 108 deletions

View File

@ -1,4 +1,3 @@
import streamifyArray from 'streamify-array';
import type { AclManager } from '../authorization/AclManager'; import type { AclManager } from '../authorization/AclManager';
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
import type { LoggerFactory } from '../logging/LoggerFactory'; import type { LoggerFactory } from '../logging/LoggerFactory';
@ -6,6 +5,7 @@ import { getLoggerFor, setGlobalLoggerFactory } from '../logging/LogUtil';
import type { ExpressHttpServerFactory } from '../server/ExpressHttpServerFactory'; import type { ExpressHttpServerFactory } from '../server/ExpressHttpServerFactory';
import type { ResourceStore } from '../storage/ResourceStore'; import type { ResourceStore } from '../storage/ResourceStore';
import { TEXT_TURTLE } from '../util/ContentTypes'; import { TEXT_TURTLE } from '../util/ContentTypes';
import { guardedStreamFrom } from '../util/StreamUtil';
import { CONTENT_TYPE } from '../util/UriConstants'; import { CONTENT_TYPE } from '../util/UriConstants';
/** /**
@ -65,7 +65,7 @@ export class Setup {
baseAclId, baseAclId,
{ {
binary: true, binary: true,
data: streamifyArray([ acl ]), data: guardedStreamFrom([ acl ]),
metadata, metadata,
}, },
); );

View File

@ -1,4 +1,5 @@
import type { Readable } from 'stream'; import type { Readable } from 'stream';
import type { Guarded } from '../../../util/GuardedStream';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { ResponseDescription } from './ResponseDescription'; import { ResponseDescription } from './ResponseDescription';
@ -10,7 +11,7 @@ export class OkResponseDescription extends ResponseDescription {
* @param metadata - Metadata concerning the response. * @param metadata - Metadata concerning the response.
* @param data - Potential data. @ignored * @param data - Potential data. @ignored
*/ */
public constructor(metadata: RepresentationMetadata, data?: Readable) { public constructor(metadata: RepresentationMetadata, data?: Guarded<Readable>) {
super(200, metadata, data); super(200, metadata, data);
} }
} }

View File

@ -1,4 +1,5 @@
import type { Readable } from 'stream'; import type { Readable } from 'stream';
import type { Guarded } from '../../../util/GuardedStream';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
/** /**
@ -7,14 +8,14 @@ import type { RepresentationMetadata } from '../../representation/Representation
export class ResponseDescription { export class ResponseDescription {
public readonly statusCode: number; public readonly statusCode: number;
public readonly metadata?: RepresentationMetadata; public readonly metadata?: RepresentationMetadata;
public readonly data?: Readable; public readonly data?: Guarded<Readable>;
/** /**
* @param statusCode - Status code to return. * @param statusCode - Status code to return.
* @param metadata - Metadata corresponding to the response (and data potentially). * @param metadata - Metadata corresponding to the response (and data potentially).
* @param data - Data that needs to be returned. @ignored * @param data - Data that needs to be returned. @ignored
*/ */
public constructor(statusCode: number, metadata?: RepresentationMetadata, data?: Readable) { public constructor(statusCode: number, metadata?: RepresentationMetadata, data?: Guarded<Readable>) {
this.statusCode = statusCode; this.statusCode = statusCode;
this.metadata = metadata; this.metadata = metadata;
this.data = data; this.data = data;

View File

@ -1,4 +1,5 @@
import type { Readable } from 'stream'; import type { Readable } from 'stream';
import type { Guarded } from '../../util/GuardedStream';
import type { RepresentationMetadata } from './RepresentationMetadata'; import type { RepresentationMetadata } from './RepresentationMetadata';
/** /**
@ -12,7 +13,7 @@ export interface Representation {
/** /**
* The raw data stream for this representation. * The raw data stream for this representation.
*/ */
data: Readable; data: Guarded<Readable>;
/** /**
* Whether the data stream consists of binary/string chunks * Whether the data stream consists of binary/string chunks
* (as opposed to complex objects). * (as opposed to complex objects).

View File

@ -3,6 +3,7 @@ import cors from 'cors';
import type { Express } from 'express'; import type { Express } from 'express';
import express from 'express'; import express from 'express';
import { getLoggerFor } from '../logging/LogUtil'; import { getLoggerFor } from '../logging/LogUtil';
import { guardStream } from '../util/GuardedStream';
import type { HttpHandler } from './HttpHandler'; import type { HttpHandler } from './HttpHandler';
import type { HttpServerFactory } from './HttpServerFactory'; import type { HttpServerFactory } from './HttpServerFactory';
@ -40,7 +41,7 @@ export class ExpressHttpServerFactory implements HttpServerFactory {
app.use(async(request, response, done): Promise<void> => { app.use(async(request, response, done): Promise<void> => {
try { try {
this.logger.info(`Received request for ${request.url}`); this.logger.info(`Received request for ${request.url}`);
await this.handler.handleSafe({ request, response }); await this.handler.handleSafe({ request: guardStream(request), response });
} catch (error: unknown) { } catch (error: unknown) {
const errMsg = error instanceof Error ? `${error.name}: ${error.message}\n${error.stack}` : 'Unknown error.'; const errMsg = error instanceof Error ? `${error.name}: ${error.message}\n${error.stack}` : 'Unknown error.';
this.logger.error(errMsg); this.logger.error(errMsg);

View File

@ -1,6 +1,7 @@
import type { IncomingMessage } from 'http'; import type { IncomingMessage } from 'http';
import type { Guarded } from '../util/GuardedStream';
/** /**
* An incoming HTTP request; * An incoming HTTP request;
*/ */
export type HttpRequest = IncomingMessage; export type HttpRequest = Guarded<IncomingMessage>;

View File

@ -1,7 +1,6 @@
import type { Readable } from 'stream'; import type { Readable } from 'stream';
import { DataFactory } from 'n3'; import { DataFactory } from 'n3';
import type { Quad } from 'rdf-js'; import type { Quad } from 'rdf-js';
import streamifyArray from 'streamify-array';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
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';
@ -12,6 +11,7 @@ import { MethodNotAllowedHttpError } from '../util/errors/MethodNotAllowedHttpEr
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { NotImplementedError } from '../util/errors/NotImplementedError'; import { NotImplementedError } from '../util/errors/NotImplementedError';
import { UnsupportedHttpError } from '../util/errors/UnsupportedHttpError'; import { UnsupportedHttpError } from '../util/errors/UnsupportedHttpError';
import type { Guarded } from '../util/GuardedStream';
import { import {
ensureTrailingSlash, ensureTrailingSlash,
getParentContainer, getParentContainer,
@ -21,6 +21,7 @@ import {
} from '../util/PathUtil'; } from '../util/PathUtil';
import { parseQuads } from '../util/QuadUtil'; import { parseQuads } from '../util/QuadUtil';
import { generateResourceQuads } from '../util/ResourceUtil'; import { generateResourceQuads } from '../util/ResourceUtil';
import { guardedStreamFrom } from '../util/StreamUtil';
import { CONTENT_TYPE, HTTP, LDP, RDF } from '../util/UriConstants'; import { CONTENT_TYPE, HTTP, LDP, RDF } from '../util/UriConstants';
import type { DataAccessor } from './accessors/DataAccessor'; import type { DataAccessor } from './accessors/DataAccessor';
import type { ResourceStore } from './ResourceStore'; import type { ResourceStore } from './ResourceStore';
@ -70,9 +71,9 @@ export class DataAccessorBasedStore implements ResourceStore {
metadata.contentType = INTERNAL_QUADS; metadata.contentType = INTERNAL_QUADS;
result = { result = {
binary: false, binary: false,
get data(): Readable { get data(): Guarded<Readable> {
// This allows other modules to still add metadata before the output data is written // This allows other modules to still add metadata before the output data is written
return streamifyArray(result.metadata.quads()); return guardedStreamFrom(result.metadata.quads());
}, },
metadata, metadata,
}; };
@ -365,7 +366,7 @@ export class DataAccessorBasedStore implements ResourceStore {
protected getEmptyContainerRepresentation(container: ResourceIdentifier): Representation { protected getEmptyContainerRepresentation(container: ResourceIdentifier): Representation {
return { return {
binary: true, binary: true,
data: streamifyArray([]), data: guardedStreamFrom([]),
metadata: new RepresentationMetadata(container.path), metadata: new RepresentationMetadata(container.path),
}; };
} }

View File

@ -4,6 +4,7 @@ import type { Representation } from '../ldp/representation/Representation';
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences'; import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../logging/LogUtil'; import { getLoggerFor } from '../logging/LogUtil';
import type { Guarded } from '../util/GuardedStream';
import type { AtomicResourceStore } from './AtomicResourceStore'; import type { AtomicResourceStore } from './AtomicResourceStore';
import type { Conditions } from './Conditions'; import type { Conditions } from './Conditions';
import type { ExpiringLock } from './ExpiringLock'; import type { ExpiringLock } from './ExpiringLock';
@ -118,7 +119,7 @@ export class LockingResourceStore implements AtomicResourceStore {
* @param source - The readable to wrap * @param source - The readable to wrap
* @param lock - The lock for the corresponding identifier. * @param lock - The lock for the corresponding identifier.
*/ */
protected createExpiringReadable(source: Readable, lock: ExpiringLock): Readable { protected createExpiringReadable(source: Guarded<Readable>, lock: ExpiringLock): Readable {
// Destroy the source when a timeout occurs. // Destroy the source when a timeout occurs.
lock.on('expired', (): void => { lock.on('expired', (): void => {
source.destroy(new Error(`Stream reading timout exceeded`)); source.destroy(new Error(`Stream reading timout exceeded`));

View File

@ -2,6 +2,7 @@ import type { Readable } from 'stream';
import type { Representation } from '../../ldp/representation/Representation'; import type { Representation } from '../../ldp/representation/Representation';
import type { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import type { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import type { Guarded } from '../../util/GuardedStream';
/** /**
* A DataAccessor is the building block closest to the actual data storage. * A DataAccessor is the building block closest to the actual data storage.
@ -27,7 +28,7 @@ export interface DataAccessor {
* It can be assumed that the incoming identifier will always correspond to a document. * It can be assumed that the incoming identifier will always correspond to a document.
* @param identifier - Identifier for which the data is requested. * @param identifier - Identifier for which the data is requested.
*/ */
getData: (identifier: ResourceIdentifier) => Promise<Readable>; getData: (identifier: ResourceIdentifier) => Promise<Guarded<Readable>>;
/** /**
* Returns the metadata corresponding to the identifier. * Returns the metadata corresponding to the identifier.
@ -42,7 +43,8 @@ export interface DataAccessor {
* @param data - Data to store. * @param data - Data to store.
* @param metadata - Metadata to store. * @param metadata - Metadata to store.
*/ */
writeDocument: (identifier: ResourceIdentifier, data: Readable, metadata: RepresentationMetadata) => Promise<void>; writeDocument: (identifier: ResourceIdentifier, data: Guarded<Readable>, metadata: RepresentationMetadata) =>
Promise<void>;
/** /**
* Writes metadata for a container. * Writes metadata for a container.

View File

@ -12,6 +12,8 @@ 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';
import { guardStream } from '../../util/GuardedStream';
import type { Guarded } from '../../util/GuardedStream';
import { isContainerIdentifier } from '../../util/PathUtil'; import { isContainerIdentifier } from '../../util/PathUtil';
import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil'; import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil';
import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil'; import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
@ -45,12 +47,12 @@ export class FileDataAccessor implements DataAccessor {
* Will return data stream directly to the file corresponding to the resource. * Will return data stream directly to the file corresponding to the resource.
* Will throw NotFoundHttpError if the input is a container. * Will throw NotFoundHttpError if the input is a container.
*/ */
public async getData(identifier: ResourceIdentifier): Promise<Readable> { public async getData(identifier: ResourceIdentifier): Promise<Guarded<Readable>> {
const link = await this.resourceMapper.mapUrlToFilePath(identifier); const link = await this.resourceMapper.mapUrlToFilePath(identifier);
const stats = await this.getStats(link.filePath); const stats = await this.getStats(link.filePath);
if (stats.isFile()) { if (stats.isFile()) {
return createReadStream(link.filePath); return guardStream(createReadStream(link.filePath));
} }
throw new NotFoundHttpError(); throw new NotFoundHttpError();
@ -76,7 +78,7 @@ export class FileDataAccessor implements DataAccessor {
* Writes the given data as a file (and potential metadata as additional file). * Writes the given data as a file (and potential metadata as additional file).
* The metadata file will be written first and will be deleted if something goes wrong writing the actual data. * The metadata file will be written first and will be deleted if something goes wrong writing the actual data.
*/ */
public async writeDocument(identifier: ResourceIdentifier, data: Readable, metadata: RepresentationMetadata): public async writeDocument(identifier: ResourceIdentifier, data: Guarded<Readable>, metadata: RepresentationMetadata):
Promise<void> { Promise<void> {
if (this.isMetadataPath(identifier.path)) { if (this.isMetadataPath(identifier.path)) {
throw new ConflictHttpError('Not allowed to create files with the metadata extension.'); throw new ConflictHttpError('Not allowed to create files with the metadata extension.');
@ -264,7 +266,7 @@ export class FileDataAccessor implements DataAccessor {
// Check if the metadata file exists first // Check if the metadata file exists first
await fsPromises.lstat(metadataPath); await fsPromises.lstat(metadataPath);
const readMetadataStream = createReadStream(metadataPath); const readMetadataStream = guardStream(createReadStream(metadataPath));
return await parseQuads(readMetadataStream); return await parseQuads(readMetadataStream);
} catch (error: unknown) { } catch (error: unknown) {
// Metadata file doesn't exist so lets keep `rawMetaData` an empty array. // Metadata file doesn't exist so lets keep `rawMetaData` an empty array.

View File

@ -1,12 +1,14 @@
import { Readable } from 'stream'; import type { Readable } from 'stream';
import arrayifyStream from 'arrayify-stream'; import arrayifyStream from 'arrayify-stream';
import { DataFactory } from 'n3'; import { DataFactory } from 'n3';
import type { NamedNode } from 'rdf-js'; import type { NamedNode } from 'rdf-js';
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 { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import type { Guarded } from '../../util/GuardedStream';
import { ensureTrailingSlash, isContainerIdentifier } from '../../util/PathUtil'; import { ensureTrailingSlash, isContainerIdentifier } from '../../util/PathUtil';
import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil'; import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
import { guardedStreamFrom } from '../../util/StreamUtil';
import type { DataAccessor } from './DataAccessor'; import type { DataAccessor } from './DataAccessor';
interface DataEntry { interface DataEntry {
@ -19,27 +21,6 @@ interface ContainerEntry {
} }
type CacheEntry = DataEntry | ContainerEntry; type CacheEntry = DataEntry | ContainerEntry;
class ArrayReadable extends Readable {
private readonly data: any[];
private idx: number;
public constructor(data: any[]) {
super({ objectMode: true });
this.data = data;
this.idx = 0;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public _read(): void {
if (this.idx < this.data.length) {
this.push(this.data[this.idx]);
this.idx += 1;
} else {
this.push(null);
}
}
}
export class InMemoryDataAccessor implements DataAccessor { export class InMemoryDataAccessor implements DataAccessor {
private readonly base: string; private readonly base: string;
private readonly store: ContainerEntry; private readonly store: ContainerEntry;
@ -56,12 +37,12 @@ export class InMemoryDataAccessor implements DataAccessor {
// All data is supported since streams never get read, only copied // All data is supported since streams never get read, only copied
} }
public async getData(identifier: ResourceIdentifier): Promise<Readable> { public async getData(identifier: ResourceIdentifier): Promise<Guarded<Readable>> {
const entry = this.getEntry(identifier); const entry = this.getEntry(identifier);
if (!this.isDataEntry(entry)) { if (!this.isDataEntry(entry)) {
throw new NotFoundHttpError(); throw new NotFoundHttpError();
} }
return new ArrayReadable(entry.data); return guardedStreamFrom(entry.data);
} }
public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> { public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> {
@ -72,7 +53,7 @@ export class InMemoryDataAccessor implements DataAccessor {
return this.generateMetadata(identifier, entry); return this.generateMetadata(identifier, entry);
} }
public async writeDocument(identifier: ResourceIdentifier, data: Readable, metadata: RepresentationMetadata): public async writeDocument(identifier: ResourceIdentifier, data: Guarded<Readable>, metadata: RepresentationMetadata):
Promise<void> { Promise<void> {
const { parent, name } = this.getParentEntry(identifier); const { parent, name } = this.getParentEntry(identifier);
parent.entries[name] = { parent.entries[name] = {

View File

@ -23,6 +23,8 @@ import { ConflictHttpError } from '../../util/errors/ConflictHttpError';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError'; import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
import { guardStream } from '../../util/GuardedStream';
import type { Guarded } from '../../util/GuardedStream';
import { ensureTrailingSlash, getParentContainer, isContainerIdentifier } from '../../util/PathUtil'; import { ensureTrailingSlash, getParentContainer, isContainerIdentifier } from '../../util/PathUtil';
import { generateResourceQuads } from '../../util/ResourceUtil'; import { generateResourceQuads } from '../../util/ResourceUtil';
import { CONTENT_TYPE, LDP } from '../../util/UriConstants'; import { CONTENT_TYPE, LDP } from '../../util/UriConstants';
@ -70,9 +72,9 @@ export class SparqlDataAccessor implements DataAccessor {
* Returns all triples stored for the corresponding identifier. * Returns all triples stored for the corresponding identifier.
* Note that this will not throw a 404 if no results were found. * Note that this will not throw a 404 if no results were found.
*/ */
public async getData(identifier: ResourceIdentifier): Promise<Readable> { public async getData(identifier: ResourceIdentifier): Promise<Guarded<Readable>> {
const name = namedNode(identifier.path); const name = namedNode(identifier.path);
return this.sendSparqlConstruct(this.sparqlConstruct(name)); return await this.sendSparqlConstruct(this.sparqlConstruct(name));
} }
/** /**
@ -114,7 +116,7 @@ export class SparqlDataAccessor implements DataAccessor {
/** /**
* Reads the given data stream and stores it together with the metadata. * Reads the given data stream and stores it together with the metadata.
*/ */
public async writeDocument(identifier: ResourceIdentifier, data: Readable, metadata: RepresentationMetadata): public async writeDocument(identifier: ResourceIdentifier, data: Guarded<Readable>, metadata: RepresentationMetadata):
Promise<void> { Promise<void> {
if (this.isMetadataIdentifier(identifier)) { if (this.isMetadataIdentifier(identifier)) {
throw new ConflictHttpError('Not allowed to create NamedNodes with the metadata extension.'); throw new ConflictHttpError('Not allowed to create NamedNodes with the metadata extension.');
@ -292,11 +294,11 @@ export class SparqlDataAccessor implements DataAccessor {
* Sends a SPARQL CONSTRUCT query to the endpoint and returns a stream of quads. * Sends a SPARQL CONSTRUCT query to the endpoint and returns a stream of quads.
* @param sparqlQuery - Query to execute. * @param sparqlQuery - Query to execute.
*/ */
private async sendSparqlConstruct(sparqlQuery: ConstructQuery): Promise<Readable> { private async sendSparqlConstruct(sparqlQuery: ConstructQuery): Promise<Guarded<Readable>> {
const query = this.generator.stringify(sparqlQuery); const query = this.generator.stringify(sparqlQuery);
this.logger.info(`Sending SPARQL CONSTRUCT query to ${this.endpoint}: ${query}`); this.logger.info(`Sending SPARQL CONSTRUCT query to ${this.endpoint}: ${query}`);
try { try {
return await this.fetcher.fetchTriples(this.endpoint, query); return guardStream(await this.fetcher.fetchTriples(this.endpoint, query));
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
this.logger.error(`SPARQL endpoint ${this.endpoint} error: ${error.message}`); this.logger.error(`SPARQL endpoint ${this.endpoint} error: ${error.message}`);

View File

@ -4,6 +4,7 @@ import type { Representation } from '../../ldp/representation/Representation';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import type { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences'; import type { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences';
import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { guardStream } from '../../util/GuardedStream';
import { CONTENT_TYPE } from '../../util/UriConstants'; import { CONTENT_TYPE } from '../../util/UriConstants';
import { validateRequestArgs, matchingTypes } from './ConversionUtil'; import { validateRequestArgs, matchingTypes } from './ConversionUtil';
import type { RepresentationConverterArgs } from './RepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter';
@ -34,7 +35,7 @@ export class QuadToRdfConverter extends TypedRepresentationConverter {
const metadata = new RepresentationMetadata(quads.metadata, { [CONTENT_TYPE]: contentType }); const metadata = new RepresentationMetadata(quads.metadata, { [CONTENT_TYPE]: contentType });
return { return {
binary: true, binary: true,
data: rdfSerializer.serialize(quads.data, { contentType }) as Readable, data: guardStream(rdfSerializer.serialize(quads.data, { contentType }) as Readable),
metadata, metadata,
}; };
} }

View File

@ -11,6 +11,7 @@ import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdenti
import { getLoggerFor } from '../../logging/LogUtil'; import { getLoggerFor } from '../../logging/LogUtil';
import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { guardStream } from '../../util/GuardedStream';
import { CONTENT_TYPE } from '../../util/UriConstants'; import { CONTENT_TYPE } from '../../util/UriConstants';
import type { ResourceLocker } from '../ResourceLocker'; import type { ResourceLocker } from '../ResourceLocker';
import type { ResourceStore } from '../ResourceStore'; import type { ResourceStore } from '../ResourceStore';
@ -77,7 +78,7 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
const metadata = new RepresentationMetadata(input.identifier.path, { [CONTENT_TYPE]: INTERNAL_QUADS }); const metadata = new RepresentationMetadata(input.identifier.path, { [CONTENT_TYPE]: INTERNAL_QUADS });
const representation: Representation = { const representation: Representation = {
binary: false, binary: false,
data: store.match() as Readable, data: guardStream(store.match() as Readable),
metadata, metadata,
}; };
await this.source.setRepresentation(input.identifier, representation); await this.source.setRepresentation(input.identifier, representation);

View File

View File

@ -4,6 +4,7 @@ import { DataFactory, StreamParser, StreamWriter } from 'n3';
import type { Literal, NamedNode, Quad } from 'rdf-js'; import type { Literal, NamedNode, Quad } from 'rdf-js';
import streamifyArray from 'streamify-array'; import streamifyArray from 'streamify-array';
import { TEXT_TURTLE } from './ContentTypes'; import { TEXT_TURTLE } from './ContentTypes';
import type { Guarded } from './GuardedStream';
import { pipeSafely } from './StreamUtil'; import { pipeSafely } from './StreamUtil';
/** /**
@ -19,7 +20,7 @@ export const pushQuad =
* *
* @returns The Readable object. * @returns The Readable object.
*/ */
export const serializeQuads = (quads: Quad[]): Readable => export const serializeQuads = (quads: Quad[]): Guarded<Readable> =>
pipeSafely(streamifyArray(quads), new StreamWriter({ format: TEXT_TURTLE })); pipeSafely(streamifyArray(quads), new StreamWriter({ format: TEXT_TURTLE }));
/** /**
@ -28,5 +29,5 @@ export const serializeQuads = (quads: Quad[]): Readable =>
* *
* @returns A promise containing the array of quads. * @returns A promise containing the array of quads.
*/ */
export const parseQuads = async(readable: Readable): Promise<Quad[]> => export const parseQuads = async(readable: Guarded<Readable>): Promise<Quad[]> =>
arrayifyStream(pipeSafely(readable, new StreamParser({ format: TEXT_TURTLE }))); arrayifyStream(pipeSafely(readable, new StreamParser({ format: TEXT_TURTLE })));

View File

@ -5,6 +5,7 @@ import { RepresentationMetadata } from '../../src/ldp/representation/Representat
import { FileDataAccessor } from '../../src/storage/accessors/FileDataAccessor'; import { FileDataAccessor } from '../../src/storage/accessors/FileDataAccessor';
import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor'; import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor';
import { ExtensionBasedMapper } from '../../src/storage/mapping/ExtensionBasedMapper'; import { ExtensionBasedMapper } from '../../src/storage/mapping/ExtensionBasedMapper';
import { guardStream } from '../../src/util/GuardedStream';
import { ensureTrailingSlash } from '../../src/util/PathUtil'; import { ensureTrailingSlash } from '../../src/util/PathUtil';
import { CONTENT_TYPE, LDP } from '../../src/util/UriConstants'; import { CONTENT_TYPE, LDP } from '../../src/util/UriConstants';
import { AuthenticatedDataAccessorBasedConfig } from '../configs/AuthenticatedDataAccessorBasedConfig'; import { AuthenticatedDataAccessorBasedConfig } from '../configs/AuthenticatedDataAccessorBasedConfig';
@ -42,7 +43,7 @@ describe.each([ dataAccessorStore, inMemoryDataAccessorStore ])('A server using
// Use store instead of file access so tests also work for non-file backends // Use store instead of file access so tests also work for non-file backends
await config.store.setRepresentation({ path: `${BASE}/permanent.txt` }, { await config.store.setRepresentation({ path: `${BASE}/permanent.txt` }, {
binary: true, binary: true,
data: createReadStream(join(__dirname, '../assets/permanent.txt')), data: guardStream(createReadStream(join(__dirname, '../assets/permanent.txt'))),
metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/plain' }), metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/plain' }),
}); });
}); });

View File

@ -1,10 +1,9 @@
import streamifyArray from 'streamify-array';
import type { Representation } from '../../src/ldp/representation/Representation'; import type { Representation } from '../../src/ldp/representation/Representation';
import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
import { ChainedConverter } from '../../src/storage/conversion/ChainedConverter'; import { ChainedConverter } from '../../src/storage/conversion/ChainedConverter';
import { QuadToRdfConverter } from '../../src/storage/conversion/QuadToRdfConverter'; import { QuadToRdfConverter } from '../../src/storage/conversion/QuadToRdfConverter';
import { RdfToQuadConverter } from '../../src/storage/conversion/RdfToQuadConverter'; import { RdfToQuadConverter } from '../../src/storage/conversion/RdfToQuadConverter';
import { readableToString } from '../../src/util/StreamUtil'; import { guardedStreamFrom, readableToString } from '../../src/util/StreamUtil';
import { CONTENT_TYPE } from '../../src/util/UriConstants'; import { CONTENT_TYPE } from '../../src/util/UriConstants';
describe('A ChainedConverter', (): void => { describe('A ChainedConverter', (): void => {
@ -18,7 +17,9 @@ describe('A ChainedConverter', (): void => {
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'application/ld+json' }); const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'application/ld+json' });
const representation: Representation = { const representation: Representation = {
binary: true, binary: true,
data: streamifyArray([ '{"@id": "http://test.com/s", "http://test.com/p": { "@id": "http://test.com/o" }}' ]), data: guardedStreamFrom(
[ '{"@id": "http://test.com/s", "http://test.com/p": { "@id": "http://test.com/o" }}' ],
),
metadata, metadata,
}; };
@ -36,7 +37,7 @@ describe('A ChainedConverter', (): void => {
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' }); const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
const representation: Representation = { const representation: Representation = {
binary: true, binary: true,
data: streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]), data: guardedStreamFrom([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]),
metadata, metadata,
}; };

View File

@ -2,13 +2,13 @@ import { EventEmitter } from 'events';
import { PassThrough } from 'stream'; import { PassThrough } from 'stream';
import type { MockResponse } from 'node-mocks-http'; import type { MockResponse } from 'node-mocks-http';
import { createResponse } from 'node-mocks-http'; import { createResponse } from 'node-mocks-http';
import streamifyArray from 'streamify-array';
import { BasicResponseWriter } from '../../../../src/ldp/http/BasicResponseWriter'; import { BasicResponseWriter } from '../../../../src/ldp/http/BasicResponseWriter';
import type { MetadataWriter } from '../../../../src/ldp/http/metadata/MetadataWriter'; import type { MetadataWriter } from '../../../../src/ldp/http/metadata/MetadataWriter';
import type { ResponseDescription } from '../../../../src/ldp/http/response/ResponseDescription'; import type { ResponseDescription } from '../../../../src/ldp/http/response/ResponseDescription';
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes'; import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
import { CONTENT_TYPE } from '../../../../src/util/UriConstants'; import { CONTENT_TYPE } from '../../../../src/util/UriConstants';
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler'; import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
@ -42,7 +42,7 @@ describe('A BasicResponseWriter', (): void => {
}); });
it('responds with a body if the description has a body.', async(): Promise<void> => { it('responds with a body if the description has a body.', async(): Promise<void> => {
const data = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]); const data = guardedStreamFrom([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]);
result = { statusCode: 201, data }; result = { statusCode: 201, data };
const end = new Promise((resolve): void => { const end = new Promise((resolve): void => {
@ -69,7 +69,7 @@ describe('A BasicResponseWriter', (): void => {
}); });
it('can handle the data stream erroring.', async(): Promise<void> => { it('can handle the data stream erroring.', async(): Promise<void> => {
const data = new PassThrough(); const data = guardedStreamFrom([]);
data.read = (): any => { data.read = (): any => {
data.emit('error', new Error('bad data!')); data.emit('error', new Error('bad data!'));
return null; return null;

View File

@ -1,7 +1,6 @@
import type { Readable } from 'stream'; import type { Readable } from 'stream';
import arrayifyStream from 'arrayify-stream'; import arrayifyStream from 'arrayify-stream';
import { DataFactory } from 'n3'; import { DataFactory } from 'n3';
import streamifyArray from 'streamify-array';
import type { Representation } from '../../../src/ldp/representation/Representation'; import type { Representation } from '../../../src/ldp/representation/Representation';
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier'; import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
@ -13,7 +12,9 @@ import { MethodNotAllowedHttpError } from '../../../src/util/errors/MethodNotAll
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
import { NotImplementedError } from '../../../src/util/errors/NotImplementedError'; import { NotImplementedError } from '../../../src/util/errors/NotImplementedError';
import { UnsupportedHttpError } from '../../../src/util/errors/UnsupportedHttpError'; import { UnsupportedHttpError } from '../../../src/util/errors/UnsupportedHttpError';
import type { Guarded } from '../../../src/util/GuardedStream';
import * as quadUtil from '../../../src/util/QuadUtil'; import * as quadUtil from '../../../src/util/QuadUtil';
import { guardedStreamFrom } from '../../../src/util/StreamUtil';
import { CONTENT_TYPE, HTTP, LDP, RDF } from '../../../src/util/UriConstants'; import { CONTENT_TYPE, HTTP, LDP, RDF } from '../../../src/util/UriConstants';
import { toNamedNode } from '../../../src/util/UriUtil'; import { toNamedNode } from '../../../src/util/UriUtil';
@ -39,7 +40,7 @@ class SimpleDataAccessor implements DataAccessor {
return undefined; return undefined;
} }
public async getData(identifier: ResourceIdentifier): Promise<Readable> { public async getData(identifier: ResourceIdentifier): Promise<Guarded<Readable>> {
this.checkExists(identifier); this.checkExists(identifier);
return this.data[identifier.path].data; return this.data[identifier.path].data;
} }
@ -83,11 +84,11 @@ describe('A DataAccessorBasedStore', (): void => {
representation = { representation = {
binary: true, binary: true,
data: streamifyArray([ resourceData ]), data: guardedStreamFrom([ resourceData ]),
metadata: new RepresentationMetadata( metadata: new RepresentationMetadata(
{ [CONTENT_TYPE]: 'text/plain', [RDF.type]: DataFactory.namedNode(LDP.Resource) }, { [CONTENT_TYPE]: 'text/plain', [RDF.type]: DataFactory.namedNode(LDP.Resource) },
), ),
} as Representation; };
}); });
describe('getting a Representation', (): void => { describe('getting a Representation', (): void => {
@ -176,7 +177,7 @@ describe('A DataAccessorBasedStore', (): void => {
const resourceID = { path: root }; const resourceID = { path: root };
representation.metadata.add(RDF.type, toNamedNode(LDP.Container)); representation.metadata.add(RDF.type, toNamedNode(LDP.Container));
representation.metadata.contentType = 'text/turtle'; representation.metadata.contentType = 'text/turtle';
representation.data = streamifyArray([ `<${`${root}resource/`}> a <coolContainer>.` ]); representation.data = guardedStreamFrom([ `<${`${root}resource/`}> a <coolContainer>.` ]);
const result = await store.addResource(resourceID, representation); const result = await store.addResource(resourceID, representation);
expect(result).toEqual({ expect(result).toEqual({
path: expect.stringMatching(new RegExp(`^${root}[^/]+/$`, 'u')), path: expect.stringMatching(new RegExp(`^${root}[^/]+/$`, 'u')),
@ -287,7 +288,7 @@ describe('A DataAccessorBasedStore', (): void => {
// Generate based on URI // Generate based on URI
representation.metadata.removeAll(RDF.type); representation.metadata.removeAll(RDF.type);
representation.metadata.contentType = 'text/turtle'; representation.metadata.contentType = 'text/turtle';
representation.data = streamifyArray([ `<${`${root}resource/`}> a <coolContainer>.` ]); representation.data = guardedStreamFrom([ `<${`${root}resource/`}> a <coolContainer>.` ]);
await expect(store.setRepresentation(resourceID, representation)).resolves.toBeUndefined(); await expect(store.setRepresentation(resourceID, representation)).resolves.toBeUndefined();
expect(accessor.data[resourceID.path]).toBeTruthy(); expect(accessor.data[resourceID.path]).toBeTruthy();
expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined(); expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined();
@ -298,7 +299,9 @@ describe('A DataAccessorBasedStore', (): void => {
representation.metadata.add(RDF.type, toNamedNode(LDP.Container)); representation.metadata.add(RDF.type, toNamedNode(LDP.Container));
representation.metadata.contentType = 'text/turtle'; representation.metadata.contentType = 'text/turtle';
representation.metadata.identifier = DataFactory.namedNode(`${root}resource/`); representation.metadata.identifier = DataFactory.namedNode(`${root}resource/`);
representation.data = streamifyArray([ `<${`${root}resource/`}> <http://www.w3.org/ns/ldp#contains> <uri>.` ]); representation.data = guardedStreamFrom(
[ `<${`${root}resource/`}> <http://www.w3.org/ns/ldp#contains> <uri>.` ],
);
await expect(store.setRepresentation(resourceID, representation)) await expect(store.setRepresentation(resourceID, representation))
.rejects.toThrow(new ConflictHttpError('Container bodies are not allowed to have containment triples.')); .rejects.toThrow(new ConflictHttpError('Container bodies are not allowed to have containment triples.'));
}); });

View File

@ -1,5 +1,5 @@
import type { Readable } from 'stream';
import { DataFactory } from 'n3'; import { DataFactory } from 'n3';
import streamifyArray from 'streamify-array';
import type { Representation } from '../../../../src/ldp/representation/Representation'; import type { Representation } from '../../../../src/ldp/representation/Representation';
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
import { FileDataAccessor } from '../../../../src/storage/accessors/FileDataAccessor'; import { FileDataAccessor } from '../../../../src/storage/accessors/FileDataAccessor';
@ -9,7 +9,8 @@ import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
import type { SystemError } from '../../../../src/util/errors/SystemError'; import type { SystemError } from '../../../../src/util/errors/SystemError';
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError'; import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
import { readableToString } from '../../../../src/util/StreamUtil'; import type { Guarded } from '../../../../src/util/GuardedStream';
import { guardedStreamFrom, readableToString } from '../../../../src/util/StreamUtil';
import { CONTENT_TYPE, DCTERMS, LDP, POSIX, RDF, XSD } from '../../../../src/util/UriConstants'; import { CONTENT_TYPE, DCTERMS, LDP, POSIX, RDF, XSD } from '../../../../src/util/UriConstants';
import { toNamedNode, toTypedLiteral } from '../../../../src/util/UriUtil'; import { toNamedNode, toTypedLiteral } from '../../../../src/util/UriUtil';
import { mockFs } from '../../../util/Util'; import { mockFs } from '../../../util/Util';
@ -24,12 +25,15 @@ describe('A FileDataAccessor', (): void => {
let accessor: FileDataAccessor; let accessor: FileDataAccessor;
let cache: { data: any }; let cache: { data: any };
let metadata: RepresentationMetadata; let metadata: RepresentationMetadata;
let data: Guarded<Readable>;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
cache = mockFs(rootFilePath, now); cache = mockFs(rootFilePath, now);
accessor = new FileDataAccessor(new ExtensionBasedMapper(base, rootFilePath)); accessor = new FileDataAccessor(new ExtensionBasedMapper(base, rootFilePath));
metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_OCTET_STREAM }); metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_OCTET_STREAM });
data = guardedStreamFrom([ 'data' ]);
}); });
it('can only handle binary data.', async(): Promise<void> => { it('can only handle binary data.', async(): Promise<void> => {
@ -140,23 +144,21 @@ describe('A FileDataAccessor', (): void => {
describe('writing a document', (): void => { describe('writing a document', (): void => {
it('throws a 404 if the identifier does not start with the base.', async(): Promise<void> => { it('throws a 404 if the identifier does not start with the base.', async(): Promise<void> => {
await expect(accessor.writeDocument({ path: 'badpath' }, streamifyArray([]), metadata)) await expect(accessor.writeDocument({ path: 'badpath' }, data, metadata))
.rejects.toThrow(NotFoundHttpError); .rejects.toThrow(NotFoundHttpError);
}); });
it('throws an error when writing to a metadata path.', async(): Promise<void> => { it('throws an error when writing to a metadata path.', async(): Promise<void> => {
await expect(accessor.writeDocument({ path: `${base}resource.meta` }, streamifyArray([]), metadata)) await expect(accessor.writeDocument({ path: `${base}resource.meta` }, data, metadata))
.rejects.toThrow(new ConflictHttpError('Not allowed to create files with the metadata extension.')); .rejects.toThrow(new ConflictHttpError('Not allowed to create files with the metadata extension.'));
}); });
it('writes the data to the corresponding file.', async(): Promise<void> => { it('writes the data to the corresponding file.', async(): Promise<void> => {
const data = streamifyArray([ 'data' ]);
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined(); await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
expect(cache.data.resource).toBe('data'); expect(cache.data.resource).toBe('data');
}); });
it('writes metadata to the corresponding metadata file.', async(): Promise<void> => { it('writes metadata to the corresponding metadata file.', async(): Promise<void> => {
const data = streamifyArray([ 'data' ]);
metadata = new RepresentationMetadata(`${base}res.ttl`, { [CONTENT_TYPE]: 'text/turtle', likes: 'apples' }); metadata = new RepresentationMetadata(`${base}res.ttl`, { [CONTENT_TYPE]: 'text/turtle', likes: 'apples' });
await expect(accessor.writeDocument({ path: `${base}res.ttl` }, data, metadata)).resolves.toBeUndefined(); await expect(accessor.writeDocument({ path: `${base}res.ttl` }, data, metadata)).resolves.toBeUndefined();
expect(cache.data['res.ttl']).toBe('data'); expect(cache.data['res.ttl']).toBe('data');
@ -164,7 +166,6 @@ describe('A FileDataAccessor', (): void => {
}); });
it('does not write metadata that is stored by the file system.', async(): Promise<void> => { it('does not write metadata that is stored by the file system.', async(): Promise<void> => {
const data = streamifyArray([ 'data' ]);
metadata.add(RDF.type, toNamedNode(LDP.Resource)); metadata.add(RDF.type, toNamedNode(LDP.Resource));
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined(); await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
expect(cache.data.resource).toBe('data'); expect(cache.data.resource).toBe('data');
@ -173,7 +174,6 @@ describe('A FileDataAccessor', (): void => {
it('deletes existing metadata if nothing new needs to be stored.', async(): Promise<void> => { it('deletes existing metadata if nothing new needs to be stored.', async(): Promise<void> => {
cache.data = { resource: 'data', 'resource.meta': 'metadata!' }; cache.data = { resource: 'data', 'resource.meta': 'metadata!' };
const data = streamifyArray([ 'data' ]);
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined(); await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined();
expect(cache.data.resource).toBe('data'); expect(cache.data.resource).toBe('data');
expect(cache.data['resource.meta']).toBeUndefined(); expect(cache.data['resource.meta']).toBeUndefined();
@ -184,13 +184,11 @@ describe('A FileDataAccessor', (): void => {
jest.requireMock('fs').promises.unlink = (): any => { jest.requireMock('fs').promises.unlink = (): any => {
throw new Error('error'); throw new Error('error');
}; };
const data = streamifyArray([ 'data' ]);
await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)) await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata))
.rejects.toThrow(new Error('error')); .rejects.toThrow(new Error('error'));
}); });
it('throws if something went wrong writing a file.', async(): Promise<void> => { it('throws if something went wrong writing a file.', async(): Promise<void> => {
const data = streamifyArray([ 'data' ]);
data.read = (): any => { data.read = (): any => {
data.emit('error', new Error('error')); data.emit('error', new Error('error'));
return null; return null;
@ -200,7 +198,6 @@ describe('A FileDataAccessor', (): void => {
}); });
it('deletes the metadata file if something went wrong writing the file.', async(): Promise<void> => { it('deletes the metadata file if something went wrong writing the file.', async(): Promise<void> => {
const data = streamifyArray([ 'data' ]);
data.read = (): any => { data.read = (): any => {
data.emit('error', new Error('error')); data.emit('error', new Error('error'));
return null; return null;
@ -216,10 +213,10 @@ describe('A FileDataAccessor', (): void => {
metadata.identifier = DataFactory.namedNode(`${base}resource`); metadata.identifier = DataFactory.namedNode(`${base}resource`);
metadata.contentType = 'text/plain'; metadata.contentType = 'text/plain';
metadata.add('new', 'metadata'); metadata.add('new', 'metadata');
await expect(accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'text' ]), metadata)) await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata))
.resolves.toBeUndefined(); .resolves.toBeUndefined();
expect(cache.data).toEqual({ expect(cache.data).toEqual({
'resource$.txt': 'text', 'resource$.txt': 'data',
'resource.meta': expect.stringMatching(`<${base}resource> <new> "metadata".`), 'resource.meta': expect.stringMatching(`<${base}resource> <new> "metadata".`),
}); });
}); });
@ -235,11 +232,11 @@ describe('A FileDataAccessor', (): void => {
// `unlink` throwing ENOENT should not be an issue if the content-type does not change // `unlink` throwing ENOENT should not be an issue if the content-type does not change
metadata.contentType = 'text/turtle'; metadata.contentType = 'text/turtle';
await expect(accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'text' ]), metadata)) await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata))
.resolves.toBeUndefined(); .resolves.toBeUndefined();
metadata.contentType = 'text/plain'; metadata.contentType = 'text/plain';
await expect(accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'text' ]), metadata)) await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata))
.rejects.toThrow(new Error('error')); .rejects.toThrow(new Error('error'));
}); });
}); });

View File

@ -1,9 +1,10 @@
import streamifyArray from 'streamify-array'; import type { Readable } from 'stream';
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
import { InMemoryDataAccessor } from '../../../../src/storage/accessors/InMemoryDataAccessor'; import { InMemoryDataAccessor } from '../../../../src/storage/accessors/InMemoryDataAccessor';
import { APPLICATION_OCTET_STREAM } from '../../../../src/util/ContentTypes'; import { APPLICATION_OCTET_STREAM } from '../../../../src/util/ContentTypes';
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
import { readableToString } from '../../../../src/util/StreamUtil'; import type { Guarded } from '../../../../src/util/GuardedStream';
import { guardedStreamFrom, readableToString } from '../../../../src/util/StreamUtil';
import { CONTENT_TYPE, LDP, RDF } from '../../../../src/util/UriConstants'; import { CONTENT_TYPE, LDP, RDF } from '../../../../src/util/UriConstants';
import { toNamedNode } from '../../../../src/util/UriUtil'; import { toNamedNode } from '../../../../src/util/UriUtil';
@ -11,11 +12,14 @@ describe('An InMemoryDataAccessor', (): void => {
const base = 'http://test.com/'; const base = 'http://test.com/';
let accessor: InMemoryDataAccessor; let accessor: InMemoryDataAccessor;
let metadata: RepresentationMetadata; let metadata: RepresentationMetadata;
let data: Guarded<Readable>;
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
accessor = new InMemoryDataAccessor(base); accessor = new InMemoryDataAccessor(base);
metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_OCTET_STREAM }); metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_OCTET_STREAM });
data = guardedStreamFrom([ 'data' ]);
}); });
it('can only handle all data.', async(): Promise<void> => { it('can only handle all data.', async(): Promise<void> => {
@ -33,12 +37,11 @@ describe('An InMemoryDataAccessor', (): void => {
}); });
it('throws an error if part of the path matches a document.', async(): Promise<void> => { it('throws an error if part of the path matches a document.', async(): Promise<void> => {
await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata); await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
await expect(accessor.getData({ path: `${base}resource/resource2` })).rejects.toThrow(new Error('Invalid path.')); await expect(accessor.getData({ path: `${base}resource/resource2` })).rejects.toThrow(new Error('Invalid path.'));
}); });
it('returns the corresponding data every time.', async(): Promise<void> => { it('returns the corresponding data every time.', async(): Promise<void> => {
const data = streamifyArray([ 'data' ]);
await accessor.writeDocument({ path: `${base}resource` }, data, metadata); await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
// Run twice to make sure the data is stored correctly // Run twice to make sure the data is stored correctly
@ -53,12 +56,12 @@ describe('An InMemoryDataAccessor', (): void => {
}); });
it('errors when trying to access the parent of root.', async(): Promise<void> => { it('errors when trying to access the parent of root.', async(): Promise<void> => {
await expect(accessor.writeDocument({ path: `${base}` }, streamifyArray([ 'data' ]), metadata)) await expect(accessor.writeDocument({ path: `${base}` }, data, metadata))
.rejects.toThrow(new Error('Root container has no parent.')); .rejects.toThrow(new Error('Root container has no parent.'));
}); });
it('throws a 404 if the trailing slash does not match its type.', async(): Promise<void> => { it('throws a 404 if the trailing slash does not match its type.', async(): Promise<void> => {
await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata); await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
await expect(accessor.getMetadata({ path: `${base}resource/` })).rejects.toThrow(NotFoundHttpError); await expect(accessor.getMetadata({ path: `${base}resource/` })).rejects.toThrow(NotFoundHttpError);
await accessor.writeContainer({ path: `${base}container/` }, metadata); await accessor.writeContainer({ path: `${base}container/` }, metadata);
await expect(accessor.getMetadata({ path: `${base}container` })).rejects.toThrow(NotFoundHttpError); await expect(accessor.getMetadata({ path: `${base}container` })).rejects.toThrow(NotFoundHttpError);
@ -66,14 +69,14 @@ describe('An InMemoryDataAccessor', (): void => {
it('returns empty metadata if there was none stored.', async(): Promise<void> => { it('returns empty metadata if there was none stored.', async(): Promise<void> => {
metadata = new RepresentationMetadata(); metadata = new RepresentationMetadata();
await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata); await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
metadata = await accessor.getMetadata({ path: `${base}resource` }); metadata = await accessor.getMetadata({ path: `${base}resource` });
expect(metadata.quads()).toHaveLength(0); expect(metadata.quads()).toHaveLength(0);
}); });
it('generates the containment metadata for a container.', async(): Promise<void> => { it('generates the containment metadata for a container.', async(): Promise<void> => {
await accessor.writeContainer({ path: `${base}container/` }, metadata); await accessor.writeContainer({ path: `${base}container/` }, metadata);
await accessor.writeDocument({ path: `${base}container/resource` }, streamifyArray([ 'data' ]), metadata); await accessor.writeDocument({ path: `${base}container/resource` }, data, metadata);
await accessor.writeContainer({ path: `${base}container/container2` }, metadata); await accessor.writeContainer({ path: `${base}container/container2` }, metadata);
metadata = await accessor.getMetadata({ path: `${base}container/` }); metadata = await accessor.getMetadata({ path: `${base}container/` });
expect(metadata.getAll(LDP.contains)).toEqualRdfTermArray( expect(metadata.getAll(LDP.contains)).toEqualRdfTermArray(
@ -83,7 +86,7 @@ describe('An InMemoryDataAccessor', (): void => {
it('adds stored metadata when requesting document metadata.', async(): Promise<void> => { it('adds stored metadata when requesting document metadata.', async(): Promise<void> => {
const inputMetadata = new RepresentationMetadata(`${base}resource`, { [RDF.type]: toNamedNode(LDP.Resource) }); const inputMetadata = new RepresentationMetadata(`${base}resource`, { [RDF.type]: toNamedNode(LDP.Resource) });
await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), inputMetadata); await accessor.writeDocument({ path: `${base}resource` }, data, inputMetadata);
metadata = await accessor.getMetadata({ path: `${base}resource` }); metadata = await accessor.getMetadata({ path: `${base}resource` });
expect(metadata.identifier.value).toBe(`${base}resource`); expect(metadata.identifier.value).toBe(`${base}resource`);
const quads = metadata.quads(); const quads = metadata.quads();
@ -107,7 +110,7 @@ describe('An InMemoryDataAccessor', (): void => {
await accessor.writeContainer({ path: `${base}container/` }, inputMetadata); await accessor.writeContainer({ path: `${base}container/` }, inputMetadata);
const resourceMetadata = new RepresentationMetadata(); const resourceMetadata = new RepresentationMetadata();
await accessor.writeDocument( await accessor.writeDocument(
{ path: `${base}container/resource` }, streamifyArray([ 'data' ]), resourceMetadata, { path: `${base}container/resource` }, data, resourceMetadata,
); );
const newMetadata = new RepresentationMetadata(inputMetadata); const newMetadata = new RepresentationMetadata(inputMetadata);
@ -128,7 +131,7 @@ describe('An InMemoryDataAccessor', (): void => {
}); });
it('errors when writing to an invalid container path..', async(): Promise<void> => { it('errors when writing to an invalid container path..', async(): Promise<void> => {
await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata); await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
await expect(accessor.writeContainer({ path: `${base}resource/container` }, metadata)) await expect(accessor.writeContainer({ path: `${base}resource/container` }, metadata))
.rejects.toThrow(new Error('Invalid path.')); .rejects.toThrow(new Error('Invalid path.'));
@ -141,7 +144,7 @@ describe('An InMemoryDataAccessor', (): void => {
}); });
it('removes the corresponding resource.', async(): Promise<void> => { it('removes the corresponding resource.', async(): Promise<void> => {
await accessor.writeDocument({ path: `${base}resource` }, streamifyArray([ 'data' ]), metadata); await accessor.writeDocument({ path: `${base}resource` }, data, metadata);
await accessor.writeContainer({ path: `${base}container/` }, metadata); await accessor.writeContainer({ path: `${base}container/` }, metadata);
await expect(accessor.deleteResource({ path: `${base}resource` })).resolves.toBeUndefined(); await expect(accessor.deleteResource({ path: `${base}resource` })).resolves.toBeUndefined();
await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined(); await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined();

View File

@ -1,9 +1,8 @@
import type { Readable } from 'stream'; import { Readable } from 'stream';
import arrayifyStream from 'arrayify-stream'; import arrayifyStream from 'arrayify-stream';
import { SparqlEndpointFetcher } from 'fetch-sparql-endpoint'; import { SparqlEndpointFetcher } from 'fetch-sparql-endpoint';
import { DataFactory } from 'n3'; import { DataFactory } from 'n3';
import type { Quad } from 'rdf-js'; import type { Quad } from 'rdf-js';
import streamifyArray from 'streamify-array';
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
import { SparqlDataAccessor } from '../../../../src/storage/accessors/SparqlDataAccessor'; import { SparqlDataAccessor } from '../../../../src/storage/accessors/SparqlDataAccessor';
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes'; import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
@ -11,6 +10,8 @@ import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError'; import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError'; import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
import type { Guarded } from '../../../../src/util/GuardedStream';
import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
import { CONTENT_TYPE, LDP, RDF } from '../../../../src/util/UriConstants'; import { CONTENT_TYPE, LDP, RDF } from '../../../../src/util/UriConstants';
import { toNamedNode } from '../../../../src/util/UriUtil'; import { toNamedNode } from '../../../../src/util/UriUtil';
@ -30,6 +31,7 @@ describe('A SparqlDataAccessor', (): void => {
const base = 'http://test.com/'; const base = 'http://test.com/';
let accessor: SparqlDataAccessor; let accessor: SparqlDataAccessor;
let metadata: RepresentationMetadata; let metadata: RepresentationMetadata;
let data: Guarded<Readable>;
let fetchTriples: jest.Mock<Promise<Readable>>; let fetchTriples: jest.Mock<Promise<Readable>>;
let fetchUpdate: jest.Mock<Promise<void>>; let fetchUpdate: jest.Mock<Promise<void>>;
let triples: Quad[]; let triples: Quad[];
@ -38,6 +40,9 @@ describe('A SparqlDataAccessor', (): void => {
beforeEach(async(): Promise<void> => { beforeEach(async(): Promise<void> => {
metadata = new RepresentationMetadata(); metadata = new RepresentationMetadata();
data = guardedStreamFrom(
[ quad(namedNode('http://name'), namedNode('http://pred'), literal('value')) ],
);
triples = [ quad(namedNode('this'), namedNode('a'), namedNode('triple')) ]; triples = [ quad(namedNode('this'), namedNode('a'), namedNode('triple')) ];
// Makes it so the `SparqlEndpointFetcher` will always return the contents of the `triples` array // Makes it so the `SparqlEndpointFetcher` will always return the contents of the `triples` array
@ -45,7 +50,7 @@ describe('A SparqlDataAccessor', (): void => {
if (fetchError) { if (fetchError) {
throw fetchError; throw fetchError;
} }
return streamifyArray(triples); return Readable.from(triples);
}); });
fetchUpdate = jest.fn(async(): Promise<void> => { fetchUpdate = jest.fn(async(): Promise<void> => {
if (updateError) { if (updateError) {
@ -62,7 +67,6 @@ describe('A SparqlDataAccessor', (): void => {
}); });
it('can only handle quad data.', async(): Promise<void> => { it('can only handle quad data.', async(): Promise<void> => {
const data = streamifyArray([]);
await expect(accessor.canHandle({ binary: true, data, metadata })).rejects.toThrow(UnsupportedMediaTypeHttpError); await expect(accessor.canHandle({ binary: true, data, metadata })).rejects.toThrow(UnsupportedMediaTypeHttpError);
metadata.contentType = 'newInternalType'; metadata.contentType = 'newInternalType';
await expect(accessor.canHandle({ binary: false, data, metadata })).rejects.toThrow(UnsupportedMediaTypeHttpError); await expect(accessor.canHandle({ binary: false, data, metadata })).rejects.toThrow(UnsupportedMediaTypeHttpError);
@ -71,8 +75,8 @@ describe('A SparqlDataAccessor', (): void => {
}); });
it('returns the corresponding quads when data is requested.', async(): Promise<void> => { it('returns the corresponding quads when data is requested.', async(): Promise<void> => {
const data = await accessor.getData({ path: 'http://identifier' }); const result = await accessor.getData({ path: 'http://identifier' });
await expect(arrayifyStream(data)).resolves.toBeRdfIsomorphic([ await expect(arrayifyStream(result)).resolves.toBeRdfIsomorphic([
quad(namedNode('this'), namedNode('a'), namedNode('triple')), quad(namedNode('this'), namedNode('a'), namedNode('triple')),
]); ]);
@ -168,7 +172,6 @@ describe('A SparqlDataAccessor', (): void => {
}); });
it('overwrites the data and metadata when writing a resource and updates parent.', async(): Promise<void> => { it('overwrites the data and metadata when writing a resource and updates parent.', async(): Promise<void> => {
const data = streamifyArray([ quad(namedNode('http://name'), namedNode('http://pred'), literal('value')) ]);
metadata = new RepresentationMetadata('http://test.com/container/resource', metadata = new RepresentationMetadata('http://test.com/container/resource',
{ [RDF.type]: [ toNamedNode(LDP.Resource) ]}); { [RDF.type]: [ toNamedNode(LDP.Resource) ]});
await expect(accessor.writeDocument({ path: 'http://test.com/container/resource' }, data, metadata)) await expect(accessor.writeDocument({ path: 'http://test.com/container/resource' }, data, metadata))
@ -202,13 +205,12 @@ describe('A SparqlDataAccessor', (): void => {
}); });
it('errors when trying to write to a metadata document.', async(): Promise<void> => { it('errors when trying to write to a metadata document.', async(): Promise<void> => {
const data = streamifyArray([ quad(namedNode('http://name'), namedNode('http://pred'), literal('value')) ]);
await expect(accessor.writeDocument({ path: 'meta:http://test.com/container/resource' }, data, metadata)) await expect(accessor.writeDocument({ path: 'meta:http://test.com/container/resource' }, data, metadata))
.rejects.toThrow(new ConflictHttpError('Not allowed to create NamedNodes with the metadata extension.')); .rejects.toThrow(new ConflictHttpError('Not allowed to create NamedNodes with the metadata extension.'));
}); });
it('errors when writing triples in a non-default graph.', async(): Promise<void> => { it('errors when writing triples in a non-default graph.', async(): Promise<void> => {
const data = streamifyArray( data = guardedStreamFrom(
[ quad(namedNode('http://name'), namedNode('http://pred'), literal('value'), namedNode('badGraph!')) ], [ quad(namedNode('http://name'), namedNode('http://pred'), literal('value'), namedNode('badGraph!')) ],
); );
await expect(accessor.writeDocument({ path: 'http://test.com/container/resource' }, data, metadata)) await expect(accessor.writeDocument({ path: 'http://test.com/container/resource' }, data, metadata))

View File

@ -2,12 +2,12 @@ import { EventEmitter } from 'events';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import type { IncomingHttpHeaders } from 'http'; import type { IncomingHttpHeaders } from 'http';
import { join } from 'path'; import { join } from 'path';
import { Readable } from 'stream';
import * as url from 'url'; import * as url from 'url';
import type { MockResponse } from 'node-mocks-http'; import type { MockResponse } from 'node-mocks-http';
import { createResponse } from 'node-mocks-http'; import { createResponse } from 'node-mocks-http';
import streamifyArray from 'streamify-array';
import type { ResourceStore } from '../../index'; import type { ResourceStore } from '../../index';
import { RepresentationMetadata } from '../../index'; import { guardedStreamFrom, RepresentationMetadata } from '../../index';
import type { PermissionSet } from '../../src/ldp/permissions/PermissionSet'; import type { PermissionSet } from '../../src/ldp/permissions/PermissionSet';
import type { HttpHandler } from '../../src/server/HttpHandler'; import type { HttpHandler } from '../../src/server/HttpHandler';
import type { HttpRequest } from '../../src/server/HttpRequest'; import type { HttpRequest } from '../../src/server/HttpRequest';
@ -52,7 +52,7 @@ export class AclTestHelper {
const representation = { const representation = {
binary: true, binary: true,
data: streamifyArray(acl), data: guardedStreamFrom(acl),
metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' }), metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' }),
}; };
@ -86,7 +86,7 @@ export class FileTestHelper {
headers: IncomingHttpHeaders, headers: IncomingHttpHeaders,
data: Buffer, data: Buffer,
): Promise<MockResponse<any>> { ): Promise<MockResponse<any>> {
const request = streamifyArray([ data ]) as HttpRequest; const request = Readable.from([ data ]) as HttpRequest;
request.url = requestUrl.pathname; request.url = requestUrl.pathname;
request.method = method; request.method = method;