diff --git a/src/storage/FileResourceStore.ts b/src/storage/FileResourceStore.ts index cba2a9ab9..76135de80 100644 --- a/src/storage/FileResourceStore.ts +++ b/src/storage/FileResourceStore.ts @@ -3,7 +3,7 @@ import { createReadStream, createWriteStream, promises as fsPromises } from 'fs' import { posix } from 'path'; import type { Readable } from 'stream'; import { DataFactory } from 'n3'; -import type { Quad } from 'rdf-js'; +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'; @@ -16,8 +16,8 @@ import { UnsupportedMediaTypeHttpError } from '../util/errors/UnsupportedMediaTy import type { InteractionController } from '../util/InteractionController'; import type { MetadataController } from '../util/MetadataController'; import { CONTENT_TYPE, DCTERMS, HTTP, POSIX, RDF, XSD } from '../util/UriConstants'; -import { toTypedLiteral } from '../util/UriUtil'; -import { ensureTrailingSlash } from '../util/Util'; +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'; @@ -244,11 +244,11 @@ export class FileResourceStore implements ResourceStore { */ private async getDirectoryRepresentation(resourceLink: ResourceLink, stats: Stats): Promise { const files = await fsPromises.readdir(resourceLink.filePath); - const quads: Quad[] = []; - const containerURI = resourceLink.identifier.path; + const containerURI = DataFactory.namedNode(resourceLink.identifier.path); - quads.push(...this.metadataController.generateResourceQuads(containerURI, stats)); + 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[] = []; @@ -259,9 +259,11 @@ export class FileResourceStore implements ResourceStore { // Metadata file doesn't exist so lets keep `rawMetaData` an empty array. } - const metadata = new RepresentationMetadata(containerURI).addQuads(rawMetadata) - .set(DCTERMS.modified, toTypedLiteral(stats.mtime.toISOString(), XSD.dateTime)) - .set(CONTENT_TYPE, INTERNAL_QUADS); + const metadata = new RepresentationMetadata(containerURI, { + [DCTERMS.modified]: toTypedLiteral(stats.mtime.toISOString(), XSD.dateTime), + [CONTENT_TYPE]: INTERNAL_QUADS, + }); + metadata.addQuads(rawMetadata); return { binary: false, @@ -278,7 +280,8 @@ export class FileResourceStore implements ResourceStore { * * @returns A promise containing all quads. */ - private async getDirChildrenQuadRepresentation(files: string[], path: string, containerURI: string): Promise { + private async getDirChildrenQuadRepresentation(files: string[], path: string, containerURI: NamedNode): + Promise { const quads: Quad[] = []; const childURIs: string[] = []; for (const childName of files) { @@ -290,7 +293,9 @@ export class FileResourceStore implements ResourceStore { const childLink = await this.resourceMapper .mapFilePathToUrl(joinPath(path, childName), childStats.isDirectory()); - quads.push(...this.metadataController.generateResourceQuads(childLink.identifier.path, childStats)); + 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. @@ -302,6 +307,21 @@ export class FileResourceStore implements ResourceStore { 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. diff --git a/src/util/MetadataController.ts b/src/util/MetadataController.ts index 4ce229006..44e8fad53 100644 --- a/src/util/MetadataController.ts +++ b/src/util/MetadataController.ts @@ -1,35 +1,31 @@ -import type { Stats } from 'fs'; import type { Readable } from 'stream'; import arrayifyStream from 'arrayify-stream'; import { DataFactory, StreamParser, StreamWriter } from 'n3'; -import type { Quad } from 'rdf-js'; +import type { NamedNode, Quad } from 'rdf-js'; import streamifyArray from 'streamify-array'; import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata'; import { TEXT_TURTLE } from './ContentTypes'; -import { DCTERMS, LDP, POSIX, RDF, XSD } from './UriConstants'; -import { toNamedNode, toTypedLiteral } from './UriUtil'; -import { pipeStreamsAndErrors } from './Util'; +import { LDP, RDF } from './UriConstants'; +import { toNamedNode } from './UriUtil'; +import { pipeStreamsAndErrors, pushQuad } from './Util'; export class MetadataController { /** - * Helper function to generate quads for a Container or Resource. - * @param uri - The URI for which the quads should be generated. - * @param stats - The Stats of the subject. + * Helper function to generate type quads for a Container or Resource. + * @param subject - Subject for the new quads. + * @param isContainer - If the identifier corresponds to a container. * * @returns The generated quads. */ - public generateResourceQuads(uri: string, stats: Stats): Quad[] { - const metadata = new RepresentationMetadata(uri); - if (stats.isDirectory()) { - metadata.add(RDF.type, toNamedNode(LDP.Container)); - metadata.add(RDF.type, toNamedNode(LDP.BasicContainer)); + public generateResourceQuads(subject: NamedNode, isContainer: boolean): Quad[] { + const quads: Quad[] = []; + if (isContainer) { + pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Container)); + pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.BasicContainer)); } - metadata.add(RDF.type, toNamedNode(LDP.Resource)); - metadata.add(POSIX.size, toTypedLiteral(stats.size, XSD.integer)); - metadata.add(DCTERMS.modified, toTypedLiteral(stats.mtime.toISOString(), XSD.dateTime)); - metadata.add(POSIX.mtime, toTypedLiteral(Math.floor(stats.mtime.getTime() / 1000), XSD.integer)); + pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Resource)); - return metadata.quads(); + return quads; } /** @@ -39,7 +35,7 @@ export class MetadataController { * * @returns The generated quads. */ - public generateContainerContainsResourceQuads(containerURI: string, childURIs: string[]): Quad[] { + public generateContainerContainsResourceQuads(containerURI: NamedNode, childURIs: string[]): Quad[] { return new RepresentationMetadata(containerURI, { [LDP.contains]: childURIs.map(DataFactory.namedNode) }).quads(); } diff --git a/src/util/Util.ts b/src/util/Util.ts index d28a26214..b8949777a 100644 --- a/src/util/Util.ts +++ b/src/util/Util.ts @@ -1,5 +1,7 @@ import type { Readable, Writable } from 'stream'; import arrayifyStream from 'arrayify-stream'; +import { DataFactory } from 'n3'; +import type { Literal, NamedNode, Quad } from 'rdf-js'; import { UnsupportedHttpError } from './errors/UnsupportedHttpError'; /** @@ -84,3 +86,10 @@ export const decodeUriPathComponents = (path: string): string => path.split('/') * Encodes all (non-slash) special characters in a URI path. */ export const encodeUriPathComponents = (path: string): string => path.split('/').map(encodeURIComponent).join('/'); + +/** + * Generates a quad with the given subject/predicate/object and pushes it to the given array. + */ +export const pushQuad = + (quads: Quad[], subject: NamedNode, predicate: NamedNode, object: NamedNode | Literal): number => + quads.push(DataFactory.quad(subject, predicate, object));