mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add getChildren function to DataAccessor interface
DataAccessors are now no longer responsible for generating ldp:contains triples.
This commit is contained in:
@@ -63,7 +63,7 @@ export class RepresentationMetadata {
|
||||
public constructor(contentType?: string);
|
||||
|
||||
/**
|
||||
* @param overrides - Metadata values (defaulting to content type if a string)
|
||||
* @param metadata - Metadata values (defaulting to content type if a string)
|
||||
*/
|
||||
public constructor(metadata?: RepresentationMetadata | MetadataRecord | string);
|
||||
|
||||
|
||||
@@ -94,9 +94,13 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
await this.auxiliaryStrategy.addMetadata(metadata);
|
||||
|
||||
if (isContainerPath(metadata.identifier.value)) {
|
||||
// Remove containment references of auxiliary resources
|
||||
const auxContains = this.getContainedAuxiliaryResources(metadata);
|
||||
metadata.remove(LDP.terms.contains, auxContains);
|
||||
// Add containment triples of non-auxiliary resources
|
||||
for await (const child of this.accessor.getChildren(identifier)) {
|
||||
if (!this.auxiliaryStrategy.isAuxiliaryIdentifier({ path: child.identifier.value })) {
|
||||
metadata.addQuads(child.quads());
|
||||
metadata.add(LDP.terms.contains, child.identifier as NamedNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a container representation from the metadata
|
||||
const data = metadata.quads();
|
||||
@@ -139,7 +143,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
// using the HTTP Slug header as defined in [RFC5023].
|
||||
// Clients who want the server to assign a URI of a resource, MUST use the POST request."
|
||||
// https://solid.github.io/specification/protocol#resource-type-heuristics
|
||||
const newID = this.createSafeUri(container, representation.metadata, parentMetadata);
|
||||
const newID = await this.createSafeUri(container, representation.metadata);
|
||||
|
||||
// Write the data. New containers should never be made for a POST request.
|
||||
await this.writeData(newID, representation, isContainerIdentifier(newID), false);
|
||||
@@ -205,8 +209,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
// https://solid.github.io/specification/protocol#deleting-resources
|
||||
if (isContainerIdentifier(identifier)) {
|
||||
// Auxiliary resources are not counted when deleting a container since they will also be deleted
|
||||
const auxContains = this.getContainedAuxiliaryResources(metadata);
|
||||
if (metadata.getAll(LDP.contains).length > auxContains.length) {
|
||||
if (await this.hasProperChildren(identifier)) {
|
||||
throw new ConflictHttpError('Can only delete empty containers.');
|
||||
}
|
||||
}
|
||||
@@ -399,10 +402,9 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
*
|
||||
* @param container - Identifier of the target container.
|
||||
* @param metadata - Metadata of the new resource.
|
||||
* @param parentMetadata - Metadata of the parent container.
|
||||
*/
|
||||
protected createSafeUri(container: ResourceIdentifier, metadata: RepresentationMetadata,
|
||||
parentMetadata: RepresentationMetadata): ResourceIdentifier {
|
||||
protected async createSafeUri(container: ResourceIdentifier, metadata: RepresentationMetadata):
|
||||
Promise<ResourceIdentifier> {
|
||||
// Get all values needed for naming the resource
|
||||
const isContainer = this.isNewContainer(metadata);
|
||||
const slug = metadata.get(HTTP.slug)?.value;
|
||||
@@ -418,11 +420,9 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
}
|
||||
|
||||
// Make sure we don't already have a resource with this exact name (or with differing trailing slash)
|
||||
const withSlash = ensureTrailingSlash(newID.path);
|
||||
const withoutSlash = trimTrailingSlashes(newID.path);
|
||||
const exists = parentMetadata.getAll(LDP.contains).some((term): boolean =>
|
||||
term.value === withSlash || term.value === withoutSlash);
|
||||
if (exists) {
|
||||
const withSlash = { path: ensureTrailingSlash(newID.path) };
|
||||
const withoutSlash = { path: trimTrailingSlashes(newID.path) };
|
||||
if (await this.resourceExists(withSlash) || await this.resourceExists(withoutSlash)) {
|
||||
newID = this.createURI(container, isContainer);
|
||||
}
|
||||
|
||||
@@ -458,11 +458,15 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the identifiers of all auxiliary resources contained within the given metadata.
|
||||
* Checks if the given container has any non-auxiliary resources.
|
||||
*/
|
||||
protected getContainedAuxiliaryResources(metadata: RepresentationMetadata): NamedNode[] {
|
||||
return metadata.getAll(LDP.terms.contains).filter((object): boolean =>
|
||||
this.auxiliaryStrategy.isAuxiliaryIdentifier({ path: object.value })) as NamedNode[];
|
||||
protected async hasProperChildren(container: ResourceIdentifier): Promise<boolean> {
|
||||
for await (const child of this.accessor.getChildren(container)) {
|
||||
if (!this.auxiliaryStrategy.isAuxiliaryIdentifier({ path: child.identifier.value })) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { Guarded } from '../../util/GuardedStream';
|
||||
* * If the input identifier ends with a slash, it should be assumed the identifier is targeting a container.
|
||||
* * Similarly, if there is no trailing slash it should assume a document.
|
||||
* * It should always throw a NotFoundHttpError if it does not have data matching the input identifier.
|
||||
* * DataAccessors are responsible for generating the relevant containment triples for containers.
|
||||
* * DataAccessors should not generate containment triples. This will be done externally using `getChildren`.
|
||||
*/
|
||||
export interface DataAccessor {
|
||||
/**
|
||||
@@ -36,6 +36,20 @@ export interface DataAccessor {
|
||||
*/
|
||||
getMetadata: (identifier: ResourceIdentifier) => Promise<RepresentationMetadata>;
|
||||
|
||||
/**
|
||||
* Returns metadata for all resources in the requested container.
|
||||
* This should not be all metadata of those resources (but it can be),
|
||||
* but instead the main metadata you want to show in situations
|
||||
* where all these resources are presented simultaneously.
|
||||
* Generally this would be metadata that is present for all of these resources,
|
||||
* such as resource type or last modified date.
|
||||
*
|
||||
* It can be safely assumed that the incoming identifier will always correspond to a container.
|
||||
*
|
||||
* @param identifier - Identifier of the parent container.
|
||||
*/
|
||||
getChildren: (identifier: ResourceIdentifier) => AsyncIterableIterator<RepresentationMetadata>;
|
||||
|
||||
/**
|
||||
* Writes data and metadata for a document.
|
||||
* If any data and/or metadata exist for the given identifier, it should be overwritten.
|
||||
|
||||
@@ -14,7 +14,7 @@ import { guardStream } from '../../util/GuardedStream';
|
||||
import type { Guarded } from '../../util/GuardedStream';
|
||||
import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil';
|
||||
import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil';
|
||||
import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
|
||||
import { generateResourceQuads } from '../../util/ResourceUtil';
|
||||
import { toLiteral } from '../../util/TermUtil';
|
||||
import { CONTENT_TYPE, DC, LDP, POSIX, RDF, XSD } from '../../util/Vocabularies';
|
||||
import type { FileIdentifierMapper, ResourceLink } from '../mapping/FileIdentifierMapper';
|
||||
@@ -70,6 +70,11 @@ export class FileDataAccessor implements DataAccessor {
|
||||
throw new NotFoundHttpError();
|
||||
}
|
||||
|
||||
public async* getChildren(identifier: ResourceIdentifier): AsyncIterableIterator<RepresentationMetadata> {
|
||||
const link = await this.resourceMapper.mapUrlToFilePath(identifier);
|
||||
yield* this.getChildMetadata(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given data as a file (and potential metadata as additional file).
|
||||
* The metadata file will be written first and will be deleted if something goes wrong writing the actual data.
|
||||
@@ -194,8 +199,7 @@ export class FileDataAccessor implements DataAccessor {
|
||||
*/
|
||||
private async getDirectoryMetadata(link: ResourceLink, stats: Stats):
|
||||
Promise<RepresentationMetadata> {
|
||||
return (await this.getBaseMetadata(link, stats, true))
|
||||
.addQuads(await this.getChildMetadataQuads(link));
|
||||
return await this.getBaseMetadata(link, stats, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,46 +281,38 @@ export class FileDataAccessor implements DataAccessor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all containment related triples for a container.
|
||||
* These include the actual containment triples and specific triples for every child resource.
|
||||
* Generate metadata for all children in a container.
|
||||
*
|
||||
* @param link - Path related metadata.
|
||||
*/
|
||||
private async getChildMetadataQuads(link: ResourceLink): Promise<Quad[]> {
|
||||
const quads: Quad[] = [];
|
||||
const childURIs: string[] = [];
|
||||
const files = await fsPromises.readdir(link.filePath);
|
||||
private async* getChildMetadata(link: ResourceLink): AsyncIterableIterator<RepresentationMetadata> {
|
||||
const dir = await fsPromises.opendir(link.filePath);
|
||||
|
||||
// For every child in the container we want to generate specific metadata
|
||||
for (const childName of files) {
|
||||
// Hide metadata files from containment triples
|
||||
for await (const entry of dir) {
|
||||
const childName = entry.name;
|
||||
// Hide metadata files
|
||||
if (this.isMetadataPath(childName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore non-file/directory entries in the folder
|
||||
const childStats = await fsPromises.lstat(joinFilePath(link.filePath, childName));
|
||||
if (!childStats.isFile() && !childStats.isDirectory()) {
|
||||
if (!entry.isFile() && !entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate the URI corresponding to the child resource
|
||||
const childLink = await this.resourceMapper
|
||||
.mapFilePathToUrl(joinFilePath(link.filePath, childName), childStats.isDirectory());
|
||||
.mapFilePathToUrl(joinFilePath(link.filePath, childName), entry.isDirectory());
|
||||
|
||||
// Generate metadata of this specific child
|
||||
const subject = DataFactory.namedNode(childLink.identifier.path);
|
||||
const childStats = await fsPromises.lstat(joinFilePath(link.filePath, childName));
|
||||
const quads: Quad[] = [];
|
||||
quads.push(...generateResourceQuads(subject, childStats.isDirectory()));
|
||||
quads.push(...this.generatePosixQuads(subject, childStats));
|
||||
childURIs.push(childLink.identifier.path);
|
||||
yield new RepresentationMetadata(subject).addQuads(quads);
|
||||
}
|
||||
|
||||
// Generate containment metadata
|
||||
const containsQuads = generateContainmentQuads(
|
||||
DataFactory.namedNode(link.identifier.path), childURIs,
|
||||
);
|
||||
|
||||
return quads.concat(containsQuads);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import type { Readable } from 'stream';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import type { NamedNode } from 'rdf-js';
|
||||
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
|
||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||
import type { Guarded } from '../../util/GuardedStream';
|
||||
import type { IdentifierStrategy } from '../../util/identifiers/IdentifierStrategy';
|
||||
import { generateContainmentQuads } from '../../util/ResourceUtil';
|
||||
import { guardedStreamFrom } from '../../util/StreamUtil';
|
||||
import type { DataAccessor } from './DataAccessor';
|
||||
|
||||
@@ -46,7 +44,15 @@ export class InMemoryDataAccessor implements DataAccessor {
|
||||
|
||||
public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> {
|
||||
const entry = this.getEntry(identifier);
|
||||
return this.generateMetadata(entry);
|
||||
return new RepresentationMetadata(entry.metadata);
|
||||
}
|
||||
|
||||
public async* getChildren(identifier: ResourceIdentifier): AsyncIterableIterator<RepresentationMetadata> {
|
||||
const entry = this.getEntry(identifier);
|
||||
if (!this.isDataEntry(entry)) {
|
||||
const childNames = Object.keys(entry.entries);
|
||||
yield* childNames.map((name): RepresentationMetadata => new RepresentationMetadata({ path: name }));
|
||||
}
|
||||
}
|
||||
|
||||
public async writeDocument(identifier: ResourceIdentifier, data: Guarded<Readable>, metadata: RepresentationMetadata):
|
||||
@@ -141,14 +147,4 @@ export class InMemoryDataAccessor implements DataAccessor {
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private generateMetadata(entry: CacheEntry): RepresentationMetadata {
|
||||
const metadata = new RepresentationMetadata(entry.metadata);
|
||||
if (!this.isDataEntry(entry)) {
|
||||
const childNames = Object.keys(entry.entries);
|
||||
const quads = generateContainmentQuads(metadata.identifier as NamedNode, childNames);
|
||||
metadata.addQuads(quads);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,15 @@ import { SparqlEndpointFetcher } from 'fetch-sparql-endpoint';
|
||||
import { DataFactory } from 'n3';
|
||||
import type { NamedNode, Quad } from 'rdf-js';
|
||||
import type {
|
||||
ConstructQuery, GraphPattern,
|
||||
ConstructQuery,
|
||||
GraphPattern,
|
||||
GraphQuads,
|
||||
InsertDeleteOperation,
|
||||
SparqlGenerator,
|
||||
Update,
|
||||
UpdateOperation,
|
||||
} from 'sparqljs';
|
||||
import {
|
||||
Generator,
|
||||
} from 'sparqljs';
|
||||
import { Generator } from 'sparqljs';
|
||||
import type { Representation } from '../../ldp/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
|
||||
@@ -83,9 +82,7 @@ export class SparqlDataAccessor implements DataAccessor {
|
||||
*/
|
||||
public async getMetadata(identifier: ResourceIdentifier): Promise<RepresentationMetadata> {
|
||||
const name = namedNode(identifier.path);
|
||||
const query = isContainerIdentifier(identifier) ?
|
||||
this.sparqlConstructContainer(name) :
|
||||
this.sparqlConstruct(this.getMetadataNode(name));
|
||||
const query = this.sparqlConstruct(this.getMetadataNode(name));
|
||||
const stream = await this.sendSparqlConstruct(query);
|
||||
const quads = await arrayifyStream(stream);
|
||||
|
||||
@@ -101,6 +98,15 @@ export class SparqlDataAccessor implements DataAccessor {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public async* getChildren(identifier: ResourceIdentifier): AsyncIterableIterator<RepresentationMetadata> {
|
||||
// Only triples that have a container identifier as subject are the containment triples
|
||||
const name = namedNode(identifier.path);
|
||||
const stream = await this.sendSparqlConstruct(this.sparqlConstruct(name));
|
||||
for await (const entry of stream) {
|
||||
yield new RepresentationMetadata((entry as Quad).object as NamedNode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given metadata for the container.
|
||||
*/
|
||||
@@ -186,23 +192,6 @@ export class SparqlDataAccessor implements DataAccessor {
|
||||
};
|
||||
}
|
||||
|
||||
private sparqlConstructContainer(name: NamedNode): ConstructQuery {
|
||||
const pattern = quad(variable('s'), variable('p'), variable('o'));
|
||||
return {
|
||||
queryType: 'CONSTRUCT',
|
||||
template: [ pattern ],
|
||||
where: [{
|
||||
type: 'union',
|
||||
patterns: [
|
||||
this.sparqlSelectGraph(name, [ pattern ]),
|
||||
this.sparqlSelectGraph(this.getMetadataNode(name), [ pattern ]),
|
||||
],
|
||||
}],
|
||||
type: 'query',
|
||||
prefixes: {},
|
||||
};
|
||||
}
|
||||
|
||||
private sparqlSelectGraph(name: NamedNode, triples: Quad[]): GraphPattern {
|
||||
return {
|
||||
type: 'graph',
|
||||
@@ -215,8 +204,8 @@ export class SparqlDataAccessor implements DataAccessor {
|
||||
* Creates an update query that overwrites the data and metadata of a resource.
|
||||
* If there are no triples we assume it's a container (so don't overwrite the main graph with containment triples).
|
||||
* @param name - Name of the resource to update.
|
||||
* @param parent - Name of the parent to update the containment triples.
|
||||
* @param metadata - New metadata of the resource.
|
||||
* @param parent - Name of the parent to update the containment triples.
|
||||
* @param triples - New data of the resource.
|
||||
*/
|
||||
private sparqlInsert(name: NamedNode, metadata: RepresentationMetadata, parent?: NamedNode, triples?: Quad[]):
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { DataFactory } from 'n3';
|
||||
import type { NamedNode, Quad } from 'rdf-js';
|
||||
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
|
||||
import type { Representation } from '../ldp/representation/Representation';
|
||||
@@ -27,18 +26,6 @@ export function generateResourceQuads(subject: NamedNode, isContainer: boolean):
|
||||
return quads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to generate the quads describing that the resource URIs are children of the container URI.
|
||||
* @param containerURI - The URI of the container.
|
||||
* @param childURIs - The URI of the child resources.
|
||||
*
|
||||
* @returns The generated quads.
|
||||
*/
|
||||
export function generateContainmentQuads(containerURI: NamedNode, childURIs: string[]): Quad[] {
|
||||
return new RepresentationMetadata(containerURI,
|
||||
{ [LDP.contains]: childURIs.map(DataFactory.namedNode) }).quads();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to clone a representation, the original representation can still be used.
|
||||
* This function loads the entire stream in memory.
|
||||
|
||||
Reference in New Issue
Block a user