feat: Create separate storage to generate keys

This commit is contained in:
Joachim Van Herwegen
2022-03-17 10:07:51 +01:00
parent 027e3707fd
commit a1a6ce01fa
8 changed files with 185 additions and 131 deletions

View File

@@ -304,6 +304,7 @@ export * from './storage/conversion/RepresentationConverter';
export * from './storage/conversion/TypedRepresentationConverter';
// Storage/KeyValue
export * from './storage/keyvalue/EncodingPathStorage';
export * from './storage/keyvalue/ExpiringStorage';
export * from './storage/keyvalue/JsonFileStorage';
export * from './storage/keyvalue/JsonResourceStorage';

View File

@@ -0,0 +1,64 @@
import { ensureTrailingSlash, joinUrl } from '../../util/PathUtil';
import type { KeyValueStorage } from './KeyValueStorage';
/**
* Transforms the keys into relative paths, to be used by the source storage.
* Encodes the input key with base64 encoding,
* to make sure there are no invalid or special path characters,
* and prepends it with the stored relative path.
* This can be useful to eventually generate URLs in specific containers
* without having to worry about cleaning the input keys.
*/
export class EncodingPathStorage<T> implements KeyValueStorage<string, T> {
private readonly basePath: string;
private readonly source: KeyValueStorage<string, T>;
public constructor(relativePath: string, source: KeyValueStorage<string, T>) {
this.source = source;
this.basePath = ensureTrailingSlash(relativePath);
}
public async get(key: string): Promise<T | undefined> {
const path = this.keyToPath(key);
return this.source.get(path);
}
public async has(key: string): Promise<boolean> {
const path = this.keyToPath(key);
return this.source.has(path);
}
public async set(key: string, value: T): Promise<this> {
const path = this.keyToPath(key);
await this.source.set(path, value);
return this;
}
public async delete(key: string): Promise<boolean> {
const path = this.keyToPath(key);
return this.source.delete(path);
}
public async* entries(): AsyncIterableIterator<[string, T]> {
for await (const [ path, value ] of this.source.entries()) {
const key = this.pathToKey(path);
yield [ key, value ];
}
}
/**
* Converts a key into a path for internal storage.
*/
private keyToPath(key: string): string {
const encodedKey = Buffer.from(key).toString('base64');
return joinUrl(this.basePath, encodedKey);
}
/**
* Converts an internal storage path string into the original path key.
*/
private pathToKey(path: string): string {
const buffer = Buffer.from(path.slice(this.basePath.length), 'base64');
return buffer.toString('utf-8');
}
}

View File

@@ -1,34 +1,32 @@
import { URL } from 'url';
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
import type { Representation } from '../../http/representation/Representation';
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import { ensureTrailingSlash } from '../../util/PathUtil';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { ensureTrailingSlash, joinUrl } from '../../util/PathUtil';
import { readableToString } from '../../util/StreamUtil';
import { LDP } from '../../util/Vocabularies';
import type { ResourceStore } from '../ResourceStore';
import type { KeyValueStorage } from './KeyValueStorage';
/**
* A {@link KeyValueStorage} for JSON-like objects using a {@link ResourceStore} as backend.
*
* The keys will be transformed so they can be safely used
* as a resource name in the given container.
* Values will be sent as data streams,
* so how these are stored depends on the underlying store.
* Creates a base URL by joining the input base URL with the container string.
*
* Assumes the input keys can be safely used to generate identifiers,
* which will be appended to the stored base URL.
*
* All non-404 errors will be re-thrown.
*/
export class JsonResourceStorage implements KeyValueStorage<string, unknown> {
export class JsonResourceStorage<T> implements KeyValueStorage<string, T> {
private readonly source: ResourceStore;
private readonly container: string;
public constructor(source: ResourceStore, baseUrl: string, container: string) {
this.source = source;
this.container = ensureTrailingSlash(new URL(container, baseUrl).href);
this.container = ensureTrailingSlash(joinUrl(baseUrl, container));
}
public async get(key: string): Promise<unknown | undefined> {
public async get(key: string): Promise<T | undefined> {
try {
const identifier = this.createIdentifier(key);
const representation = await this.source.getRepresentation(identifier, { type: { 'application/json': 1 }});
@@ -65,42 +63,15 @@ export class JsonResourceStorage implements KeyValueStorage<string, unknown> {
}
}
public async* entries(): AsyncIterableIterator<[string, unknown]> {
// Getting ldp:contains metadata from container to find entries
let container: Representation;
try {
container = await this.source.getRepresentation({ path: this.container }, {});
} catch (error: unknown) {
// Container might not exist yet, will be created the first time `set` gets called
if (!NotFoundHttpError.isInstance(error)) {
throw error;
}
return;
}
// Only need the metadata
container.data.destroy();
const members = container.metadata.getAll(LDP.terms.contains).map((term): string => term.value);
for (const member of members) {
const representation = await this.source.getRepresentation({ path: member }, { type: { 'application/json': 1 }});
const json = JSON.parse(await readableToString(representation.data));
yield [ this.parseMember(member), json ];
}
public entries(): never {
// There is no way of knowing which resources were added, or we should keep track in an index file
throw new NotImplementedHttpError();
}
/**
* Converts a key into an identifier for internal storage.
*/
private createIdentifier(key: string): ResourceIdentifier {
const buffer = Buffer.from(key);
return { path: `${this.container}${buffer.toString('base64')}` };
}
/**
* Converts an internal storage identifier string into the original identifier key.
*/
private parseMember(member: string): string {
const buffer = Buffer.from(member.slice(this.container.length), 'base64');
return buffer.toString('utf-8');
return { path: joinUrl(this.container, key) };
}
}