Skip to content

Commit

Permalink
Merge 0d8e772 into aa510bc
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Aug 24, 2020
2 parents aa510bc + 0d8e772 commit 2016660
Show file tree
Hide file tree
Showing 26 changed files with 451 additions and 181 deletions.
13 changes: 7 additions & 6 deletions bin/server.ts
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import { DATA_TYPE_BINARY } from '../src/util/ContentTypes';
import streamifyArray from 'streamify-array';
import { TYPE } from '../src/util/MetadataTypes';
import yargs from 'yargs';
import {
AcceptPreferenceParser,
Expand All @@ -13,6 +14,7 @@ import {
QuadToTurtleConverter,
Representation,
RepresentationConvertingStore,
RepresentationMetadata,
SimpleAclAuthorizer,
SimpleBodyParser,
SimpleCredentialsExtractor,
Expand Down Expand Up @@ -114,16 +116,15 @@ const aclSetup = async(): Promise<void> => {
acl:mode acl:Control;
acl:accessTo <${base}>;
acl:default <${base}>.`;
const id = await aclManager.getAcl({ path: base });
const metadata = new RepresentationMetadata(id.path);
metadata.add(TYPE, 'text/turtle');
await store.setRepresentation(
await aclManager.getAcl({ path: base }),
id,
{
dataType: DATA_TYPE_BINARY,
data: streamifyArray([ acl ]),
metadata: {
raw: [],
profiles: [],
contentType: 'text/turtle',
},
metadata,
},
);
};
Expand Down
8 changes: 3 additions & 5 deletions src/ldp/http/SimpleBodyParser.ts
@@ -1,5 +1,6 @@
import { BinaryRepresentation } from '../representation/BinaryRepresentation';
import { BodyParser } from './BodyParser';
import { CONTENT_TYPE } from '../../util/MetadataTypes';
import { DATA_TYPE_BINARY } from '../../util/ContentTypes';
import { HttpRequest } from '../../server/HttpRequest';
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
Expand All @@ -25,11 +26,8 @@ export class SimpleBodyParser extends BodyParser {

const mediaType = contentType.split(';')[0];

const metadata: RepresentationMetadata = {
raw: [],
profiles: [],
contentType: mediaType,
};
const metadata = new RepresentationMetadata();
metadata.add(CONTENT_TYPE, mediaType);

return {
dataType: DATA_TYPE_BINARY,
Expand Down
3 changes: 2 additions & 1 deletion src/ldp/http/SimpleResponseWriter.ts
@@ -1,3 +1,4 @@
import { CONTENT_TYPE } from '../../util/MetadataTypes';
import { DATA_TYPE_BINARY } from '../../util/ContentTypes';
import { HttpError } from '../../util/errors/HttpError';
import { HttpResponse } from '../../server/HttpResponse';
Expand Down Expand Up @@ -30,7 +31,7 @@ export class SimpleResponseWriter extends ResponseWriter {
} else {
input.response.setHeader('location', input.result.identifier.path);
if (input.result.body) {
const contentType = input.result.body.metadata.contentType ?? 'text/plain';
const contentType = input.result.body.metadata.get(CONTENT_TYPE)?.value ?? 'text/plain';
input.response.setHeader('content-type', contentType);
input.result.body.data.pipe(input.response);
}
Expand Down
11 changes: 6 additions & 5 deletions src/ldp/http/SimpleSparqlUpdateBodyParser.ts
@@ -1,8 +1,10 @@
import { BodyParser } from './BodyParser';
import { CONTENT_TYPE } from '../../util/MetadataTypes';
import { DATA_TYPE_BINARY } from '../../util/ContentTypes';
import { HttpRequest } from '../../server/HttpRequest';
import { PassThrough } from 'stream';
import { readableToString } from '../../util/Util';
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
import { SparqlUpdatePatch } from './SparqlUpdatePatch';
import { translate } from 'sparqlalgebrajs';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
Expand Down Expand Up @@ -34,16 +36,15 @@ export class SimpleSparqlUpdateBodyParser extends BodyParser {
const sparql = await readableToString(toAlgebraStream);
const algebra = translate(sparql, { quads: true });

const metadata = new RepresentationMetadata();
metadata.add(CONTENT_TYPE, 'application/sparql-update');

// Prevent body from being requested again
return {
algebra,
dataType: DATA_TYPE_BINARY,
data: dataCopy,
metadata: {
raw: [],
profiles: [],
contentType: 'application/sparql-update',
},
metadata,
};
} catch (error) {
throw new UnsupportedHttpError(error);
Expand Down
131 changes: 106 additions & 25 deletions src/ldp/representation/RepresentationMetadata.ts
@@ -1,47 +1,128 @@
/**
* Contains metadata relevant to a representation.
*/
import { Quad } from 'rdf-js';
import { Store } from 'n3';
import { BlankNode, Literal, NamedNode, Quad, Term } from 'rdf-js';
import { quad as createQuad, literal, namedNode } from '@rdfjs/data-model';

/**
* Metadata corresponding to a {@link Representation}.
* Stores the metadata triples and provides methods for easy access.
*/
export interface RepresentationMetadata {
export class RepresentationMetadata {
private store: Store;
private id: NamedNode | BlankNode;

/**
* All metadata triples of the resource.
* @param identifier - Identifier of the resource relevant to this metadata.
* A blank node will be generated if none is provided.
* Strings will be converted to named nodes.
* @param quads - Quads to fill the metadata with.
*/
raw: Quad[];
public constructor(identifier?: NamedNode | BlankNode | string, quads?: Quad[]) {
this.store = new Store(quads);
if (identifier) {
if (typeof identifier === 'string') {
this.id = namedNode(identifier);
} else {
this.id = identifier;
}
} else {
this.id = this.store.createBlankNode();
}
}

/**
* Optional metadata profiles.
* @returns All metadata quads.
*/
profiles?: string[];
public quads(): Quad[] {
return this.store.getQuads(null, null, null, null);
}

/**
* Optional size of the representation.
* Identifier of the resource this metadata is relevant to.
* Will update all relevant triples if this value gets changed.
*/
byteSize?: number;
public get identifier(): NamedNode | BlankNode {
return this.id;
}

public set identifier(id: NamedNode | BlankNode) {
const quads = this.quads().map((quad): Quad => {
if (quad.subject.equals(this.id)) {
return createQuad(id, quad.predicate, quad.object, quad.graph);
}
if (quad.object.equals(this.id)) {
return createQuad(quad.subject, quad.predicate, id, quad.graph);
}
return quad;
});
this.store = new Store(quads);
this.id = id;
}

/**
* Optional content type of the representation.
* @param quads - Quads to add to the metadata.
*/
contentType?: string;
public addQuads(quads: Quad[]): void {
this.store.addQuads(quads);
}

/**
* Optional encoding of the representation.
* @param quads - Quads to remove from the metadata.
*/
encoding?: string;
public removeQuads(quads: Quad[]): void {
this.store.removeQuads(quads);
}

/**
* Optional language of the representation.
* Adds a value linked to the identifier. Strings get converted to literals.
* @param predicate - Predicate linking identifier to value.
* @param object - Value to add.
*/
language?: string;
public add(predicate: NamedNode, object: NamedNode | Literal | string): void {
this.store.addQuad(this.id, predicate, typeof object === 'string' ? literal(object) : object);
}

/**
* Optional timestamp of the representation.
* Removes the given value from the metadata. Strings get converted to literals.
* @param predicate - Predicate linking identifier to value.
* @param object - Value to remove.
*/
dateTime?: Date;
public remove(predicate: NamedNode, object: NamedNode | Literal | string): void {
this.store.removeQuad(this.id, predicate, typeof object === 'string' ? literal(object) : object);
}

/**
* Optional link relationships of the representation.
* Removes all values linked through the given predicate.
* @param predicate - Predicate to remove.
*/
linkRel?: { [id: string]: Set<string> };
public removeAll(predicate: NamedNode): void {
this.removeQuads(this.store.getQuads(this.id, predicate, null, null));
}

/**
* @param predicate - Predicate to get the value for.
*
* @throws Error
* If there are multiple matching values.
*
* @returns The corresponding value. Undefined if there is no match
*/
public get(predicate: NamedNode): Term | undefined {
const quads = this.store.getQuads(this.id, predicate, null, null);
if (quads.length === 0) {
return;
}
if (quads.length > 1) {
throw new Error(`Multiple results for ${predicate.value}`);
}
return quads[0].object;
}

/**
* Optional slug of the representation.
* Used to suggest the URI for the resource created.
* Sets the value for the given predicate, removing all other instances.
* @param predicate - Predicate linking to the value.
* @param object - Value to set.
*/
slug?: string;
public set(predicate: NamedNode, object: NamedNode | Literal | string): void {
this.removeAll(predicate);
this.add(predicate, object);
}
}
49 changes: 28 additions & 21 deletions src/storage/FileResourceStore.ts
@@ -1,5 +1,6 @@
import arrayifyStream from 'arrayify-stream';
import { ConflictHttpError } from '../util/errors/ConflictHttpError';
import { DataFactory } from 'n3';
import { contentType as getContentTypeFromExtension } from 'mime-types';
import { InteractionController } from '../util/InteractionController';
import { MetadataController } from '../util/MetadataController';
Expand All @@ -14,6 +15,7 @@ import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { ResourceStore } from './ResourceStore';
import streamifyArray from 'streamify-array';
import { UnsupportedMediaTypeHttpError } from '../util/errors/UnsupportedMediaTypeHttpError';
import { BYTE_SIZE, CONTENT_TYPE, LAST_CHANGED, SLUG, TYPE } from '../util/MetadataTypes';
import { CONTENT_TYPE_QUADS, DATA_TYPE_BINARY, DATA_TYPE_QUAD } from '../util/ContentTypes';
import { createReadStream, createWriteStream, promises as fsPromises, Stats } from 'fs';
import { ensureTrailingSlash, trimTrailingSlashes } from '../util/Util';
Expand Down Expand Up @@ -60,16 +62,20 @@ export class FileResourceStore implements ResourceStore {

// Get the path from the request URI, all metadata triples if any, and the Slug and Link header values.
const path = this.parseIdentifier(container);
const { slug, raw } = representation.metadata;
const linkTypes = representation.metadata.linkRel?.type;
const slug = representation.metadata.get(SLUG)?.value;
const type = representation.metadata.get(TYPE)?.value;

// Create a new container or resource in the parent container with a specific name based on the incoming headers.
const isContainer = this.interactionController.isContainer(slug, type);
const newIdentifier = this.interactionController.generateIdentifier(isContainer, slug);

let metadata;
// eslint-disable-next-line no-param-reassign
representation.metadata.identifier = DataFactory.namedNode(newIdentifier);
const raw = representation.metadata.quads();
if (raw.length > 0) {
metadata = this.metadataController.generateReadableFromQuads(raw);
}

// Create a new container or resource in the parent container with a specific name based on the incoming headers.
const isContainer = this.interactionController.isContainer(slug, linkTypes);
const newIdentifier = this.interactionController.generateIdentifier(isContainer, slug);
return isContainer ?
this.createContainer(path, newIdentifier, path.endsWith('/'), metadata) :
this.createFile(path, newIdentifier, representation.data, path.endsWith('/'), metadata);
Expand Down Expand Up @@ -154,15 +160,17 @@ export class FileResourceStore implements ResourceStore {
if ((typeof path !== 'string' || normalizePath(path) === '/') && typeof slug !== 'string') {
throw new ConflictHttpError('Container with that identifier already exists (root).');
}
const { raw } = representation.metadata;
const linkTypes = representation.metadata.linkRel?.type;
// eslint-disable-next-line no-param-reassign
representation.metadata.identifier = DataFactory.namedNode(identifier.path);
const raw = representation.metadata.quads();
const type = representation.metadata.get(TYPE)?.value;
let metadata: Readable | undefined;
if (raw.length > 0) {
metadata = streamifyArray(raw);
}

// Create a new container or resource in the parent container with a specific name based on the incoming headers.
const isContainer = this.interactionController.isContainer(slug, linkTypes);
const isContainer = this.interactionController.isContainer(slug, type);
const newIdentifier = this.interactionController.generateIdentifier(isContainer, slug);
return isContainer ?
await this.setDirectoryRepresentation(path, newIdentifier, metadata) :
Expand Down Expand Up @@ -241,7 +249,8 @@ export class FileResourceStore implements ResourceStore {
*
* @returns The corresponding Representation.
*/
private async getFileRepresentation(path: string, stats: Stats): Promise<Representation> {
private async getFileRepresentation(path: string, stats: Stats):
Promise<Representation> {
const readStream = createReadStream(path);
const contentType = getContentTypeFromExtension(extname(path));
let rawMetadata: Quad[] = [];
Expand All @@ -251,13 +260,11 @@ export class FileResourceStore implements ResourceStore {
} catch (_) {
// Metadata file doesn't exist so lets keep `rawMetaData` an empty array.
}
const metadata: RepresentationMetadata = {
raw: rawMetadata,
dateTime: stats.mtime,
byteSize: stats.size,
};
const metadata = new RepresentationMetadata(this.mapFilepathToUrl(path), rawMetadata);
metadata.set(LAST_CHANGED, stats.mtime.toISOString());
metadata.set(BYTE_SIZE, DataFactory.literal(stats.size));
if (contentType) {
metadata.contentType = contentType;
metadata.set(CONTENT_TYPE, contentType);
}
return { metadata, data: readStream, dataType: DATA_TYPE_BINARY };
}
Expand Down Expand Up @@ -289,14 +296,14 @@ export class FileResourceStore implements ResourceStore {
// Metadata file doesn't exist so lets keep `rawMetaData` an empty array.
}

const metadata = new RepresentationMetadata(containerURI, rawMetadata);
metadata.set(LAST_CHANGED, stats.mtime.toISOString());
metadata.set(CONTENT_TYPE, CONTENT_TYPE_QUADS);

return {
dataType: DATA_TYPE_QUAD,
data: streamifyArray(quads),
metadata: {
raw: rawMetadata,
dateTime: stats.mtime,
contentType: CONTENT_TYPE_QUADS,
},
metadata,
};
}

Expand Down
6 changes: 4 additions & 2 deletions src/storage/RepresentationConvertingStore.ts
@@ -1,4 +1,5 @@
import { Conditions } from './Conditions';
import { CONTENT_TYPE } from '../util/MetadataTypes';
import { matchingMediaType } from '../util/Util';
import { PassthroughStore } from './PassthroughStore';
import { Representation } from '../ldp/representation/Representation';
Expand Down Expand Up @@ -37,11 +38,12 @@ export class RepresentationConvertingStore extends PassthroughStore {
if (!preferences.type) {
return true;
}
const contentType = representation.metadata.get(CONTENT_TYPE);
return Boolean(
representation.metadata.contentType &&
contentType &&
preferences.type.some((type): boolean =>
type.weight > 0 &&
matchingMediaType(type.value, representation.metadata.contentType!)),
matchingMediaType(type.value, contentType.value)),
);
}
}

0 comments on commit 2016660

Please sign in to comment.