Skip to content

Commit

Permalink
Merge e013eb7 into 075295b
Browse files Browse the repository at this point in the history
  • Loading branch information
smessie committed Aug 28, 2020
2 parents 075295b + e013eb7 commit 6e2c0bf
Show file tree
Hide file tree
Showing 14 changed files with 1,357 additions and 103 deletions.
4 changes: 3 additions & 1 deletion bin/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
TurtleToQuadConverter,
UrlContainerManager,
} from '..';
import { InteractionController } from '../src/util/InteractionController';
import { ResourceStoreController } from '../src/util/ResourceStoreController';

const { argv } = yargs
.usage('node ./bin/server.js [args]')
Expand Down Expand Up @@ -61,7 +63,7 @@ const permissionsExtractor = new CompositeAsyncHandler([
]);

// Will have to see how to best handle this
const store = new SimpleResourceStore(runtimeConfig);
const store = new SimpleResourceStore(new ResourceStoreController(runtimeConfig, new InteractionController()));
const converter = new CompositeAsyncHandler([
new TurtleToQuadConverter(),
new QuadToTurtleConverter(),
Expand Down
52 changes: 41 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,18 @@
"@types/n3": "^1.4.0",
"@types/node": "^14.0.1",
"@types/rdf-js": "^3.0.0",
"@types/sparqljs": "^3.0.1",
"@types/uuid": "^8.3.0",
"@types/yargs": "^15.0.5",
"async-lock": "^1.2.4",
"cors": "^2.8.5",
"cross-fetch": "^3.0.5",
"express": "^4.17.1",
"mime-types": "^2.1.27",
"n3": "^1.4.0",
"rdf-terms": "^1.5.1",
"sparqlalgebrajs": "^2.3.1",
"sparqljs": "^3.1.1",
"uuid": "^8.3.0",
"yargs": "^15.4.1"
},
Expand Down
95 changes: 41 additions & 54 deletions src/storage/FileResourceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,32 @@ import { ConflictHttpError } from '../util/errors/ConflictHttpError';
import { MethodNotAllowedHttpError } from '../util/errors/MethodNotAllowedHttpError';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { UnsupportedMediaTypeHttpError } from '../util/errors/UnsupportedMediaTypeHttpError';
import { InteractionController } from '../util/InteractionController';
import { MetadataController } from '../util/MetadataController';
import { ResourceStoreController } from '../util/ResourceStoreController';
import { ensureTrailingSlash, trimTrailingSlashes } from '../util/Util';
import { ResourceStore } from './ResourceStore';

const { extname, join: joinPath, normalize: normalizePath } = posix;
const { extname, join: joinPath } = posix;

/**
* Resource store storing its data in the file system backend.
* All requests will throw an {@link NotFoundHttpError} if unknown identifiers get passed.
*/
export class FileResourceStore implements ResourceStore {
private readonly runtimeConfig: RuntimeConfig;
private readonly interactionController: InteractionController;
private readonly metadataController: MetadataController;
private readonly resourceStoreController: ResourceStoreController;

/**
* @param runtimeConfig - The runtime config.
* @param interactionController - Instance of InteractionController to use.
* @param metadataController - Instance of MetadataController to use.
* @param resourceStoreController - Instance of ResourceStoreController to use.
*/
public constructor(runtimeConfig: RuntimeConfig, interactionController: InteractionController,
metadataController: MetadataController) {
public constructor(runtimeConfig: RuntimeConfig, metadataController: MetadataController,
resourceStoreController: ResourceStoreController) {
this.runtimeConfig = runtimeConfig;
this.interactionController = interactionController;
this.metadataController = metadataController;
}

public get baseRequestURI(): string {
return trimTrailingSlashes(this.runtimeConfig.base);
this.resourceStoreController = resourceStoreController;
}

public get rootFilepath(): string {
Expand All @@ -59,22 +55,21 @@ export class FileResourceStore implements ResourceStore {
* @returns The newly generated identifier.
*/
public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> {
if (representation.dataType !== DATA_TYPE_BINARY) {
throw new UnsupportedMediaTypeHttpError('FileResourceStore only supports binary representations.');
}
// Check if the representation has a valid dataType.
this.ensureValidDataType(representation);

// 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;
// Get the expected behaviour based on the incoming identifier and representation.
const { isContainer, path, newIdentifier } = this.resourceStoreController.getBehaviourAddResource(container,
representation);

// Get all metadata triples if any.
const { raw } = representation.metadata;
let metadata;
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 All @@ -85,10 +80,8 @@ export class FileResourceStore implements ResourceStore {
* @param identifier - Identifier of resource to delete.
*/
public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
let path = this.parseIdentifier(identifier);
if (path === '' || ensureTrailingSlash(path) === '/') {
throw new MethodNotAllowedHttpError('Cannot delete root container.');
}
let path = this.resourceStoreController.parseIdentifier(identifier);
this.resourceStoreController.validateDeletePath(path);

// Get the file status of the path defined by the request URI mapped to the corresponding filepath.
path = joinPath(this.rootFilepath, path);
Expand Down Expand Up @@ -118,7 +111,7 @@ export class FileResourceStore implements ResourceStore {
*/
public async getRepresentation(identifier: ResourceIdentifier): Promise<Representation> {
// Get the file status of the path defined by the request URI mapped to the corresponding filepath.
const path = joinPath(this.rootFilepath, this.parseIdentifier(identifier));
const path = joinPath(this.rootFilepath, this.resourceStoreController.parseIdentifier(identifier));
let stats;
try {
stats = await fsPromises.lstat(path);
Expand Down Expand Up @@ -149,45 +142,26 @@ export class FileResourceStore implements ResourceStore {
* @param representation - New Representation.
*/
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): Promise<void> {
if (representation.dataType !== DATA_TYPE_BINARY) {
throw new UnsupportedMediaTypeHttpError('FileResourceStore only supports binary representations.');
}
// Check if the representation has a valid dataType.
this.ensureValidDataType(representation);

// Break up the request URI in the different parts `path` and `slug` as we know their semantics from addResource
// to call the InteractionController in the same way.
const [ , path, slug ] = /^(.*\/)([^/]+\/?)?$/u.exec(this.parseIdentifier(identifier)) ?? [];
if ((typeof path !== 'string' || normalizePath(path) === '/') && typeof slug !== 'string') {
throw new ConflictHttpError('Container with that identifier already exists (root).');
}
// Get the expected behaviour based on the incoming identifier and representation.
const { isContainer, path, newIdentifier } = this.resourceStoreController.getBehaviourSetRepresentation(identifier,
representation);

// Get all metadata triples if any.
const { raw } = representation.metadata;
const linkTypes = representation.metadata.linkRel?.type;
let metadata: Readable | undefined;
let metadata;
if (raw.length > 0) {
metadata = streamifyArray(raw);
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 ?
await this.setDirectoryRepresentation(path, newIdentifier, metadata) :
await this.setFileRepresentation(path, newIdentifier, representation.data, metadata);
}

/**
* Strips the baseRequestURI from the identifier and checks if the stripped base URI matches the store's one.
* @param identifier - Incoming identifier.
*
* @throws {@link NotFoundHttpError}
* If the identifier does not match the baseRequestURI path of the store.
*/
private parseIdentifier(identifier: ResourceIdentifier): string {
if (!identifier.path.startsWith(this.baseRequestURI)) {
throw new NotFoundHttpError();
}
return identifier.path.slice(this.baseRequestURI.length);
}

/**
* Strips the rootFilepath path from the filepath and adds the baseRequestURI in front of it.
* @param path - The filepath.
Expand All @@ -199,7 +173,20 @@ export class FileResourceStore implements ResourceStore {
if (!path.startsWith(this.rootFilepath)) {
throw new Error(`File ${path} is not part of the file storage at ${this.rootFilepath}.`);
}
return this.baseRequestURI + path.slice(this.rootFilepath.length);
return new URL(path.slice(this.rootFilepath.length), this.runtimeConfig.base).toString();
}

/**
* Check if the representation has a valid dataType.
* @param representation - Incoming Representation.
*
* @throws {@link UnsupportedMediaTypeHttpError}
* If the incoming dataType does not match the store's supported dataType.
*/
private ensureValidDataType(representation: Representation): void {
if (representation.dataType !== DATA_TYPE_BINARY) {
throw new UnsupportedMediaTypeHttpError('The FileResourceStore only supports binary representations.');
}
}

/**
Expand Down
Loading

0 comments on commit 6e2c0bf

Please sign in to comment.