mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add file based ResourceStore (#52)
* feat: Add file resource store * test: Write some tests for FileResourceStore * fix: Reformat code and fix various small things from reviews * fix: Change constants to just be the corresponding URL * fix: Remove extra unnecessary wrap in a Promise * fix: Write some more tests and fix related bugs * fix: Use old way to import fs promises to support older Node versions * refactor: Refactor code and tests * refactor: Refactor and better document code * fix: Change comparison with undefined by typeof check * fix: Invert typeof check
This commit is contained in:
29
src/util/InteractionController.ts
Normal file
29
src/util/InteractionController.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { trimTrailingSlashes } from './Util';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { LINK_TYPE_LDP_BC, LINK_TYPE_LDPC } from './LinkTypes';
|
||||
|
||||
export class InteractionController {
|
||||
/**
|
||||
* Check whether a new container or a resource should be created based on the given parameters.
|
||||
* @param slug - Incoming slug header.
|
||||
* @param link - Incoming link header.
|
||||
*/
|
||||
public isContainer(slug?: string, link?: Set<string>): boolean {
|
||||
if (!slug || !slug.endsWith('/')) {
|
||||
return Boolean(link?.has(LINK_TYPE_LDPC)) || Boolean(link?.has(LINK_TYPE_LDP_BC));
|
||||
}
|
||||
return !link || link.has(LINK_TYPE_LDPC) || link.has(LINK_TYPE_LDP_BC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the identifier path the new resource should have.
|
||||
* @param isContainer - Whether or not the resource is a container.
|
||||
* @param slug - Incoming slug header.
|
||||
*/
|
||||
public generateIdentifier(isContainer: boolean, slug?: string): string {
|
||||
if (!slug) {
|
||||
return `${uuid()}${isContainer ? '/' : ''}`;
|
||||
}
|
||||
return `${trimTrailingSlashes(slug)}${isContainer ? '/' : ''}`;
|
||||
}
|
||||
}
|
||||
3
src/util/LinkTypes.ts
Normal file
3
src/util/LinkTypes.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const LINK_TYPE_LDPC = 'http://www.w3.org/ns/ldp#Container';
|
||||
export const LINK_TYPE_LDP_BC = 'http://www.w3.org/ns/ldp#BasicContainer';
|
||||
export const LINK_TYPE_LDPR = 'http://www.w3.org/ns/ldp#Resource';
|
||||
84
src/util/MetadataController.ts
Normal file
84
src/util/MetadataController.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { Readable } from 'stream';
|
||||
import { Stats } from 'fs';
|
||||
import streamifyArray from 'streamify-array';
|
||||
import { DataFactory, StreamParser, StreamWriter } from 'n3';
|
||||
import { LDP, RDF, STAT, TERMS, XML } from './Prefixes';
|
||||
import { NamedNode, Quad } from 'rdf-js';
|
||||
|
||||
export const TYPE_PREDICATE = DataFactory.namedNode(`${RDF}type`);
|
||||
export const MODIFIED_PREDICATE = DataFactory.namedNode(`${TERMS}modified`);
|
||||
export const CONTAINS_PREDICATE = DataFactory.namedNode(`${LDP}contains`);
|
||||
export const MTIME_PREDICATE = DataFactory.namedNode(`${STAT}mtime`);
|
||||
export const SIZE_PREDICATE = DataFactory.namedNode(`${STAT}size`);
|
||||
|
||||
export const CONTAINER_OBJECT = DataFactory.namedNode(`${LDP}Container`);
|
||||
export const BASIC_CONTAINER_OBJECT = DataFactory.namedNode(`${LDP}BasicContainer`);
|
||||
export const RESOURCE_OBJECT = DataFactory.namedNode(`${LDP}Resource`);
|
||||
export const DATETIME_OBJECT = DataFactory.namedNode(`${XML}dateTime`);
|
||||
|
||||
export class MetadataController {
|
||||
/**
|
||||
* Helper function to generate quads for a Container or Resource.
|
||||
* @param URI - The URI for which the quads should be generated.
|
||||
* @param stats - The Stats of the subject.
|
||||
*
|
||||
* @returns The generated quads.
|
||||
*/
|
||||
public generateResourceQuads(URI: string, stats: Stats): Quad[] {
|
||||
const subject: NamedNode = DataFactory.namedNode(URI);
|
||||
const quads: Quad[] = [];
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
quads.push(DataFactory.quad(subject, TYPE_PREDICATE, CONTAINER_OBJECT));
|
||||
quads.push(DataFactory.quad(subject, TYPE_PREDICATE, BASIC_CONTAINER_OBJECT));
|
||||
}
|
||||
quads.push(DataFactory.quad(subject, TYPE_PREDICATE, RESOURCE_OBJECT));
|
||||
quads.push(DataFactory.quad(subject, SIZE_PREDICATE, DataFactory.literal(stats.size)));
|
||||
quads.push(DataFactory.quad(
|
||||
subject,
|
||||
MODIFIED_PREDICATE,
|
||||
DataFactory.literal(stats.mtime.toUTCString(), DATETIME_OBJECT),
|
||||
));
|
||||
quads.push(DataFactory.quad(
|
||||
subject,
|
||||
MTIME_PREDICATE,
|
||||
DataFactory.literal(stats.mtime.getTime() / 100),
|
||||
));
|
||||
|
||||
return quads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to generate the quad describing that the resource URI is a child of the container URI.
|
||||
* @param containerURI - The URI of the container.
|
||||
* @param childURI - The URI of the child resource.
|
||||
*
|
||||
* @returns The generated quad.
|
||||
*/
|
||||
public generateContainerContainsResourceQuad(containerURI: string, childURI: string): Quad {
|
||||
return DataFactory.quad(DataFactory.namedNode(containerURI), CONTAINS_PREDICATE, DataFactory.namedNode(
|
||||
childURI,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert an array of quads into a Readable object.
|
||||
* @param quads - The array of quads.
|
||||
*
|
||||
* @returns The Readable object.
|
||||
*/
|
||||
public generateReadableFromQuads(quads: Quad[]): Readable {
|
||||
return streamifyArray(quads).pipe(new StreamWriter({ format: 'text/turtle' }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert a Readable into an array of quads.
|
||||
* @param readable - The readable object.
|
||||
*
|
||||
* @returns A promise containing the array of quads.
|
||||
*/
|
||||
public async generateQuadsFromReadable(readable: Readable): Promise<Quad[]> {
|
||||
return arrayifyStream(readable.pipe(new StreamParser({ format: 'text/turtle' })));
|
||||
}
|
||||
}
|
||||
5
src/util/Prefixes.ts
Normal file
5
src/util/Prefixes.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
||||
export const LDP = 'http://www.w3.org/ns/ldp#';
|
||||
export const TERMS = 'http://purl.org/dc/terms/';
|
||||
export const XML = 'http://www.w3.org/2001/XMLSchema#';
|
||||
export const STAT = 'http://www.w3.org/ns/posix/stat#';
|
||||
@@ -20,6 +20,15 @@ export const ensureTrailingSlash = (path: string): string => path.replace(/\/*$/
|
||||
*/
|
||||
export const readableToString = async(stream: Readable): Promise<string> => (await arrayifyStream(stream)).join('');
|
||||
|
||||
/**
|
||||
* Makes sure the input path has no slashes at the end.
|
||||
*
|
||||
* @param path - Path to check.
|
||||
*
|
||||
* @returns The potentially changed path.
|
||||
*/
|
||||
export const trimTrailingSlashes = (path: string): string => path.replace(/\/+$/u, '');
|
||||
|
||||
/**
|
||||
* Checks if the given two media types/ranges match each other.
|
||||
* Takes wildcards into account.
|
||||
|
||||
9
src/util/errors/ConflictHttpError.ts
Normal file
9
src/util/errors/ConflictHttpError.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { HttpError } from './HttpError';
|
||||
/**
|
||||
* An error thrown when a request conflict with current state of the server.
|
||||
*/
|
||||
export class ConflictHttpError extends HttpError {
|
||||
public constructor(message?: string) {
|
||||
super(409, 'ConflictHttpError', message);
|
||||
}
|
||||
}
|
||||
9
src/util/errors/MethodNotAllowedHttpError.ts
Normal file
9
src/util/errors/MethodNotAllowedHttpError.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { HttpError } from './HttpError';
|
||||
/**
|
||||
* An error thrown when data was found for the requested identifier, but is not supported by the target resource.
|
||||
*/
|
||||
export class MethodNotAllowedHttpError extends HttpError {
|
||||
public constructor(message?: string) {
|
||||
super(405, 'MethodNotAllowedHttpError', message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user