Skip to content

Commit

Permalink
feat: Create InMemoryDataAccessor
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Oct 9, 2020
1 parent 447e9c1 commit 359035d
Show file tree
Hide file tree
Showing 9 changed files with 397 additions and 39 deletions.
2 changes: 1 addition & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export * from './src/server/HttpResponse';
// Storage/Accessors
export * from './src/storage/accessors/DataAccessor';
export * from './src/storage/accessors/FileDataAccessor';
export * from './src/storage/accessors/NormalizedDataAccessor';
export * from './src/storage/accessors/InMemoryDataAccessor';

// Storage/Conversion
export * from './src/storage/conversion/ChainedConverter';
Expand Down
177 changes: 177 additions & 0 deletions src/storage/accessors/InMemoryDataAccessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { Readable } from 'stream';
import arrayifyStream from 'arrayify-stream';
import { DataFactory } from 'n3';
import type { NamedNode } from 'rdf-js';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import type { MetadataController } from '../../util/MetadataController';
import { ensureTrailingSlash } from '../../util/Util';
import type { DataAccessor } from './DataAccessor';

interface DataEntry {
data: any[];
metadata?: RepresentationMetadata;
}
interface ContainerEntry {
entries: { [name: string]: CacheEntry };
metadata?: RepresentationMetadata;
}
type CacheEntry = DataEntry | ContainerEntry;

class ArrayReadable extends Readable {
private readonly data: any[];
private idx: number;

public constructor(data: any[]) {
super({ objectMode: true });
this.data = data;
this.idx = 0;
}

// eslint-disable-next-line @typescript-eslint/naming-convention
public _read(): void {
if (this.idx < this.data.length) {
this.push(this.data[this.idx]);
this.idx += 1;
} else {
this.push(null);
}
}
}

export class InMemoryDataAccessor implements DataAccessor {
private readonly base: string;
private readonly store: ContainerEntry;
private readonly metadataController: MetadataController;

public constructor(base: string, metadataController: MetadataController) {
this.base = ensureTrailingSlash(base);
this.metadataController = metadataController;

const metadata = new RepresentationMetadata(this.base);
metadata.addQuads(this.metadataController.generateResourceQuads(DataFactory.namedNode(this.base), true));
this.store = { entries: {}, metadata };
}

public async canHandle(): Promise<void> {
// All data is supported since streams never get read, only copied
}

public async getData(identifier: ResourceIdentifier): Promise<Readable> {
const entry = this.getEntry(identifier);
if (!this.isDataEntry(entry)) {
throw new NotFoundHttpError();
}
return new ArrayReadable(entry.data);
}

public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> {
const entry = this.getEntry(identifier);
if (this.isDataEntry(entry) === identifier.path.endsWith('/')) {
throw new NotFoundHttpError();
}
return this.generateMetadata(identifier, entry);
}

public async getNormalizedMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> {
const entry = this.getEntry(identifier);
return this.generateMetadata(identifier, entry);
}

public async writeDataResource(identifier: ResourceIdentifier, data: Readable, metadata?: RepresentationMetadata):
Promise<void> {
const { parent, name } = this.getParentEntry(identifier);
parent.entries[name] = {
// Drain original stream and create copy
data: await arrayifyStream(data),
metadata,
};
}

public async writeContainer(identifier: ResourceIdentifier, metadata?: RepresentationMetadata): Promise<void> {
try {
// Overwrite existing metadata but keep children if container already exists
const entry = this.getEntry(identifier);
entry.metadata = metadata;
} catch (error: unknown) {
// Create new entry if it didn't exist yet
if (error instanceof NotFoundHttpError) {
const { parent, name } = this.getParentEntry(identifier);
parent.entries[name] = {
entries: {},
metadata,
};
} else {
throw error;
}
}
}

public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
const { parent, name } = this.getParentEntry(identifier);
if (!parent.entries[name]) {
throw new NotFoundHttpError();
}
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete parent.entries[name];
}

private isDataEntry(entry: CacheEntry): entry is DataEntry {
return Boolean((entry as DataEntry).data);
}

private getParentEntry(identifier: ResourceIdentifier): { parent: ContainerEntry; name: string } {
const parts = identifier.path.slice(this.base.length).split('/').filter((part): boolean => part.length > 0);

if (parts.length === 0) {
throw new Error('Root container has no parent.');
}

// Name of the resource will be the last entry in the path
const name = parts[parts.length - 1];

// All names preceding the last should be nested containers
const containers = parts.slice(0, -1);

// Step through the parts of the path up to the end
let parent = this.store;
for (const container of containers) {
const child = parent.entries[container];
if (!child) {
throw new NotFoundHttpError();
} else if (this.isDataEntry(child)) {
throw new Error('Invalid path.');
}
parent = child;
}

return { parent, name };
}

private getEntry(identifier: ResourceIdentifier): CacheEntry {
if (identifier.path === this.base) {
return this.store;
}
const { parent, name } = this.getParentEntry(identifier);
const entry = parent.entries[name];
if (!entry) {
throw new NotFoundHttpError();
}
return entry;
}

private generateMetadata(identifier: ResourceIdentifier, entry: CacheEntry): RepresentationMetadata {
const metadata = entry.metadata ?
new RepresentationMetadata(entry.metadata) :
new RepresentationMetadata(identifier.path);
if (!this.isDataEntry(entry)) {
const childNames = Object.keys(entry.entries).map((name): string =>
`${identifier.path}${name}${this.isDataEntry(entry.entries[name]) ? '' : '/'}`);
const quads = this.metadataController
.generateContainerContainsResourceQuads(metadata.identifier as NamedNode, childNames);
metadata.addQuads(quads);
}
return metadata;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { HttpHandler,
ResourceStore } from '../../index';
import type {
DataAccessor,
HttpHandler,
ResourceStore,
} from '../../index';
import {
AuthenticatedLdpHandler,
BasicResponseWriter,
Expand All @@ -15,7 +18,7 @@ import {
getBasicRequestParser,
getOperationHandler,
getWebAclAuthorizer,
getFileDataAccessorStore,
getDataAccessorStore,
} from './Util';

/**
Expand All @@ -24,14 +27,14 @@ import {
* - a FileResourceStore wrapped in a converting store (rdf to quad & quad to rdf)
* - GET, POST, PUT & DELETE operation handlers
*/
export class AuthenticatedFileBasedDataAccessorConfig implements ServerConfig {
export class AuthenticatedDataAccessorBasedConfig implements ServerConfig {
public base: string;
public store: ResourceStore;

public constructor(base: string, rootFilepath: string) {
public constructor(base: string, dataAccessor: DataAccessor) {
this.base = base;
this.store = getConvertingStore(
getFileDataAccessorStore(base, rootFilepath),
getDataAccessorStore(base, dataAccessor),
[ new QuadToRdfConverter(),
new RdfToQuadConverter() ],
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { HttpHandler,
ResourceStore } from '../../index';
import type {
DataAccessor,
HttpHandler,
ResourceStore,
} from '../../index';
import {
AllowEverythingAuthorizer,
AuthenticatedLdpHandler,
Expand All @@ -16,21 +19,21 @@ import {
getOperationHandler,
getConvertingStore,
getBasicRequestParser,
getFileDataAccessorStore,
getDataAccessorStore,
} from './Util';

/**
* FileBasedDataAccessorConfig works with
* DataAccessorBasedConfig works with
* - an AllowEverythingAuthorizer (no acl)
* - a DataAccessorBasedStore with a FileDataAccessor wrapped in a converting store (rdf to quad & quad to rdf)
* - GET, POST, PUT & DELETE operation handlers
*/
export class FileBasedDataAccessorConfig implements ServerConfig {
export class DataAccessorBasedConfig implements ServerConfig {
public store: ResourceStore;

public constructor(base: string, rootFilepath: string) {
public constructor(base: string, dataAccessor: DataAccessor) {
this.store = getConvertingStore(
getFileDataAccessorStore(base, rootFilepath),
getDataAccessorStore(base, dataAccessor),
[ new QuadToRdfConverter(), new RdfToQuadConverter() ],
);
}
Expand Down
11 changes: 6 additions & 5 deletions test/configs/Util.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { join } from 'path';
import type { BodyParser,
import type {
BodyParser, DataAccessor,
HttpRequest,
Operation,
Representation,
RepresentationConverter,
ResourceStore,
ResponseDescription } from '../../index';
ResponseDescription,
} from '../../index';
import {
AcceptPreferenceParser,
BasicRequestParser,
Expand All @@ -14,7 +16,6 @@ import {
DataAccessorBasedStore,
DeleteOperationHandler,
ExtensionBasedMapper,
FileDataAccessor,
FileResourceStore,
GetOperationHandler,
InMemoryResourceStore,
Expand Down Expand Up @@ -62,9 +63,9 @@ export const getFileResourceStore = (base: string, rootFilepath: string): FileRe
*
* @returns The data accessor based store.
*/
export const getFileDataAccessorStore = (base: string, rootFilepath: string): DataAccessorBasedStore =>
export const getDataAccessorStore = (base: string, dataAccessor: DataAccessor): DataAccessorBasedStore =>
new DataAccessorBasedStore(
new FileDataAccessor(new ExtensionBasedMapper(base, rootFilepath), new MetadataController()),
dataAccessor,
base,
new MetadataController(),
new UrlContainerManager(base),
Expand Down
18 changes: 11 additions & 7 deletions test/integration/AuthenticatedFileBasedStore.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { copyFileSync, mkdirSync } from 'fs';
import { join } from 'path';
import * as rimraf from 'rimraf';
import { FileDataAccessor } from '../../src/storage/accessors/FileDataAccessor';
import { ExtensionBasedMapper } from '../../src/storage/ExtensionBasedMapper';
import { MetadataController } from '../../src/util/MetadataController';
import { ensureTrailingSlash } from '../../src/util/Util';
import { AuthenticatedFileBasedDataAccessorConfig } from '../configs/AuthenticatedFileBasedDataAccessorConfig';
import { AuthenticatedDataAccessorBasedConfig } from '../configs/AuthenticatedDataAccessorBasedConfig';
import { AuthenticatedFileResourceStoreConfig } from '../configs/AuthenticatedFileResourceStoreConfig';
import type { ServerConfig } from '../configs/ServerConfig';
import { BASE, getRootFilePath } from '../configs/Util';
import { AclTestHelper, FileTestHelper } from '../util/TestHelpers';

const fileResourceStore: [string, (rootFilePath: string) => ServerConfig] = [
'FileResourceStore',
'AuthenticatedFileResourceStore',
(rootFilePath: string): ServerConfig => new AuthenticatedFileResourceStoreConfig(BASE, rootFilePath),
];
const dataAccessorStore: [string, (rootFilePath: string) => ServerConfig] = [
'FileDataAccessorBasedStore',
(rootFilePath: string): ServerConfig => new AuthenticatedFileBasedDataAccessorConfig(BASE, rootFilePath),
'AuthenticatedFileDataAccessorBasedStore',
(rootFilePath: string): ServerConfig => new AuthenticatedDataAccessorBasedConfig(BASE,
new FileDataAccessor(new ExtensionBasedMapper(BASE, rootFilePath), new MetadataController())),
];

describe.each([ fileResourceStore, dataAccessorStore ])('A server using a %s', (name, configFn): void => {
Expand Down Expand Up @@ -47,7 +51,7 @@ describe.each([ fileResourceStore, dataAccessorStore ])('A server using a %s', (
await aclHelper.setSimpleAcl({ read: true, write: true, append: true }, 'agent');

// Create file
let response = await fileHelper.createFile('../assets/testfile2.txt', 'testfile2.txt');
let response = await fileHelper.createFile('../assets/testfile2.txt', 'testfile2.txt', 'text/plain');
const id = response._getHeaders().location;

// Get file
Expand All @@ -67,7 +71,7 @@ describe.each([ fileResourceStore, dataAccessorStore ])('A server using a %s', (
await aclHelper.setSimpleAcl({ read: true, write: true, append: true }, 'authenticated');

// Try to create file
const response = await fileHelper.createFile('../assets/testfile2.txt', 'testfile2.txt', true);
const response = await fileHelper.createFile('../assets/testfile2.txt', 'testfile2.txt', 'text/plain', true);
expect(response.statusCode).toBe(401);
});

Expand All @@ -77,7 +81,7 @@ describe.each([ fileResourceStore, dataAccessorStore ])('A server using a %s', (
await aclHelper.setSimpleAcl({ read: true, write: false, append: false }, 'agent');

// Try to create file
let response = await fileHelper.createFile('../assets/testfile2.txt', 'testfile2.txt', true);
let response = await fileHelper.createFile('../assets/testfile2.txt', 'testfile2.txt', 'text/plain', true);
expect(response.statusCode).toBe(401);

// GET permanent file
Expand Down
Loading

0 comments on commit 359035d

Please sign in to comment.