From 12e501844fe925747b5740bd0987b1716deafee9 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 29 Jun 2021 10:45:41 +0200 Subject: [PATCH] fix: Prevent generated metadata from being stored --- src/ldp/auxiliary/LinkMetadataGenerator.ts | 5 ++- src/storage/DataAccessorBasedStore.ts | 37 +++++++++++++++---- src/storage/accessors/FileDataAccessor.ts | 12 ++++-- src/util/Vocabularies.ts | 5 +++ .../auxiliary/LinkMetadataGenerator.test.ts | 2 + .../RepresentationMetadata.test.ts | 4 +- .../storage/DataAccessorBasedStore.test.ts | 24 +++++++++--- .../accessors/FileDataAccessor.test.ts | 7 +++- 8 files changed, 75 insertions(+), 21 deletions(-) diff --git a/src/ldp/auxiliary/LinkMetadataGenerator.ts b/src/ldp/auxiliary/LinkMetadataGenerator.ts index 5ec4eba26..bb7563b27 100644 --- a/src/ldp/auxiliary/LinkMetadataGenerator.ts +++ b/src/ldp/auxiliary/LinkMetadataGenerator.ts @@ -1,4 +1,5 @@ import { namedNode } from '@rdfjs/data-model'; +import { SOLID_META } from '../../util/Vocabularies'; import type { RepresentationMetadata } from '../representation/RepresentationMetadata'; import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy'; import { MetadataGenerator } from './MetadataGenerator'; @@ -22,7 +23,9 @@ export class LinkMetadataGenerator extends MetadataGenerator { public async handle(metadata: RepresentationMetadata): Promise { const identifier = { path: metadata.identifier.value }; if (!this.identifierStrategy.isAuxiliaryIdentifier(identifier)) { - metadata.add(this.link, namedNode(this.identifierStrategy.getAuxiliaryIdentifier(identifier).path)); + metadata.add(this.link, + namedNode(this.identifierStrategy.getAuxiliaryIdentifier(identifier).path), + SOLID_META.terms.ResponseMetadata); } } } diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts index 0625c9e42..0be878886 100644 --- a/src/storage/DataAccessorBasedStore.ts +++ b/src/storage/DataAccessorBasedStore.ts @@ -26,7 +26,18 @@ import { } from '../util/PathUtil'; import { parseQuads } from '../util/QuadUtil'; import { addResourceMetadata } from '../util/ResourceUtil'; -import { CONTENT_TYPE, DC, SOLID_HTTP, LDP, POSIX, PIM, RDF, VANN, XSD } from '../util/Vocabularies'; +import { + CONTENT_TYPE, + DC, + SOLID_HTTP, + LDP, + POSIX, + PIM, + RDF, + XSD, + SOLID_META, + PREFERRED_PREFIX_TERM, +} from '../util/Vocabularies'; import type { DataAccessor } from './accessors/DataAccessor'; import type { ResourceStore } from './ResourceStore'; @@ -98,16 +109,23 @@ export class DataAccessorBasedStore implements ResourceStore { 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); + metadata.add(LDP.terms.contains, child.identifier as NamedNode, SOLID_META.terms.ResponseMetadata); } } // Generate a container representation from the metadata - const data = metadata.quads(); - metadata.addQuad(DC.terms.namespace, VANN.terms.preferredNamespacePrefix, 'dc'); - metadata.addQuad(LDP.terms.namespace, VANN.terms.preferredNamespacePrefix, 'ldp'); - metadata.addQuad(POSIX.terms.namespace, VANN.terms.preferredNamespacePrefix, 'posix'); - metadata.addQuad(XSD.terms.namespace, VANN.terms.preferredNamespacePrefix, 'xsd'); + // All triples should be in the same graph for the data representation + const data = metadata.quads().map((triple): Quad => { + if (triple.graph.termType === 'DefaultGraph') { + return triple; + } + return DataFactory.quad(triple.subject, triple.predicate, triple.object); + }); + + metadata.addQuad(DC.terms.namespace, PREFERRED_PREFIX_TERM, 'dc', SOLID_META.terms.ResponseMetadata); + metadata.addQuad(LDP.terms.namespace, PREFERRED_PREFIX_TERM, 'ldp', SOLID_META.terms.ResponseMetadata); + metadata.addQuad(POSIX.terms.namespace, PREFERRED_PREFIX_TERM, 'posix', SOLID_META.terms.ResponseMetadata); + metadata.addQuad(XSD.terms.namespace, PREFERRED_PREFIX_TERM, 'xsd', SOLID_META.terms.ResponseMetadata); representation = new BasicRepresentation(data, metadata, INTERNAL_QUADS); } else { // Retrieve a document representation from the accessor @@ -317,6 +335,11 @@ export class DataAccessorBasedStore implements ResourceStore { } } + // Remove all generated metadata to prevent it from being stored permanently + representation.metadata.removeQuads( + representation.metadata.quads(null, null, null, SOLID_META.terms.ResponseMetadata), + ); + await (isContainer ? this.accessor.writeContainer(identifier, representation.metadata) : this.accessor.writeDocument(identifier, representation.data, representation.metadata)); diff --git a/src/storage/accessors/FileDataAccessor.ts b/src/storage/accessors/FileDataAccessor.ts index 98cc513a2..a82208ce2 100644 --- a/src/storage/accessors/FileDataAccessor.ts +++ b/src/storage/accessors/FileDataAccessor.ts @@ -15,7 +15,7 @@ import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil'; import { parseQuads, serializeQuads } from '../../util/QuadUtil'; import { addResourceMetadata } from '../../util/ResourceUtil'; import { toLiteral } from '../../util/TermUtil'; -import { CONTENT_TYPE, DC, LDP, POSIX, RDF, XSD } from '../../util/Vocabularies'; +import { CONTENT_TYPE, DC, LDP, POSIX, RDF, SOLID_META, XSD } from '../../util/Vocabularies'; import type { FileIdentifierMapper, ResourceLink } from '../mapping/FileIdentifierMapper'; import type { DataAccessor } from './DataAccessor'; @@ -319,10 +319,14 @@ export class FileDataAccessor implements DataAccessor { * @param stats - Stats of the file/directory corresponding to the resource. */ private addPosixMetadata(metadata: RepresentationMetadata, stats: Stats): void { - metadata.add(DC.terms.modified, toLiteral(stats.mtime.toISOString(), XSD.terms.dateTime)); - metadata.add(POSIX.terms.mtime, toLiteral(Math.floor(stats.mtime.getTime() / 1000), XSD.terms.integer)); + metadata.add(DC.terms.modified, + toLiteral(stats.mtime.toISOString(), XSD.terms.dateTime), + SOLID_META.terms.ResponseMetadata); + metadata.add(POSIX.terms.mtime, + toLiteral(Math.floor(stats.mtime.getTime() / 1000), XSD.terms.integer), + SOLID_META.terms.ResponseMetadata); if (!stats.isDirectory()) { - metadata.add(POSIX.terms.size, toLiteral(stats.size, XSD.terms.integer)); + metadata.add(POSIX.terms.size, toLiteral(stats.size, XSD.terms.integer), SOLID_META.terms.ResponseMetadata); } } diff --git a/src/util/Vocabularies.ts b/src/util/Vocabularies.ts index 07efce64b..c37cd0207 100644 --- a/src/util/Vocabularies.ts +++ b/src/util/Vocabularies.ts @@ -128,6 +128,11 @@ export const SOLID_HTTP = createUriAndTermNamespace('urn:npm:solid:community-ser 'slug', ); +export const SOLID_META = createUriAndTermNamespace('urn:npm:solid:community-server:meta:', + // This identifier is used as graph for all metadata that is generated on the fly and should not be stored + 'ResponseMetadata', +); + export const VANN = createUriAndTermNamespace('http://purl.org/vocab/vann/', 'preferredNamespacePrefix', ); diff --git a/test/unit/ldp/auxiliary/LinkMetadataGenerator.test.ts b/test/unit/ldp/auxiliary/LinkMetadataGenerator.test.ts index e4b115be5..a2d13fd39 100644 --- a/test/unit/ldp/auxiliary/LinkMetadataGenerator.test.ts +++ b/test/unit/ldp/auxiliary/LinkMetadataGenerator.test.ts @@ -2,6 +2,7 @@ import type { AuxiliaryIdentifierStrategy } from '../../../../src/ldp/auxiliary/ import { LinkMetadataGenerator } from '../../../../src/ldp/auxiliary/LinkMetadataGenerator'; import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier'; +import { SOLID_META } from '../../../../src/util/Vocabularies'; describe('A LinkMetadataGenerator', (): void => { const link = 'link'; @@ -35,5 +36,6 @@ describe('A LinkMetadataGenerator', (): void => { await expect(generator.handle(metadata)).resolves.toBeUndefined(); expect(metadata.quads()).toHaveLength(1); expect(metadata.get(link)?.value).toBe(auxiliaryId.path); + expect(metadata.getAll(link, SOLID_META.terms.ResponseMetadata)).toHaveLength(1); }); }); diff --git a/test/unit/ldp/representation/RepresentationMetadata.test.ts b/test/unit/ldp/representation/RepresentationMetadata.test.ts index 8e7256dd6..15221dc4a 100644 --- a/test/unit/ldp/representation/RepresentationMetadata.test.ts +++ b/test/unit/ldp/representation/RepresentationMetadata.test.ts @@ -150,13 +150,13 @@ describe('A RepresentationMetadata', (): void => { it('can add a quad.', async(): Promise => { const newQuad = quad(namedNode('random'), namedNode('new'), literal('triple')); metadata.addQuad('random', 'new', 'triple'); - expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.concat([ newQuad ])); + expect(metadata.quads()).toBeRdfIsomorphic([ ...inputQuads, newQuad ]); }); it('can add a quad with a graph.', async(): Promise => { const newQuad = quad(namedNode('random'), namedNode('new'), literal('triple'), namedNode('graph')); metadata.addQuad('random', 'new', 'triple', 'graph'); - expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.concat([ newQuad ])); + expect(metadata.quads()).toBeRdfIsomorphic([ ...inputQuads, newQuad ]); }); it('can add quads.', async(): Promise => { diff --git a/test/unit/storage/DataAccessorBasedStore.test.ts b/test/unit/storage/DataAccessorBasedStore.test.ts index 9b8f2d3b8..e9c909134 100644 --- a/test/unit/storage/DataAccessorBasedStore.test.ts +++ b/test/unit/storage/DataAccessorBasedStore.test.ts @@ -21,9 +21,8 @@ import type { Guarded } from '../../../src/util/GuardedStream'; import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy'; import { trimTrailingSlashes } from '../../../src/util/PathUtil'; import { guardedStreamFrom } from '../../../src/util/StreamUtil'; -import { CONTENT_TYPE, SOLID_HTTP, LDP, PIM, RDF } from '../../../src/util/Vocabularies'; -import quad = DataFactory.quad; -import namedNode = DataFactory.namedNode; +import { CONTENT_TYPE, SOLID_HTTP, LDP, PIM, RDF, SOLID_META } from '../../../src/util/Vocabularies'; +const { namedNode, quad } = DataFactory; class SimpleDataAccessor implements DataAccessor { public readonly data: Record = {}; @@ -379,8 +378,8 @@ describe('A DataAccessorBasedStore', (): void => { it('can write resources.', async(): Promise => { const resourceID = { path: `${root}resource` }; await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([ - { path: 'http://test.com/' }, - { path: 'http://test.com/resource' }, + { path: root }, + { path: `${root}resource` }, ]); await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]); }); @@ -393,13 +392,26 @@ describe('A DataAccessorBasedStore', (): void => { representation.metadata.contentType = 'text/turtle'; representation.data = guardedStreamFrom([ `<${`${root}resource/`}> a .` ]); await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([ - { path: `${root}` }, + { path: root }, { path: `${root}container/` }, ]); expect(accessor.data[resourceID.path]).toBeTruthy(); expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined(); }); + it('does not write generated metadata.', async(): Promise => { + const resourceID = { path: `${root}resource` }; + representation.metadata.add('notGen', 'value'); + representation.metadata.add('gen', 'value', SOLID_META.terms.ResponseMetadata); + await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([ + { path: root }, + { path: `${root}resource` }, + ]); + await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]); + expect(accessor.data[resourceID.path].metadata.get('notGen')?.value).toBe('value'); + expect(accessor.data[resourceID.path].metadata.get('gen')).toBeUndefined(); + }); + it('can write resources even if root does not exist.', async(): Promise => { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete accessor.data[root]; diff --git a/test/unit/storage/accessors/FileDataAccessor.test.ts b/test/unit/storage/accessors/FileDataAccessor.test.ts index bc5858058..da3396683 100644 --- a/test/unit/storage/accessors/FileDataAccessor.test.ts +++ b/test/unit/storage/accessors/FileDataAccessor.test.ts @@ -11,9 +11,10 @@ import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError import type { SystemError } from '../../../../src/util/errors/SystemError'; import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError'; import type { Guarded } from '../../../../src/util/GuardedStream'; +import { isContainerPath } from '../../../../src/util/PathUtil'; import { guardedStreamFrom, readableToString } from '../../../../src/util/StreamUtil'; import { toLiteral } from '../../../../src/util/TermUtil'; -import { CONTENT_TYPE, DC, LDP, POSIX, RDF, XSD } from '../../../../src/util/Vocabularies'; +import { CONTENT_TYPE, DC, LDP, POSIX, RDF, SOLID_META, XSD } from '../../../../src/util/Vocabularies'; import { mockFs } from '../../../util/Util'; jest.mock('fs'); @@ -102,6 +103,7 @@ describe('A FileDataAccessor', (): void => { expect(metadata.get(POSIX.size)).toEqualRdfTerm(toLiteral('data'.length, XSD.terms.integer)); expect(metadata.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime)); expect(metadata.get(POSIX.mtime)).toEqualRdfTerm(toLiteral(Math.floor(now.getTime() / 1000), XSD.terms.integer)); + expect(metadata.quads(null, null, null, SOLID_META.terms.ResponseMetadata)).toHaveLength(3); }); it('does not generate size metadata for a container.', async(): Promise => { @@ -121,6 +123,7 @@ describe('A FileDataAccessor', (): void => { expect(metadata.get(POSIX.size)).toBeUndefined(); expect(metadata.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime)); expect(metadata.get(POSIX.mtime)).toEqualRdfTerm(toLiteral(Math.floor(now.getTime() / 1000), XSD.terms.integer)); + expect(metadata.quads(null, null, null, SOLID_META.terms.ResponseMetadata)).toHaveLength(2); }); it('generates metadata for container child resources.', async(): Promise => { @@ -136,6 +139,8 @@ describe('A FileDataAccessor', (): void => { expect(child.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime)); expect(child.get(POSIX.mtime)).toEqualRdfTerm(toLiteral(Math.floor(now.getTime() / 1000), XSD.terms.integer)); + expect(child.quads(null, null, null, SOLID_META.terms.ResponseMetadata)) + .toHaveLength(isContainerPath(child.identifier.value) ? 2 : 3); } });