mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
refactor: Remove file and in memory stores
This commit is contained in:
parent
c999abb7b0
commit
03c64e5617
@ -2,10 +2,33 @@
|
|||||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
|
||||||
"@graph": [
|
"@graph": [
|
||||||
{
|
{
|
||||||
"@id": "urn:solid-server:default:ResourceStore",
|
"@id": "urn:solid-server:default:MetadataController",
|
||||||
"@type": "InMemoryResourceStore",
|
"@type": "MetadataController"
|
||||||
"InMemoryResourceStore:_base": {
|
},
|
||||||
|
{
|
||||||
|
"@id": "urn:solid-server:default:DataAccessor",
|
||||||
|
"@type": "InMemoryDataAccessor",
|
||||||
|
"InMemoryDataAccessor:_base": {
|
||||||
"@id": "urn:solid-server:default:variable:base"
|
"@id": "urn:solid-server:default:variable:base"
|
||||||
|
},
|
||||||
|
"InMemoryDataAccessor:_metadataController": {
|
||||||
|
"@id": "urn:solid-server:default:MetadataController"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "urn:solid-server:default:ResourceStore",
|
||||||
|
"@type": "DataAccessorBasedStore",
|
||||||
|
"DataAccessorBasedStore:_accessor": {
|
||||||
|
"@id": "urn:solid-server:default:DataAccessor"
|
||||||
|
},
|
||||||
|
"DataAccessorBasedStore:_base": {
|
||||||
|
"@id": "urn:solid-server:default:variable:base"
|
||||||
|
},
|
||||||
|
"DataAccessorBasedStore:_metadataController": {
|
||||||
|
"@id": "urn:solid-server:default:MetadataController"
|
||||||
|
},
|
||||||
|
"DataAccessorBasedStore:_containerManager": {
|
||||||
|
"@id": "urn:solid-server:default:UrlContainerManager"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
3
index.ts
3
index.ts
@ -102,8 +102,6 @@ export * from './src/storage/Conditions';
|
|||||||
export * from './src/storage/ContainerManager';
|
export * from './src/storage/ContainerManager';
|
||||||
export * from './src/storage/DataAccessorBasedStore';
|
export * from './src/storage/DataAccessorBasedStore';
|
||||||
export * from './src/storage/ExtensionBasedMapper';
|
export * from './src/storage/ExtensionBasedMapper';
|
||||||
export * from './src/storage/FileResourceStore';
|
|
||||||
export * from './src/storage/InMemoryResourceStore';
|
|
||||||
export * from './src/storage/Lock';
|
export * from './src/storage/Lock';
|
||||||
export * from './src/storage/LockingResourceStore';
|
export * from './src/storage/LockingResourceStore';
|
||||||
export * from './src/storage/PassthroughStore';
|
export * from './src/storage/PassthroughStore';
|
||||||
@ -130,6 +128,5 @@ export * from './src/util/errors/UnsupportedMediaTypeHttpError';
|
|||||||
export * from './src/util/HeaderUtil';
|
export * from './src/util/HeaderUtil';
|
||||||
export * from './src/util/AsyncHandler';
|
export * from './src/util/AsyncHandler';
|
||||||
export * from './src/util/CompositeAsyncHandler';
|
export * from './src/util/CompositeAsyncHandler';
|
||||||
export * from './src/util/InteractionController';
|
|
||||||
export * from './src/util/MetadataController';
|
export * from './src/util/MetadataController';
|
||||||
export * from './src/util/Util';
|
export * from './src/util/Util';
|
||||||
|
@ -1,470 +0,0 @@
|
|||||||
import type { Stats } from 'fs';
|
|
||||||
import { createReadStream, createWriteStream, promises as fsPromises } from 'fs';
|
|
||||||
import { posix } from 'path';
|
|
||||||
import type { Readable } from 'stream';
|
|
||||||
import { DataFactory } from 'n3';
|
|
||||||
import type { NamedNode, Quad } from 'rdf-js';
|
|
||||||
import streamifyArray from 'streamify-array';
|
|
||||||
import type { Representation } from '../ldp/representation/Representation';
|
|
||||||
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
|
|
||||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
|
||||||
import { INTERNAL_QUADS } from '../util/ContentTypes';
|
|
||||||
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 type { InteractionController } from '../util/InteractionController';
|
|
||||||
import type { MetadataController } from '../util/MetadataController';
|
|
||||||
import { CONTENT_TYPE, DCTERMS, HTTP, POSIX, RDF, XSD } from '../util/UriConstants';
|
|
||||||
import { toNamedNode, toTypedLiteral } from '../util/UriUtil';
|
|
||||||
import { ensureTrailingSlash, pushQuad } from '../util/Util';
|
|
||||||
import type { ExtensionBasedMapper } from './ExtensionBasedMapper';
|
|
||||||
import type { ResourceLink } from './FileIdentifierMapper';
|
|
||||||
import type { ResourceStore } from './ResourceStore';
|
|
||||||
|
|
||||||
const { 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 interactionController: InteractionController;
|
|
||||||
private readonly metadataController: MetadataController;
|
|
||||||
private readonly resourceMapper: ExtensionBasedMapper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param resourceMapper - The file resource mapper.
|
|
||||||
* @param interactionController - Instance of InteractionController to use.
|
|
||||||
* @param metadataController - Instance of MetadataController to use.
|
|
||||||
*/
|
|
||||||
public constructor(resourceMapper: ExtensionBasedMapper, interactionController: InteractionController,
|
|
||||||
metadataController: MetadataController) {
|
|
||||||
this.interactionController = interactionController;
|
|
||||||
this.metadataController = metadataController;
|
|
||||||
this.resourceMapper = resourceMapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store the incoming data as a file under a file path corresponding to `container.path`,
|
|
||||||
* where slashes correspond to subdirectories.
|
|
||||||
* @param container - The identifier to store the new data under.
|
|
||||||
* @param representation - Data to store. Only File streams are supported.
|
|
||||||
*
|
|
||||||
* @returns The newly generated identifier.
|
|
||||||
*/
|
|
||||||
public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> {
|
|
||||||
if (!representation.binary) {
|
|
||||||
throw new UnsupportedMediaTypeHttpError('FileResourceStore only supports binary representations.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the path from the request URI, all metadata triples if any, and the Slug and Link header values.
|
|
||||||
const path = (await this.resourceMapper.mapUrlToFilePath(container)).filePath;
|
|
||||||
const slug = representation.metadata.get(HTTP.slug)?.value;
|
|
||||||
const types = representation.metadata.getAll(RDF.type);
|
|
||||||
|
|
||||||
// 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, types);
|
|
||||||
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.serializeQuads(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isContainer ?
|
|
||||||
this.createContainer(path, newIdentifier, path.endsWith('/'), metadata) :
|
|
||||||
this.createFile(path, newIdentifier, representation.data, path.endsWith('/'), metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the given resource.
|
|
||||||
* @param identifier - Identifier of resource to delete.
|
|
||||||
*/
|
|
||||||
public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
|
|
||||||
let path = this.resourceMapper.getRelativePath(identifier);
|
|
||||||
if (path === '' || ensureTrailingSlash(path) === '/') {
|
|
||||||
throw new MethodNotAllowedHttpError('Cannot delete root container.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the file status of the path defined by the request URI mapped to the corresponding filepath.
|
|
||||||
path = (await this.resourceMapper.mapUrlToFilePath(identifier)).filePath;
|
|
||||||
let stats;
|
|
||||||
try {
|
|
||||||
stats = await fsPromises.lstat(path);
|
|
||||||
} catch {
|
|
||||||
throw new NotFoundHttpError();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete as file or as directory according to the status.
|
|
||||||
if (stats.isFile()) {
|
|
||||||
await this.deleteFile(path);
|
|
||||||
} else if (stats.isDirectory()) {
|
|
||||||
await this.deleteDirectory(ensureTrailingSlash(path));
|
|
||||||
} else {
|
|
||||||
throw new NotFoundHttpError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the stored representation for the given identifier.
|
|
||||||
* No preferences are supported.
|
|
||||||
* @param identifier - Identifier to retrieve.
|
|
||||||
*
|
|
||||||
* @returns The corresponding Representation.
|
|
||||||
*/
|
|
||||||
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 resourceLink = await this.resourceMapper.mapUrlToFilePath(identifier);
|
|
||||||
let stats;
|
|
||||||
try {
|
|
||||||
stats = await fsPromises.lstat(resourceLink.filePath);
|
|
||||||
} catch {
|
|
||||||
throw new NotFoundHttpError();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the file or directory representation of the path according to its status.
|
|
||||||
if (stats.isFile()) {
|
|
||||||
return await this.getFileRepresentation(resourceLink, stats);
|
|
||||||
}
|
|
||||||
if (stats.isDirectory()) {
|
|
||||||
return await this.getDirectoryRepresentation(resourceLink, stats);
|
|
||||||
}
|
|
||||||
throw new NotFoundHttpError();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws Not supported.
|
|
||||||
*/
|
|
||||||
public async modifyResource(): Promise<void> {
|
|
||||||
throw new Error('Not supported.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces the stored Representation with the new one for the given identifier.
|
|
||||||
* @param identifier - Identifier to replace.
|
|
||||||
* @param representation - New Representation.
|
|
||||||
*/
|
|
||||||
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): Promise<void> {
|
|
||||||
if (!representation.binary) {
|
|
||||||
throw new UnsupportedMediaTypeHttpError('FileResourceStore only supports binary representations.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Break up the request URI in the different parts `containerPath` and `documentName` as we know their semantics
|
|
||||||
// from addResource to call the InteractionController in the same way.
|
|
||||||
const { containerPath, documentName } = this.resourceMapper.extractDocumentName(identifier);
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
representation.metadata.identifier = DataFactory.namedNode(identifier.path);
|
|
||||||
const raw = representation.metadata.quads();
|
|
||||||
const types = representation.metadata.getAll(RDF.type);
|
|
||||||
let metadata: Readable | undefined;
|
|
||||||
if (raw.length > 0) {
|
|
||||||
metadata = this.metadataController.serializeQuads(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(documentName, types);
|
|
||||||
const newIdentifier = this.interactionController.generateIdentifier(isContainer, documentName);
|
|
||||||
return isContainer ?
|
|
||||||
await this.setDirectoryRepresentation(containerPath, newIdentifier, metadata) :
|
|
||||||
await this.setFileRepresentation(containerPath, newIdentifier, representation.data, metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to delete a file and its corresponding metadata file if such exists.
|
|
||||||
* @param path - The path to the file.
|
|
||||||
*/
|
|
||||||
private async deleteFile(path: string): Promise<void> {
|
|
||||||
await fsPromises.unlink(path);
|
|
||||||
|
|
||||||
// Only delete the metadata file as auxiliary resource because this is the only file created by this store.
|
|
||||||
try {
|
|
||||||
await fsPromises.unlink(`${path}.metadata`);
|
|
||||||
} catch {
|
|
||||||
// It's ok if there was no metadata file.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to delete a directory and its corresponding metadata file if such exists.
|
|
||||||
* @param path - The path to the directory.
|
|
||||||
*/
|
|
||||||
private async deleteDirectory(path: string): Promise<void> {
|
|
||||||
const files = await fsPromises.readdir(path);
|
|
||||||
const match = files.find((file): any => !file.startsWith('.metadata'));
|
|
||||||
if (typeof match === 'string') {
|
|
||||||
throw new ConflictHttpError('Can only delete empty containers.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only delete the metadata file as auxiliary resource because this is the only file created by this store.
|
|
||||||
try {
|
|
||||||
await fsPromises.unlink(joinPath(path, '.metadata'));
|
|
||||||
} catch {
|
|
||||||
// It's ok if there was no metadata file.
|
|
||||||
}
|
|
||||||
|
|
||||||
await fsPromises.rmdir(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to get the representation of a file in the file system.
|
|
||||||
* It loads the quads from the corresponding metadata file if it exists.
|
|
||||||
* @param resourceLink - The path information of the resource.
|
|
||||||
* @param stats - The Stats of the file.
|
|
||||||
*
|
|
||||||
* @returns The corresponding Representation.
|
|
||||||
*/
|
|
||||||
private async getFileRepresentation(resourceLink: ResourceLink, stats: Stats): Promise<Representation> {
|
|
||||||
const readStream = createReadStream(resourceLink.filePath);
|
|
||||||
let rawMetadata: Quad[] = [];
|
|
||||||
try {
|
|
||||||
const readMetadataStream = createReadStream(`${resourceLink.filePath}.metadata`);
|
|
||||||
rawMetadata = await this.metadataController.parseQuads(readMetadataStream);
|
|
||||||
} catch {
|
|
||||||
// Metadata file doesn't exist so lets keep `rawMetaData` an empty array.
|
|
||||||
}
|
|
||||||
const metadata = new RepresentationMetadata(resourceLink.identifier.path).addQuads(rawMetadata)
|
|
||||||
.set(DCTERMS.modified, toTypedLiteral(stats.mtime.toISOString(), XSD.dateTime))
|
|
||||||
.set(POSIX.size, toTypedLiteral(stats.size, XSD.integer));
|
|
||||||
metadata.contentType = resourceLink.contentType;
|
|
||||||
return { metadata, data: readStream, binary: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to get the representation of a directory in the file system.
|
|
||||||
* It loads the quads from the corresponding metadata file if it exists
|
|
||||||
* and generates quad representations for all its children.
|
|
||||||
*
|
|
||||||
* @param resourceLink - The path information of the resource.
|
|
||||||
* @param stats - The Stats of the directory.
|
|
||||||
*
|
|
||||||
* @returns The corresponding Representation.
|
|
||||||
*/
|
|
||||||
private async getDirectoryRepresentation(resourceLink: ResourceLink, stats: Stats): Promise<Representation> {
|
|
||||||
const files = await fsPromises.readdir(resourceLink.filePath);
|
|
||||||
|
|
||||||
const containerURI = DataFactory.namedNode(resourceLink.identifier.path);
|
|
||||||
|
|
||||||
const quads = this.metadataController.generateResourceQuads(containerURI, true);
|
|
||||||
quads.push(...this.generatePosixQuads(containerURI, stats));
|
|
||||||
quads.push(...await this.getDirChildrenQuadRepresentation(files, resourceLink.filePath, containerURI));
|
|
||||||
|
|
||||||
let rawMetadata: Quad[] = [];
|
|
||||||
try {
|
|
||||||
const readMetadataStream = createReadStream(joinPath(resourceLink.filePath, '.metadata'));
|
|
||||||
rawMetadata = await this.metadataController.parseQuads(readMetadataStream);
|
|
||||||
} catch {
|
|
||||||
// Metadata file doesn't exist so lets keep `rawMetaData` an empty array.
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = new RepresentationMetadata(containerURI, {
|
|
||||||
[DCTERMS.modified]: toTypedLiteral(stats.mtime.toISOString(), XSD.dateTime),
|
|
||||||
[CONTENT_TYPE]: INTERNAL_QUADS,
|
|
||||||
});
|
|
||||||
metadata.addQuads(rawMetadata);
|
|
||||||
|
|
||||||
return {
|
|
||||||
binary: false,
|
|
||||||
data: streamifyArray(quads),
|
|
||||||
metadata,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to get quad representations for all children in a directory.
|
|
||||||
* @param files - List of all children in the directory.
|
|
||||||
* @param path - The path to the directory.
|
|
||||||
* @param containerURI - The URI of the directory.
|
|
||||||
*
|
|
||||||
* @returns A promise containing all quads.
|
|
||||||
*/
|
|
||||||
private async getDirChildrenQuadRepresentation(files: string[], path: string, containerURI: NamedNode):
|
|
||||||
Promise<Quad[]> {
|
|
||||||
const quads: Quad[] = [];
|
|
||||||
const childURIs: string[] = [];
|
|
||||||
for (const childName of files) {
|
|
||||||
try {
|
|
||||||
const childStats = await fsPromises.lstat(joinPath(path, childName));
|
|
||||||
if (!childStats.isFile() && !childStats.isDirectory()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const childLink = await this.resourceMapper
|
|
||||||
.mapFilePathToUrl(joinPath(path, childName), childStats.isDirectory());
|
|
||||||
|
|
||||||
const subject = DataFactory.namedNode(childLink.identifier.path);
|
|
||||||
quads.push(...this.metadataController.generateResourceQuads(subject, childStats.isDirectory()));
|
|
||||||
quads.push(...this.generatePosixQuads(subject, childStats));
|
|
||||||
childURIs.push(childLink.identifier.path);
|
|
||||||
} catch {
|
|
||||||
// Skip the child if there is an error.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const containsQuads = this.metadataController.generateContainerContainsResourceQuads(containerURI, childURIs);
|
|
||||||
|
|
||||||
return quads.concat(containsQuads);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to add file system related metadata
|
|
||||||
* @param subject - Subject for the new quads.
|
|
||||||
* @param stats - Stats of the file/directory corresponding to the resource.
|
|
||||||
*/
|
|
||||||
private generatePosixQuads(subject: NamedNode, stats: Stats): Quad[] {
|
|
||||||
const quads: Quad[] = [];
|
|
||||||
pushQuad(quads, subject, toNamedNode(POSIX.size), toTypedLiteral(stats.size, XSD.integer));
|
|
||||||
pushQuad(quads, subject, toNamedNode(DCTERMS.modified), toTypedLiteral(stats.mtime.toISOString(), XSD.dateTime));
|
|
||||||
pushQuad(quads, subject, toNamedNode(POSIX.mtime), toTypedLiteral(
|
|
||||||
Math.floor(stats.mtime.getTime() / 1000), XSD.integer,
|
|
||||||
));
|
|
||||||
return quads;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to (re)write file for the resource if no container with that identifier exists.
|
|
||||||
* @param path - The path to the directory of the file.
|
|
||||||
* @param newIdentifier - The name of the file to be created or overwritten.
|
|
||||||
* @param data - The data to be put in the file.
|
|
||||||
* @param metadata - Optional metadata.
|
|
||||||
*/
|
|
||||||
private async setFileRepresentation(path: string, newIdentifier: string, data: Readable, metadata?: Readable):
|
|
||||||
Promise<void> {
|
|
||||||
// (Re)write file for the resource if no container with that identifier exists.
|
|
||||||
let stats;
|
|
||||||
try {
|
|
||||||
stats = await fsPromises.lstat(joinPath(path, newIdentifier));
|
|
||||||
} catch {
|
|
||||||
await this.createFile(path, newIdentifier, data, true, metadata);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (stats.isFile()) {
|
|
||||||
await this.createFile(path, newIdentifier, data, true, metadata);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new ConflictHttpError('Container with that identifier already exists.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to create a container if the identifier doesn't exist yet.
|
|
||||||
* @param path - The path to the parent directory in which the new directory should be created.
|
|
||||||
* @param newIdentifier - The name of the directory to be created.
|
|
||||||
* @param metadata - Optional metadata.
|
|
||||||
*/
|
|
||||||
private async setDirectoryRepresentation(path: string, newIdentifier: string, metadata?: Readable): Promise<void> {
|
|
||||||
// Create a container if the identifier doesn't exist yet.
|
|
||||||
try {
|
|
||||||
await fsPromises.access(joinPath(path, newIdentifier));
|
|
||||||
throw new ConflictHttpError('Resource with that identifier already exists.');
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof ConflictHttpError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identifier doesn't exist yet so we can create a container.
|
|
||||||
await this.createContainer(path, newIdentifier, true, metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a file to represent a resource.
|
|
||||||
* @param path - The path to the directory in which the file should be created.
|
|
||||||
* @param resourceName - The name of the file to be created.
|
|
||||||
* @param data - The data to be put in the file.
|
|
||||||
* @param allowRecursiveCreation - Whether necessary but not existing intermediate containers may be created.
|
|
||||||
* @param metadata - Optional metadata that will be stored at `path/resourceName.metadata` if set.
|
|
||||||
*
|
|
||||||
* @returns Promise of the identifier of the newly created resource.
|
|
||||||
*/
|
|
||||||
private async createFile(path: string, resourceName: string, data: Readable,
|
|
||||||
allowRecursiveCreation: boolean, metadata?: Readable): Promise<ResourceIdentifier> {
|
|
||||||
// Create the intermediate containers if `allowRecursiveCreation` is true.
|
|
||||||
if (allowRecursiveCreation) {
|
|
||||||
await this.createContainer(path, '', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the file status of the filepath of the directory where the file is to be created.
|
|
||||||
const stats = await fsPromises.lstat(path);
|
|
||||||
|
|
||||||
// Only create the file if the provided filepath is a valid directory.
|
|
||||||
if (!stats.isDirectory()) {
|
|
||||||
throw new MethodNotAllowedHttpError('The given path is not a valid container.');
|
|
||||||
} else {
|
|
||||||
// If metadata is specified, save it in a corresponding metadata file.
|
|
||||||
if (metadata) {
|
|
||||||
await this.createDataFile(joinPath(path, `${resourceName}.metadata`), metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no error thrown from above, indicating failed metadata file creation, create the actual resource file.
|
|
||||||
try {
|
|
||||||
const fullPath = joinPath(path, resourceName);
|
|
||||||
await this.createDataFile(fullPath, data);
|
|
||||||
return (await this.resourceMapper.mapFilePathToUrl(fullPath, false)).identifier;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
// Normal file has not been created so we don't want the metadata file to remain.
|
|
||||||
await fsPromises.unlink(joinPath(path, `${resourceName}.metadata`));
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a directory to represent a container.
|
|
||||||
* @param path - The path to the parent directory in which the new directory should be created.
|
|
||||||
* @param containerName - The name of the directory to be created.
|
|
||||||
* @param allowRecursiveCreation - Whether necessary but not existing intermediate containers may be created.
|
|
||||||
* @param metadata - Optional metadata that will be stored at `path/containerName/.metadata` if set.
|
|
||||||
*
|
|
||||||
* @returns Promise of the identifier of the newly created container.
|
|
||||||
*/
|
|
||||||
private async createContainer(path: string, containerName: string,
|
|
||||||
allowRecursiveCreation: boolean, metadata?: Readable): Promise<ResourceIdentifier> {
|
|
||||||
const fullPath = joinPath(path, containerName);
|
|
||||||
|
|
||||||
// If recursive creation is not allowed, check if the parent container exists and then create the child directory.
|
|
||||||
try {
|
|
||||||
if (!allowRecursiveCreation) {
|
|
||||||
const stats = await fsPromises.lstat(path);
|
|
||||||
if (!stats.isDirectory()) {
|
|
||||||
throw new MethodNotAllowedHttpError('The given path is not a valid container.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await fsPromises.mkdir(fullPath, { recursive: allowRecursiveCreation });
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof MethodNotAllowedHttpError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new MethodNotAllowedHttpError();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no error thrown from above, indicating failed container creation, create a corresponding metadata file in the
|
|
||||||
// new directory if applicable.
|
|
||||||
if (metadata) {
|
|
||||||
try {
|
|
||||||
await this.createDataFile(joinPath(fullPath, '.metadata'), metadata);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
// Failed to create the metadata file so remove the created directory.
|
|
||||||
await fsPromises.rmdir(fullPath);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (await this.resourceMapper.mapFilePathToUrl(fullPath, true)).identifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function without extra validation checking to create a data file.
|
|
||||||
* @param path - The filepath of the file to be created.
|
|
||||||
* @param data - The data to be put in the file.
|
|
||||||
*/
|
|
||||||
private async createDataFile(path: string, data: Readable): Promise<void> {
|
|
||||||
return new Promise((resolve, reject): any => {
|
|
||||||
const writeStream = createWriteStream(path);
|
|
||||||
data.pipe(writeStream);
|
|
||||||
data.on('error', reject);
|
|
||||||
|
|
||||||
writeStream.on('error', reject);
|
|
||||||
writeStream.on('finish', resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,169 +0,0 @@
|
|||||||
import { PassThrough } from 'stream';
|
|
||||||
import arrayifyStream from 'arrayify-stream';
|
|
||||||
import streamifyArray from 'streamify-array';
|
|
||||||
import type { Representation } from '../ldp/representation/Representation';
|
|
||||||
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
|
|
||||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
|
||||||
import { TEXT_TURTLE } from '../util/ContentTypes';
|
|
||||||
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
|
||||||
import { CONTENT_TYPE } from '../util/UriConstants';
|
|
||||||
import { ensureTrailingSlash } from '../util/Util';
|
|
||||||
import type { ResourceStore } from './ResourceStore';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resource store storing its data in an in-memory map.
|
|
||||||
* Current Solid functionality support is quite basic: containers are not really supported for example.
|
|
||||||
*/
|
|
||||||
export class InMemoryResourceStore implements ResourceStore {
|
|
||||||
private readonly store: { [id: string]: Representation };
|
|
||||||
private readonly base: string;
|
|
||||||
private index = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param base - Base that will be stripped of all incoming URIs
|
|
||||||
* and added to all outgoing ones to find the relative path.
|
|
||||||
*/
|
|
||||||
public constructor(base: string) {
|
|
||||||
this.base = ensureTrailingSlash(base);
|
|
||||||
|
|
||||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: TEXT_TURTLE });
|
|
||||||
this.store = {
|
|
||||||
// Default root entry (what you get when the identifier is equal to the base)
|
|
||||||
'': {
|
|
||||||
binary: true,
|
|
||||||
data: streamifyArray([]),
|
|
||||||
metadata,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the incoming data under a new URL corresponding to `container.path + number`.
|
|
||||||
* Slash added when needed.
|
|
||||||
* @param container - The identifier to store the new data under.
|
|
||||||
* @param representation - Data to store.
|
|
||||||
*
|
|
||||||
* @returns The newly generated identifier.
|
|
||||||
*/
|
|
||||||
public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> {
|
|
||||||
const containerPath = this.parseIdentifier(container);
|
|
||||||
this.checkPath(containerPath);
|
|
||||||
const newID = { path: `${ensureTrailingSlash(container.path)}${this.index}` };
|
|
||||||
const newPath = this.parseIdentifier(newID);
|
|
||||||
this.index += 1;
|
|
||||||
this.store[newPath] = await this.copyRepresentation(representation);
|
|
||||||
return newID;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the given resource.
|
|
||||||
* @param identifier - Identifier of resource to delete.
|
|
||||||
*/
|
|
||||||
public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
|
|
||||||
const path = this.parseIdentifier(identifier);
|
|
||||||
this.checkPath(path);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
||||||
delete this.store[path];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the stored representation for the given identifier.
|
|
||||||
* Preferences will be ignored, data will be returned as it was received.
|
|
||||||
*
|
|
||||||
* @param identifier - Identifier to retrieve.
|
|
||||||
*
|
|
||||||
* @returns The corresponding Representation.
|
|
||||||
*/
|
|
||||||
public async getRepresentation(identifier: ResourceIdentifier): Promise<Representation> {
|
|
||||||
const path = this.parseIdentifier(identifier);
|
|
||||||
this.checkPath(path);
|
|
||||||
return this.generateRepresentation(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws Not supported.
|
|
||||||
*/
|
|
||||||
public async modifyResource(): Promise<void> {
|
|
||||||
throw new Error('Not supported.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Puts the given data in the given location.
|
|
||||||
* @param identifier - Identifier to replace.
|
|
||||||
* @param representation - New Representation.
|
|
||||||
*/
|
|
||||||
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): Promise<void> {
|
|
||||||
const path = this.parseIdentifier(identifier);
|
|
||||||
this.store[path] = await this.copyRepresentation(representation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strips the base from the identifier and checks if it is valid.
|
|
||||||
* @param identifier - Incoming identifier.
|
|
||||||
*
|
|
||||||
* @throws {@link NotFoundHttpError}
|
|
||||||
* If the identifier doesn't start with the base ID.
|
|
||||||
*
|
|
||||||
* @returns A string representing the relative path.
|
|
||||||
*/
|
|
||||||
private parseIdentifier(identifier: ResourceIdentifier): string {
|
|
||||||
const path = identifier.path.slice(this.base.length);
|
|
||||||
if (!identifier.path.startsWith(this.base)) {
|
|
||||||
throw new NotFoundHttpError();
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the relative path is in the store.
|
|
||||||
* @param path - Incoming identifier.
|
|
||||||
*
|
|
||||||
* @throws {@link NotFoundHttpError}
|
|
||||||
* If the path is not in the store.
|
|
||||||
*/
|
|
||||||
private checkPath(path: string): void {
|
|
||||||
if (!this.store[path]) {
|
|
||||||
throw new NotFoundHttpError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies the Representation by draining the original data stream and creating a new one.
|
|
||||||
*
|
|
||||||
* @param source - Incoming Representation.
|
|
||||||
*/
|
|
||||||
private async copyRepresentation(source: Representation): Promise<Representation> {
|
|
||||||
return {
|
|
||||||
binary: source.binary,
|
|
||||||
data: streamifyArray(await arrayifyStream(source.data)),
|
|
||||||
metadata: source.metadata,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a Representation that is identical to the one stored,
|
|
||||||
* but makes sure to duplicate the data stream so it stays readable for later calls.
|
|
||||||
*
|
|
||||||
* @param path - Path in store of Representation.
|
|
||||||
*
|
|
||||||
* @returns The resulting Representation.
|
|
||||||
*/
|
|
||||||
private async generateRepresentation(path: string): Promise<Representation> {
|
|
||||||
// Note: when converting to a complete ResourceStore and using readable-stream
|
|
||||||
// object mode should be set correctly here (now fixed due to Node 10)
|
|
||||||
const source = this.store[path];
|
|
||||||
const objectMode = { writableObjectMode: true, readableObjectMode: true };
|
|
||||||
const streamOutput = new PassThrough(objectMode);
|
|
||||||
const streamInternal = new PassThrough({ ...objectMode, highWaterMark: Number.MAX_SAFE_INTEGER });
|
|
||||||
source.data.pipe(streamOutput);
|
|
||||||
source.data.pipe(streamInternal);
|
|
||||||
|
|
||||||
source.data = streamInternal;
|
|
||||||
|
|
||||||
return {
|
|
||||||
binary: source.binary,
|
|
||||||
data: streamOutput,
|
|
||||||
metadata: source.metadata,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import type { Term } from 'rdf-js';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { LDP } from './UriConstants';
|
|
||||||
import { trimTrailingSlashes } from './Util';
|
|
||||||
|
|
||||||
export class InteractionController {
|
|
||||||
/**
|
|
||||||
* Check whether a new container or a resource should be created based on the given parameters.
|
|
||||||
* @param slug - Incoming slug metadata.
|
|
||||||
* @param types - Incoming type metadata.
|
|
||||||
*/
|
|
||||||
public isContainer(slug?: string, types?: Term[]): boolean {
|
|
||||||
if (types && types.length > 0) {
|
|
||||||
return types.some((type): boolean => type.value === LDP.Container || type.value === LDP.BasicContainer);
|
|
||||||
}
|
|
||||||
return Boolean(slug?.endsWith('/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the identifier path the new resource should have.
|
|
||||||
* @param isContainer - Whether or not the resource is a container.
|
|
||||||
* @param slug - Incoming slug metadata.
|
|
||||||
*/
|
|
||||||
public generateIdentifier(isContainer: boolean, slug?: string): string {
|
|
||||||
if (!slug) {
|
|
||||||
return `${uuid()}${isContainer ? '/' : ''}`;
|
|
||||||
}
|
|
||||||
return `${trimTrailingSlashes(slug)}${isContainer ? '/' : ''}`;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
import type { HttpHandler,
|
|
||||||
ResourceStore } from '../../index';
|
|
||||||
import {
|
|
||||||
AuthenticatedLdpHandler,
|
|
||||||
BasicResponseWriter,
|
|
||||||
CompositeAsyncHandler,
|
|
||||||
MethodPermissionsExtractor,
|
|
||||||
RdfToQuadConverter,
|
|
||||||
UnsecureWebIdExtractor,
|
|
||||||
QuadToRdfConverter,
|
|
||||||
} from '../../index';
|
|
||||||
import type { ServerConfig } from './ServerConfig';
|
|
||||||
import {
|
|
||||||
getFileResourceStore,
|
|
||||||
getConvertingStore,
|
|
||||||
getBasicRequestParser,
|
|
||||||
getOperationHandler,
|
|
||||||
getWebAclAuthorizer,
|
|
||||||
} from './Util';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AuthenticatedFileResourceStoreConfig works with
|
|
||||||
* - a WebAclAuthorizer
|
|
||||||
* - a FileResourceStore wrapped in a converting store (rdf to quad & quad to rdf)
|
|
||||||
* - GET, POST, PUT & DELETE operation handlers
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class AuthenticatedFileResourceStoreConfig implements ServerConfig {
|
|
||||||
public base: string;
|
|
||||||
public store: ResourceStore;
|
|
||||||
|
|
||||||
public constructor(base: string, rootFilepath: string) {
|
|
||||||
this.base = base;
|
|
||||||
this.store = getConvertingStore(
|
|
||||||
getFileResourceStore(base, rootFilepath),
|
|
||||||
[ new QuadToRdfConverter(),
|
|
||||||
new RdfToQuadConverter() ],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getHttpHandler(): HttpHandler {
|
|
||||||
const requestParser = getBasicRequestParser();
|
|
||||||
|
|
||||||
const credentialsExtractor = new UnsecureWebIdExtractor();
|
|
||||||
const permissionsExtractor = new CompositeAsyncHandler([
|
|
||||||
new MethodPermissionsExtractor(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const operationHandler = getOperationHandler(this.store);
|
|
||||||
|
|
||||||
const responseWriter = new BasicResponseWriter();
|
|
||||||
const authorizer = getWebAclAuthorizer(this.store, this.base);
|
|
||||||
|
|
||||||
const handler = new AuthenticatedLdpHandler({
|
|
||||||
requestParser,
|
|
||||||
credentialsExtractor,
|
|
||||||
permissionsExtractor,
|
|
||||||
authorizer,
|
|
||||||
operationHandler,
|
|
||||||
responseWriter,
|
|
||||||
});
|
|
||||||
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
import type { HttpHandler,
|
|
||||||
ResourceStore } from '../../index';
|
|
||||||
import {
|
|
||||||
AllowEverythingAuthorizer,
|
|
||||||
AuthenticatedLdpHandler,
|
|
||||||
BasicResponseWriter,
|
|
||||||
CompositeAsyncHandler,
|
|
||||||
MethodPermissionsExtractor,
|
|
||||||
QuadToRdfConverter,
|
|
||||||
RawBodyParser,
|
|
||||||
RdfToQuadConverter,
|
|
||||||
UnsecureWebIdExtractor,
|
|
||||||
} from '../../index';
|
|
||||||
import type { ServerConfig } from './ServerConfig';
|
|
||||||
import {
|
|
||||||
getFileResourceStore,
|
|
||||||
getOperationHandler,
|
|
||||||
getConvertingStore,
|
|
||||||
getBasicRequestParser,
|
|
||||||
} from './Util';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* FileResourceStoreConfig works with
|
|
||||||
* - an AllowEverythingAuthorizer (no acl)
|
|
||||||
* - a FileResourceStore wrapped in a converting store (rdf to quad & quad to rdf)
|
|
||||||
* - GET, POST, PUT & DELETE operation handlers
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class FileResourceStoreConfig implements ServerConfig {
|
|
||||||
public store: ResourceStore;
|
|
||||||
|
|
||||||
public constructor(base: string, rootFilepath: string) {
|
|
||||||
this.store = getConvertingStore(
|
|
||||||
getFileResourceStore(base, rootFilepath),
|
|
||||||
[ new QuadToRdfConverter(), new RdfToQuadConverter() ],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getHttpHandler(): HttpHandler {
|
|
||||||
// This is for the sake of test coverage, as it could also be just getBasicRequestParser()
|
|
||||||
const requestParser = getBasicRequestParser([ new RawBodyParser() ]);
|
|
||||||
|
|
||||||
const credentialsExtractor = new UnsecureWebIdExtractor();
|
|
||||||
const permissionsExtractor = new CompositeAsyncHandler([
|
|
||||||
new MethodPermissionsExtractor(),
|
|
||||||
]);
|
|
||||||
const authorizer = new AllowEverythingAuthorizer();
|
|
||||||
|
|
||||||
const operationHandler = getOperationHandler(this.store);
|
|
||||||
const responseWriter = new BasicResponseWriter();
|
|
||||||
|
|
||||||
const handler = new AuthenticatedLdpHandler({
|
|
||||||
requestParser,
|
|
||||||
credentialsExtractor,
|
|
||||||
permissionsExtractor,
|
|
||||||
authorizer,
|
|
||||||
operationHandler,
|
|
||||||
responseWriter,
|
|
||||||
});
|
|
||||||
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
}
|
|
@ -16,12 +16,9 @@ import {
|
|||||||
ContentTypeParser,
|
ContentTypeParser,
|
||||||
DataAccessorBasedStore,
|
DataAccessorBasedStore,
|
||||||
DeleteOperationHandler,
|
DeleteOperationHandler,
|
||||||
ExtensionBasedMapper,
|
|
||||||
FileResourceStore,
|
|
||||||
GetOperationHandler,
|
GetOperationHandler,
|
||||||
HeadOperationHandler,
|
HeadOperationHandler,
|
||||||
InMemoryResourceStore,
|
InMemoryDataAccessor,
|
||||||
InteractionController,
|
|
||||||
LinkTypeParser,
|
LinkTypeParser,
|
||||||
MetadataController,
|
MetadataController,
|
||||||
PatchingStore,
|
PatchingStore,
|
||||||
@ -46,20 +43,6 @@ export const BASE = 'http://test.com';
|
|||||||
*/
|
*/
|
||||||
export const getRootFilePath = (subfolder: string): string => join(__dirname, '../testData', subfolder);
|
export const getRootFilePath = (subfolder: string): string => join(__dirname, '../testData', subfolder);
|
||||||
|
|
||||||
/**
|
|
||||||
* Gives a file resource store based on (default) runtime config.
|
|
||||||
* @param base - Base URL.
|
|
||||||
* @param rootFilepath - The root file path.
|
|
||||||
*
|
|
||||||
* @returns The file resource store.
|
|
||||||
*/
|
|
||||||
export const getFileResourceStore = (base: string, rootFilepath: string): FileResourceStore =>
|
|
||||||
new FileResourceStore(
|
|
||||||
new ExtensionBasedMapper(base, rootFilepath),
|
|
||||||
new InteractionController(),
|
|
||||||
new MetadataController(),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gives a file data accessor store based on (default) runtime config.
|
* Gives a file data accessor store based on (default) runtime config.
|
||||||
* @param base - Base URL.
|
* @param base - Base URL.
|
||||||
@ -81,8 +64,8 @@ export const getDataAccessorStore = (base: string, dataAccessor: DataAccessor):
|
|||||||
*
|
*
|
||||||
* @returns The in memory resource store.
|
* @returns The in memory resource store.
|
||||||
*/
|
*/
|
||||||
export const getInMemoryResourceStore = (base = BASE): InMemoryResourceStore =>
|
export const getInMemoryResourceStore = (base = BASE): DataAccessorBasedStore =>
|
||||||
new InMemoryResourceStore(base);
|
getDataAccessorStore(base, new InMemoryDataAccessor(BASE, new MetadataController()));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gives a converting store given some converters.
|
* Gives a converting store given some converters.
|
||||||
|
@ -1,27 +1,30 @@
|
|||||||
import { copyFileSync, mkdirSync } from 'fs';
|
import { createReadStream, mkdirSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import * as rimraf from 'rimraf';
|
import * as rimraf from 'rimraf';
|
||||||
|
import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
|
||||||
import { FileDataAccessor } from '../../src/storage/accessors/FileDataAccessor';
|
import { FileDataAccessor } from '../../src/storage/accessors/FileDataAccessor';
|
||||||
|
import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor';
|
||||||
import { ExtensionBasedMapper } from '../../src/storage/ExtensionBasedMapper';
|
import { ExtensionBasedMapper } from '../../src/storage/ExtensionBasedMapper';
|
||||||
import { MetadataController } from '../../src/util/MetadataController';
|
import { MetadataController } from '../../src/util/MetadataController';
|
||||||
|
import { CONTENT_TYPE } from '../../src/util/UriConstants';
|
||||||
import { ensureTrailingSlash } from '../../src/util/Util';
|
import { ensureTrailingSlash } from '../../src/util/Util';
|
||||||
import { AuthenticatedDataAccessorBasedConfig } from '../configs/AuthenticatedDataAccessorBasedConfig';
|
import { AuthenticatedDataAccessorBasedConfig } from '../configs/AuthenticatedDataAccessorBasedConfig';
|
||||||
import { AuthenticatedFileResourceStoreConfig } from '../configs/AuthenticatedFileResourceStoreConfig';
|
|
||||||
import type { ServerConfig } from '../configs/ServerConfig';
|
import type { ServerConfig } from '../configs/ServerConfig';
|
||||||
import { BASE, getRootFilePath } from '../configs/Util';
|
import { BASE, getRootFilePath } from '../configs/Util';
|
||||||
import { AclTestHelper, FileTestHelper } from '../util/TestHelpers';
|
import { AclTestHelper, FileTestHelper } from '../util/TestHelpers';
|
||||||
|
|
||||||
const fileResourceStore: [string, (rootFilePath: string) => ServerConfig] = [
|
|
||||||
'AuthenticatedFileResourceStore',
|
|
||||||
(rootFilePath: string): ServerConfig => new AuthenticatedFileResourceStoreConfig(BASE, rootFilePath),
|
|
||||||
];
|
|
||||||
const dataAccessorStore: [string, (rootFilePath: string) => ServerConfig] = [
|
const dataAccessorStore: [string, (rootFilePath: string) => ServerConfig] = [
|
||||||
'AuthenticatedFileDataAccessorBasedStore',
|
'AuthenticatedFileDataAccessorBasedStore',
|
||||||
(rootFilePath: string): ServerConfig => new AuthenticatedDataAccessorBasedConfig(BASE,
|
(rootFilePath: string): ServerConfig => new AuthenticatedDataAccessorBasedConfig(BASE,
|
||||||
new FileDataAccessor(new ExtensionBasedMapper(BASE, rootFilePath), new MetadataController())),
|
new FileDataAccessor(new ExtensionBasedMapper(BASE, rootFilePath), new MetadataController())),
|
||||||
];
|
];
|
||||||
|
const inMemoryDataAccessorStore: [string, (rootFilePath: string) => ServerConfig] = [
|
||||||
|
'AuthenticatedInMemoryDataAccessorBasedStore',
|
||||||
|
(): ServerConfig => new AuthenticatedDataAccessorBasedConfig(BASE,
|
||||||
|
new InMemoryDataAccessor(BASE, new MetadataController())),
|
||||||
|
];
|
||||||
|
|
||||||
describe.each([ fileResourceStore, dataAccessorStore ])('A server using a %s', (name, configFn): void => {
|
describe.each([ dataAccessorStore, inMemoryDataAccessorStore ])('A server using a %s', (name, configFn): void => {
|
||||||
describe('with acl', (): void => {
|
describe('with acl', (): void => {
|
||||||
let config: ServerConfig;
|
let config: ServerConfig;
|
||||||
let aclHelper: AclTestHelper;
|
let aclHelper: AclTestHelper;
|
||||||
@ -37,7 +40,13 @@ describe.each([ fileResourceStore, dataAccessorStore ])('A server using a %s', (
|
|||||||
|
|
||||||
// Make sure the root directory exists
|
// Make sure the root directory exists
|
||||||
mkdirSync(rootFilePath, { recursive: true });
|
mkdirSync(rootFilePath, { recursive: true });
|
||||||
copyFileSync(join(__dirname, '../assets/permanent.txt'), `${rootFilePath}/permanent.txt`);
|
|
||||||
|
// Use store instead of file access so tests also work for non-file backends
|
||||||
|
await config.store.setRepresentation({ path: `${BASE}/permanent.txt` }, {
|
||||||
|
binary: true,
|
||||||
|
data: createReadStream(join(__dirname, '../assets/permanent.txt')),
|
||||||
|
metadata: new RepresentationMetadata({ [CONTENT_TYPE]: 'text/plain' }),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async(): Promise<void> => {
|
afterAll(async(): Promise<void> => {
|
@ -6,15 +6,10 @@ import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAc
|
|||||||
import { ExtensionBasedMapper } from '../../src/storage/ExtensionBasedMapper';
|
import { ExtensionBasedMapper } from '../../src/storage/ExtensionBasedMapper';
|
||||||
import { MetadataController } from '../../src/util/MetadataController';
|
import { MetadataController } from '../../src/util/MetadataController';
|
||||||
import { DataAccessorBasedConfig } from '../configs/DataAccessorBasedConfig';
|
import { DataAccessorBasedConfig } from '../configs/DataAccessorBasedConfig';
|
||||||
import { FileResourceStoreConfig } from '../configs/FileResourceStoreConfig';
|
|
||||||
import type { ServerConfig } from '../configs/ServerConfig';
|
import type { ServerConfig } from '../configs/ServerConfig';
|
||||||
import { BASE, getRootFilePath } from '../configs/Util';
|
import { BASE, getRootFilePath } from '../configs/Util';
|
||||||
import { FileTestHelper } from '../util/TestHelpers';
|
import { FileTestHelper } from '../util/TestHelpers';
|
||||||
|
|
||||||
const fileResourceStore: [string, (rootFilePath: string) => ServerConfig] = [
|
|
||||||
'FileResourceStore',
|
|
||||||
(rootFilePath: string): ServerConfig => new FileResourceStoreConfig(BASE, rootFilePath),
|
|
||||||
];
|
|
||||||
const fileDataAccessorStore: [string, (rootFilePath: string) => ServerConfig] = [
|
const fileDataAccessorStore: [string, (rootFilePath: string) => ServerConfig] = [
|
||||||
'FileDataAccessorBasedStore',
|
'FileDataAccessorBasedStore',
|
||||||
(rootFilePath: string): ServerConfig => new DataAccessorBasedConfig(BASE,
|
(rootFilePath: string): ServerConfig => new DataAccessorBasedConfig(BASE,
|
||||||
@ -26,7 +21,7 @@ const inMemoryDataAccessorStore: [string, (rootFilePath: string) => ServerConfig
|
|||||||
new InMemoryDataAccessor(BASE, new MetadataController())),
|
new InMemoryDataAccessor(BASE, new MetadataController())),
|
||||||
];
|
];
|
||||||
|
|
||||||
const configs = [ fileResourceStore, fileDataAccessorStore, inMemoryDataAccessorStore ];
|
const configs = [ fileDataAccessorStore, inMemoryDataAccessorStore ];
|
||||||
|
|
||||||
describe.each(configs)('A server using a %s', (name, configFn): void => {
|
describe.each(configs)('A server using a %s', (name, configFn): void => {
|
||||||
describe('without acl', (): void => {
|
describe('without acl', (): void => {
|
||||||
|
@ -1,584 +0,0 @@
|
|||||||
import type { Stats, WriteStream } from 'fs';
|
|
||||||
import fs, { promises as fsPromises } from 'fs';
|
|
||||||
import { posix } from 'path';
|
|
||||||
import { Readable } from 'stream';
|
|
||||||
import { literal, namedNode, quad as quadRDF } from '@rdfjs/data-model';
|
|
||||||
import arrayifyStream from 'arrayify-stream';
|
|
||||||
import { DataFactory } from 'n3';
|
|
||||||
import streamifyArray from 'streamify-array';
|
|
||||||
import type { Representation } from '../../../src/ldp/representation/Representation';
|
|
||||||
import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
|
|
||||||
import { ExtensionBasedMapper } from '../../../src/storage/ExtensionBasedMapper';
|
|
||||||
import { FileResourceStore } from '../../../src/storage/FileResourceStore';
|
|
||||||
import { INTERNAL_QUADS } from '../../../src/util/ContentTypes';
|
|
||||||
import { ConflictHttpError } from '../../../src/util/errors/ConflictHttpError';
|
|
||||||
import { MethodNotAllowedHttpError } from '../../../src/util/errors/MethodNotAllowedHttpError';
|
|
||||||
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
|
|
||||||
import { UnsupportedMediaTypeHttpError } from '../../../src/util/errors/UnsupportedMediaTypeHttpError';
|
|
||||||
import { InteractionController } from '../../../src/util/InteractionController';
|
|
||||||
import { MetadataController } from '../../../src/util/MetadataController';
|
|
||||||
import { DCTERMS, HTTP, LDP, POSIX, RDF, XSD } from '../../../src/util/UriConstants';
|
|
||||||
|
|
||||||
const { join: joinPath } = posix;
|
|
||||||
|
|
||||||
const base = 'http://test.com/';
|
|
||||||
const rootFilepath = '/Users/default/home/public/';
|
|
||||||
|
|
||||||
jest.mock('fs', (): any => ({
|
|
||||||
createReadStream: jest.fn(),
|
|
||||||
createWriteStream: jest.fn(),
|
|
||||||
promises: {
|
|
||||||
rmdir: jest.fn(),
|
|
||||||
lstat: jest.fn(),
|
|
||||||
readdir: jest.fn(),
|
|
||||||
mkdir: jest.fn(),
|
|
||||||
unlink: jest.fn(),
|
|
||||||
access: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('A FileResourceStore', (): void => {
|
|
||||||
let store: FileResourceStore;
|
|
||||||
let representation: Representation;
|
|
||||||
let readableMock: Readable;
|
|
||||||
let stats: Stats;
|
|
||||||
let writeStream: WriteStream;
|
|
||||||
const rawData = 'lorem ipsum dolor sit amet consectetur adipiscing';
|
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
fs.promises = {
|
|
||||||
rmdir: jest.fn(),
|
|
||||||
lstat: jest.fn(),
|
|
||||||
readdir: jest.fn(),
|
|
||||||
mkdir: jest.fn(),
|
|
||||||
unlink: jest.fn(),
|
|
||||||
access: jest.fn(),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
store = new FileResourceStore(
|
|
||||||
new ExtensionBasedMapper(base, rootFilepath),
|
|
||||||
new InteractionController(),
|
|
||||||
new MetadataController(),
|
|
||||||
);
|
|
||||||
|
|
||||||
representation = {
|
|
||||||
binary: true,
|
|
||||||
data: streamifyArray([ rawData ]),
|
|
||||||
metadata: new RepresentationMetadata(),
|
|
||||||
};
|
|
||||||
|
|
||||||
stats = {
|
|
||||||
isDirectory: jest.fn((): any => false) as any,
|
|
||||||
isFile: jest.fn((): any => false) as any,
|
|
||||||
mtime: new Date(),
|
|
||||||
size: 5,
|
|
||||||
} as jest.Mocked<Stats>;
|
|
||||||
|
|
||||||
writeStream = {
|
|
||||||
on: jest.fn((name: string, func: () => void): any => {
|
|
||||||
if (name === 'finish') {
|
|
||||||
func();
|
|
||||||
}
|
|
||||||
return writeStream;
|
|
||||||
}) as any,
|
|
||||||
once: jest.fn((): any => writeStream) as any,
|
|
||||||
emit: jest.fn((): any => true) as any,
|
|
||||||
write: jest.fn((): any => true) as any,
|
|
||||||
end: jest.fn() as any,
|
|
||||||
} as jest.Mocked<WriteStream>;
|
|
||||||
(fs.createWriteStream as jest.Mock).mockReturnValue(writeStream);
|
|
||||||
|
|
||||||
readableMock = {
|
|
||||||
on: jest.fn((name: string, func: () => void): any => {
|
|
||||||
if (name === 'finish') {
|
|
||||||
func();
|
|
||||||
}
|
|
||||||
return readableMock;
|
|
||||||
}) as any,
|
|
||||||
pipe: jest.fn((): any => readableMock) as any,
|
|
||||||
} as jest.Mocked<Readable>;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors if a resource was not found.', async(): Promise<void> => {
|
|
||||||
(fsPromises.lstat as jest.Mock).mockImplementation((): any => {
|
|
||||||
throw new Error('Path does not exist.');
|
|
||||||
});
|
|
||||||
(fsPromises.readdir as jest.Mock).mockImplementation((): any => []);
|
|
||||||
await expect(store.getRepresentation({ path: 'http://wrong.com/wrong' })).rejects.toThrow(NotFoundHttpError);
|
|
||||||
await expect(store.getRepresentation({ path: `${base}wrong` })).rejects.toThrow(NotFoundHttpError);
|
|
||||||
await expect(store.getRepresentation({ path: `${base}wrong/` })).rejects.toThrow(NotFoundHttpError);
|
|
||||||
await expect(store.addResource({ path: 'http://wrong.com/wrong' }, representation))
|
|
||||||
.rejects.toThrow(NotFoundHttpError);
|
|
||||||
await expect(store.deleteResource({ path: 'wrong' })).rejects.toThrow(NotFoundHttpError);
|
|
||||||
await expect(store.deleteResource({ path: `${base}wrong` })).rejects.toThrow(NotFoundHttpError);
|
|
||||||
await expect(store.deleteResource({ path: `${base}wrong/` })).rejects.toThrow(NotFoundHttpError);
|
|
||||||
await expect(store.setRepresentation({ path: 'http://wrong.com/' }, representation))
|
|
||||||
.rejects.toThrow(NotFoundHttpError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors when modifying resources.', async(): Promise<void> => {
|
|
||||||
await expect(store.modifyResource()).rejects.toThrow(Error);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors for wrong input data types.', async(): Promise<void> => {
|
|
||||||
(representation as any).binary = false;
|
|
||||||
await expect(store.addResource({ path: base }, representation)).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
|
||||||
await expect(store.setRepresentation({ path: `${base}foo` }, representation)).rejects
|
|
||||||
.toThrow(UnsupportedMediaTypeHttpError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can write and read a container.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
// Add
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
|
|
||||||
// Mock: Get
|
|
||||||
stats.isDirectory = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValueOnce([]);
|
|
||||||
(fs.createReadStream as jest.Mock).mockImplementationOnce((): any => new Error('Metadata file does not exist.'));
|
|
||||||
|
|
||||||
// Write container (POST)
|
|
||||||
representation.metadata.add(RDF.type, LDP.BasicContainer);
|
|
||||||
representation.metadata.add(HTTP.slug, 'myContainer/');
|
|
||||||
const identifier = await store.addResource({ path: base }, representation);
|
|
||||||
expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'myContainer/'), { recursive: true });
|
|
||||||
expect(identifier.path).toBe(`${base}myContainer/`);
|
|
||||||
|
|
||||||
// Read container
|
|
||||||
const result = await store.getRepresentation(identifier);
|
|
||||||
expect(result).toEqual({
|
|
||||||
binary: false,
|
|
||||||
data: expect.any(Readable),
|
|
||||||
metadata: expect.any(RepresentationMetadata),
|
|
||||||
});
|
|
||||||
expect(result.metadata.get(DCTERMS.modified)?.value).toEqual(stats.mtime.toISOString());
|
|
||||||
expect(result.metadata.contentType).toEqual(INTERNAL_QUADS);
|
|
||||||
await expect(arrayifyStream(result.data)).resolves.toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors for container creation with path to non container.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValue([ 'foo' ]);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
representation.metadata.add(RDF.type, LDP.BasicContainer);
|
|
||||||
representation.metadata.add(HTTP.slug, 'myContainer/');
|
|
||||||
await expect(store.addResource({ path: `${base}foo` }, representation)).rejects.toThrow(MethodNotAllowedHttpError);
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors 404 for POST invalid path ending without slash and 405 for valid.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValue([]);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
representation.metadata.add(RDF.type, LDP.BasicContainer);
|
|
||||||
representation.metadata.add(HTTP.slug, 'myContainer/');
|
|
||||||
await expect(store.addResource({ path: `${base}doesnotexist` }, representation))
|
|
||||||
.rejects.toThrow(NotFoundHttpError);
|
|
||||||
expect(fsPromises.readdir as jest.Mock).toHaveBeenLastCalledWith(rootFilepath);
|
|
||||||
|
|
||||||
representation.metadata.set(RDF.type, LDP.Resource);
|
|
||||||
representation.metadata.set(HTTP.slug, 'file.txt');
|
|
||||||
await expect(store.addResource({ path: `${base}doesnotexist` }, representation))
|
|
||||||
.rejects.toThrow(NotFoundHttpError);
|
|
||||||
expect(fsPromises.readdir as jest.Mock).toHaveBeenLastCalledWith(rootFilepath);
|
|
||||||
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValue([ 'existingresource' ]);
|
|
||||||
representation.metadata.removeAll(RDF.type);
|
|
||||||
await expect(store.addResource({ path: `${base}existingresource` }, representation))
|
|
||||||
.rejects.toThrow(MethodNotAllowedHttpError);
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toHaveBeenLastCalledWith(joinPath(rootFilepath, 'existingresource'));
|
|
||||||
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockImplementation((): void => {
|
|
||||||
throw new Error('not a directory');
|
|
||||||
});
|
|
||||||
representation.metadata.removeAll(RDF.type);
|
|
||||||
await expect(store.addResource({ path: `${base}existingresource/container/` }, representation))
|
|
||||||
.rejects.toThrow(MethodNotAllowedHttpError);
|
|
||||||
expect(fsPromises.lstat as jest.Mock)
|
|
||||||
.toHaveBeenLastCalledWith(joinPath(rootFilepath, 'existingresource'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can set data.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
// Set
|
|
||||||
(fsPromises.lstat as jest.Mock).mockImplementationOnce((): any => {
|
|
||||||
throw new Error('Path does not exist.');
|
|
||||||
});
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockReturnValue(true);
|
|
||||||
stats.isDirectory = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
|
|
||||||
// Mock: Get
|
|
||||||
stats = { ...stats };
|
|
||||||
stats.isFile = jest.fn((): any => true);
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValueOnce([ 'file.txt' ]);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fs.createReadStream as jest.Mock).mockReturnValueOnce(streamifyArray([ rawData ]));
|
|
||||||
(fs.createReadStream as jest.Mock).mockReturnValueOnce(streamifyArray([]));
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
await store.setRepresentation({ path: `${base}file.txt` }, representation);
|
|
||||||
expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
|
|
||||||
const result = await store.getRepresentation({ path: `${base}file.txt` });
|
|
||||||
expect(result).toEqual({
|
|
||||||
binary: true,
|
|
||||||
data: expect.any(Readable),
|
|
||||||
metadata: expect.any(RepresentationMetadata),
|
|
||||||
});
|
|
||||||
expect(result.metadata.get(DCTERMS.modified)?.value).toEqual(stats.mtime.toISOString());
|
|
||||||
expect(result.metadata.get(POSIX.size)?.value).toEqual(`${stats.size}`);
|
|
||||||
expect(result.metadata.contentType).toEqual('text/plain');
|
|
||||||
await expect(arrayifyStream(result.data)).resolves.toEqual([ rawData ]);
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
|
|
||||||
expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
|
|
||||||
expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt.metadata'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can delete data.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
// Delete
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValueOnce([ 'file.txt' ]);
|
|
||||||
stats.isFile = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fsPromises.unlink as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
(fsPromises.unlink as jest.Mock).mockImplementationOnce((): any => {
|
|
||||||
throw new Error('Metadata file does not exist.');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock: Get
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValueOnce([ ]);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockImplementationOnce((): any => {
|
|
||||||
throw new Error('Path does not exist.');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
await store.deleteResource({ path: `${base}file.txt` });
|
|
||||||
expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
|
|
||||||
await expect(store.getRepresentation({ path: `${base}file.txt` })).rejects.toThrow(NotFoundHttpError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('creates intermediate container when POSTing resource to path ending with slash.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
stats.isDirectory = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
representation.metadata.add(RDF.type, LDP.Resource);
|
|
||||||
representation.metadata.add(HTTP.slug, 'file.txt');
|
|
||||||
const identifier = await store.addResource({ path: `${base}doesnotexistyet/` }, representation);
|
|
||||||
expect(identifier.path).toBe(`${base}doesnotexistyet/file.txt`);
|
|
||||||
expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexistyet/'),
|
|
||||||
{ recursive: true });
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'doesnotexistyet/'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('creates metadata file when metadata triples are passed.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
// Add
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
stats.isDirectory = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
|
|
||||||
// Mock: Set
|
|
||||||
(fsPromises.lstat as jest.Mock).mockImplementationOnce((): any => {
|
|
||||||
throw new Error('Path does not exist.');
|
|
||||||
});
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
stats = { ...stats };
|
|
||||||
stats.isDirectory = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
representation.metadata.add(RDF.type, LDP.Resource);
|
|
||||||
representation.data = readableMock;
|
|
||||||
await store.addResource({ path: `${base}foo/` }, representation);
|
|
||||||
expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'), { recursive: true });
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
|
|
||||||
|
|
||||||
await store.setRepresentation({ path: `${base}foo/file.txt` }, representation);
|
|
||||||
expect(fs.createWriteStream as jest.Mock).toBeCalledTimes(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors when deleting root container.', async(): Promise<void> => {
|
|
||||||
// Tests
|
|
||||||
await expect(store.deleteResource({ path: base })).rejects.toThrow(MethodNotAllowedHttpError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors when deleting non empty container.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
stats.isDirectory = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValueOnce([ '.metadata', 'file.txt' ]);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
await expect(store.deleteResource({ path: `${base}notempty/` })).rejects.toThrow(ConflictHttpError);
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'notempty/'));
|
|
||||||
expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'notempty/'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deletes metadata file when deleting container.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
stats.isDirectory = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValueOnce([ '.metadata' ]);
|
|
||||||
(fsPromises.unlink as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
(fsPromises.rmdir as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
await store.deleteResource({ path: `${base}foo/` });
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
|
|
||||||
expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
|
|
||||||
expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', '.metadata'));
|
|
||||||
expect(fsPromises.rmdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors 404 when accessing non resource (file/directory), e.g. special files.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValue(stats);
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValue([ '14' ]);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
await expect(store.deleteResource({ path: `${base}dev/pts/14` })).rejects.toThrow(NotFoundHttpError);
|
|
||||||
await expect(store.getRepresentation({ path: `${base}dev/pts/14` })).rejects.toThrow(NotFoundHttpError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the quads of the files in a directory when a directory is queried.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
stats.isDirectory = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValueOnce([ 'file.txt', '.nonresource' ]);
|
|
||||||
stats = { ...stats };
|
|
||||||
stats.isFile = jest.fn((): any => true);
|
|
||||||
stats.isDirectory = jest.fn((): any => false);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
stats = { ...stats };
|
|
||||||
stats.isFile = jest.fn((): any => false);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fs.createReadStream as jest.Mock).mockImplementationOnce((): any => new Error('Metadata file does not exist.'));
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
const containerNode = namedNode(`${base}foo/`);
|
|
||||||
const fileNode = namedNode(`${base}foo/file.txt`);
|
|
||||||
const quads = [
|
|
||||||
quadRDF(containerNode, namedNode(RDF.type), namedNode(LDP.Container)),
|
|
||||||
quadRDF(containerNode, namedNode(RDF.type), namedNode(LDP.BasicContainer)),
|
|
||||||
quadRDF(containerNode, namedNode(RDF.type), namedNode(LDP.Resource)),
|
|
||||||
quadRDF(containerNode, namedNode(POSIX.size), DataFactory.literal(stats.size)),
|
|
||||||
quadRDF(containerNode, namedNode(DCTERMS.modified), literal(stats.mtime.toISOString(), namedNode(XSD.dateTime))),
|
|
||||||
quadRDF(containerNode, namedNode(POSIX.mtime), DataFactory.literal(Math.floor(stats.mtime.getTime() / 1000))),
|
|
||||||
quadRDF(containerNode, namedNode(LDP.contains), fileNode),
|
|
||||||
quadRDF(fileNode, namedNode(RDF.type), namedNode(LDP.Resource)),
|
|
||||||
quadRDF(fileNode, namedNode(POSIX.size), DataFactory.literal(stats.size)),
|
|
||||||
quadRDF(fileNode, namedNode(DCTERMS.modified), literal(stats.mtime.toISOString(), namedNode(XSD.dateTime))),
|
|
||||||
quadRDF(fileNode, namedNode(POSIX.mtime), DataFactory.literal(Math.floor(stats.mtime.getTime() / 1000))),
|
|
||||||
];
|
|
||||||
const result = await store.getRepresentation({ path: `${base}foo/` });
|
|
||||||
expect(result).toEqual({
|
|
||||||
binary: false,
|
|
||||||
data: expect.any(Readable),
|
|
||||||
metadata: expect.any(RepresentationMetadata),
|
|
||||||
});
|
|
||||||
expect(result.metadata.get(DCTERMS.modified)?.value).toEqual(stats.mtime.toISOString());
|
|
||||||
expect(result.metadata.contentType).toEqual(INTERNAL_QUADS);
|
|
||||||
await expect(arrayifyStream(result.data)).resolves.toBeRdfIsomorphic(quads);
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
|
|
||||||
expect(fsPromises.readdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', 'file.txt'));
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', '.nonresource'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can overwrite representation and its metadata with PUT.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
stats.isFile = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
stats = { ...stats };
|
|
||||||
stats.isDirectory = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
representation.metadata.add(RDF.type, LDP.Resource);
|
|
||||||
await store.setRepresentation({ path: `${base}alreadyexists.txt` }, representation);
|
|
||||||
expect(fs.createWriteStream as jest.Mock).toBeCalledTimes(2);
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'alreadyexists.txt'));
|
|
||||||
expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(rootFilepath, { recursive: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors when overwriting container with PUT.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fsPromises.access as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
await expect(store.setRepresentation({ path: `${base}alreadyexists` }, representation)).rejects
|
|
||||||
.toThrow(ConflictHttpError);
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'alreadyexists'));
|
|
||||||
representation.metadata.add(RDF.type, LDP.BasicContainer);
|
|
||||||
await expect(store.setRepresentation({ path: `${base}alreadyexists/` }, representation)).rejects
|
|
||||||
.toThrow(ConflictHttpError);
|
|
||||||
expect(fsPromises.access as jest.Mock).toBeCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can create a container with PUT.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
(fsPromises.access as jest.Mock).mockImplementationOnce((): any => {
|
|
||||||
throw new Error('Path does not exist.');
|
|
||||||
});
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
representation.metadata.add(RDF.type, LDP.BasicContainer);
|
|
||||||
await store.setRepresentation({ path: `${base}foo/` }, representation);
|
|
||||||
expect(fsPromises.mkdir as jest.Mock).toBeCalledTimes(1);
|
|
||||||
expect(fsPromises.access as jest.Mock).toBeCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('undoes metadata file creation when resource creation fails.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
stats.isDirectory = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fs.createWriteStream as jest.Mock).mockReturnValueOnce(writeStream);
|
|
||||||
(fs.createWriteStream as jest.Mock).mockImplementationOnce((): any => {
|
|
||||||
throw new Error('Failed to create new file.');
|
|
||||||
});
|
|
||||||
(fsPromises.unlink as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
representation.metadata.add(RDF.type, LDP.Resource);
|
|
||||||
representation.metadata.add(HTTP.slug, 'file.txt');
|
|
||||||
await expect(store.addResource({ path: base }, representation)).rejects.toThrow(Error);
|
|
||||||
expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt.metadata'));
|
|
||||||
expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
|
|
||||||
expect(fsPromises.unlink as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt.metadata'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('undoes container creation when metadata file creation fails.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
(fs.createWriteStream as jest.Mock).mockImplementationOnce((): any => {
|
|
||||||
throw new Error('Failed to create new file.');
|
|
||||||
});
|
|
||||||
(fsPromises.rmdir as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
representation.metadata.add(RDF.type, LDP.BasicContainer);
|
|
||||||
representation.metadata.add(HTTP.slug, 'foo/');
|
|
||||||
await expect(store.addResource({ path: base }, representation)).rejects.toThrow(Error);
|
|
||||||
expect(fsPromises.rmdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo/'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('creates container when POSTing without linkRel and with slug ending with slash.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
// Add
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
representation.metadata.add(HTTP.slug, 'myContainer/');
|
|
||||||
const identifier = await store.addResource({ path: base }, representation);
|
|
||||||
expect(identifier.path).toBe(`${base}myContainer/`);
|
|
||||||
expect(fsPromises.mkdir as jest.Mock).toBeCalledTimes(1);
|
|
||||||
expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'myContainer/'), { recursive: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns default contentType when unknown for representation.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
stats.isFile = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fs.createReadStream as jest.Mock).mockReturnValueOnce(streamifyArray([ rawData ]));
|
|
||||||
(fs.createReadStream as jest.Mock).mockImplementationOnce((): any => new Error('Metadata file does not exist.'));
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValueOnce([ '.htaccess' ]);
|
|
||||||
|
|
||||||
const result = await store.getRepresentation({ path: `${base}.htaccess` });
|
|
||||||
expect(result).toEqual({
|
|
||||||
binary: true,
|
|
||||||
data: expect.any(Readable),
|
|
||||||
metadata: expect.any(RepresentationMetadata),
|
|
||||||
});
|
|
||||||
expect(result.metadata.contentType).toEqual('application/octet-stream');
|
|
||||||
expect(result.metadata.get(DCTERMS.modified)?.value).toEqual(stats.mtime.toISOString());
|
|
||||||
expect(result.metadata.get(POSIX.size)?.value).toEqual(`${stats.size}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors when performing a PUT on the root path.', async(): Promise<void> => {
|
|
||||||
await expect(store.setRepresentation({ path: base }, representation)).rejects.toThrow(ConflictHttpError);
|
|
||||||
await expect(store.setRepresentation({ path: base.slice(0, -1) }, representation)).rejects
|
|
||||||
.toThrow(ConflictHttpError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('creates resource when PUT to resource path without linkRel header.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
(fsPromises.lstat as jest.Mock).mockImplementationOnce((): any => {
|
|
||||||
throw new Error('Path does not exist.');
|
|
||||||
});
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockReturnValue(true);
|
|
||||||
stats.isDirectory = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
await store.setRepresentation({ path: `${base}file.txt` }, representation);
|
|
||||||
expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(rootFilepath, { recursive: true });
|
|
||||||
expect(fs.createWriteStream as jest.Mock).toBeCalledTimes(1);
|
|
||||||
expect(fs.createWriteStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'file.txt'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('creates container when POST to existing container path ending without slash and slug without slash.',
|
|
||||||
async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
stats.isDirectory = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockReturnValue(true);
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValueOnce([ 'foo' ]);
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
representation.metadata.add(RDF.type, LDP.BasicContainer);
|
|
||||||
representation.metadata.add(HTTP.slug, 'bar');
|
|
||||||
const identifier = await store.addResource({ path: `${base}foo` }, representation);
|
|
||||||
expect(identifier.path).toBe(`${base}foo/bar/`);
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo'));
|
|
||||||
expect(fsPromises.mkdir as jest.Mock).toBeCalledWith(joinPath(rootFilepath, 'foo', 'bar/'), { recursive: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('generates a new URI when adding without a slug.', async(): Promise<void> => {
|
|
||||||
// Mock the fs functions.
|
|
||||||
// Post
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fsPromises.mkdir as jest.Mock).mockReturnValue(true);
|
|
||||||
stats.isDirectory = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
|
|
||||||
// Mock: Get
|
|
||||||
stats.isFile = jest.fn((): any => true);
|
|
||||||
(fsPromises.lstat as jest.Mock).mockReturnValueOnce(stats);
|
|
||||||
(fs.createReadStream as jest.Mock).mockReturnValueOnce(streamifyArray([ rawData ]));
|
|
||||||
(fs.createReadStream as jest.Mock).mockImplementationOnce((): any => new Error('Metadata file does not exist.'));
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
await store.addResource({ path: base }, representation);
|
|
||||||
const filePath: string = (fs.createWriteStream as jest.Mock).mock.calls[0][0];
|
|
||||||
expect(filePath.startsWith(rootFilepath)).toBeTruthy();
|
|
||||||
const name = filePath.slice(rootFilepath.length);
|
|
||||||
|
|
||||||
(fsPromises.readdir as jest.Mock).mockReturnValueOnce([ name ]);
|
|
||||||
const result = await store.getRepresentation({ path: `${base}${name}` });
|
|
||||||
expect(result).toEqual({
|
|
||||||
binary: true,
|
|
||||||
data: expect.any(Readable),
|
|
||||||
metadata: expect.any(RepresentationMetadata),
|
|
||||||
});
|
|
||||||
expect(result.metadata.get(DCTERMS.modified)?.value).toEqual(stats.mtime.toISOString());
|
|
||||||
expect(result.metadata.get(POSIX.size)?.value).toEqual(`${stats.size}`);
|
|
||||||
await expect(arrayifyStream(result.data)).resolves.toEqual([ rawData ]);
|
|
||||||
expect(fsPromises.lstat as jest.Mock).toBeCalledWith(joinPath(rootFilepath, name));
|
|
||||||
expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, name));
|
|
||||||
expect(fs.createReadStream as jest.Mock).toBeCalledWith(joinPath(rootFilepath, `${name}.metadata`));
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,73 +0,0 @@
|
|||||||
import { Readable } from 'stream';
|
|
||||||
import streamifyArray from 'streamify-array';
|
|
||||||
import type { Representation } from '../../../src/ldp/representation/Representation';
|
|
||||||
import type { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata';
|
|
||||||
import { InMemoryResourceStore } from '../../../src/storage/InMemoryResourceStore';
|
|
||||||
import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
|
|
||||||
import { readableToString } from '../../../src/util/Util';
|
|
||||||
|
|
||||||
const base = 'http://test.com/';
|
|
||||||
|
|
||||||
describe('A InMemoryResourceStore', (): void => {
|
|
||||||
let store: InMemoryResourceStore;
|
|
||||||
let representation: Representation;
|
|
||||||
const dataString = '<http://test.com/s> <http://test.com/p> <http://test.com/o>.';
|
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
|
||||||
store = new InMemoryResourceStore(base);
|
|
||||||
|
|
||||||
representation = {
|
|
||||||
binary: true,
|
|
||||||
data: streamifyArray([ dataString ]),
|
|
||||||
metadata: {} as RepresentationMetadata,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors if a resource was not found.', async(): Promise<void> => {
|
|
||||||
await expect(store.getRepresentation({ path: `${base}wrong` })).rejects.toThrow(NotFoundHttpError);
|
|
||||||
await expect(store.addResource({ path: 'http://wrong.com/wrong' }, representation))
|
|
||||||
.rejects.toThrow(NotFoundHttpError);
|
|
||||||
await expect(store.deleteResource({ path: 'wrong' })).rejects.toThrow(NotFoundHttpError);
|
|
||||||
await expect(store.setRepresentation({ path: 'http://wrong.com/' }, representation))
|
|
||||||
.rejects.toThrow(NotFoundHttpError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('errors when modifying resources.', async(): Promise<void> => {
|
|
||||||
await expect(store.modifyResource()).rejects.toThrow(Error);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can write and read data.', async(): Promise<void> => {
|
|
||||||
const identifier = await store.addResource({ path: base }, representation);
|
|
||||||
expect(identifier.path.startsWith(base)).toBeTruthy();
|
|
||||||
const result = await store.getRepresentation(identifier);
|
|
||||||
expect(result).toEqual({
|
|
||||||
binary: true,
|
|
||||||
data: expect.any(Readable),
|
|
||||||
metadata: representation.metadata,
|
|
||||||
});
|
|
||||||
await expect(readableToString(result.data)).resolves.toEqual(dataString);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can add resources to previously added resources.', async(): Promise<void> => {
|
|
||||||
const identifier = await store.addResource({ path: base }, representation);
|
|
||||||
representation.data = streamifyArray([ ]);
|
|
||||||
const childIdentifier = await store.addResource(identifier, representation);
|
|
||||||
expect(childIdentifier.path).toContain(identifier.path);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can set data.', async(): Promise<void> => {
|
|
||||||
await store.setRepresentation({ path: base }, representation);
|
|
||||||
const result = await store.getRepresentation({ path: base });
|
|
||||||
expect(result).toEqual({
|
|
||||||
binary: true,
|
|
||||||
data: expect.any(Readable),
|
|
||||||
metadata: representation.metadata,
|
|
||||||
});
|
|
||||||
await expect(readableToString(result.data)).resolves.toEqual(dataString);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can delete data.', async(): Promise<void> => {
|
|
||||||
await store.deleteResource({ path: base });
|
|
||||||
await expect(store.getRepresentation({ path: base })).rejects.toThrow(NotFoundHttpError);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
x
Reference in New Issue
Block a user