diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts index 08c77dde2..9c23156e8 100644 --- a/src/storage/DataAccessorBasedStore.ts +++ b/src/storage/DataAccessorBasedStore.ts @@ -12,7 +12,13 @@ import { MethodNotAllowedHttpError } from '../util/errors/MethodNotAllowedHttpEr import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { NotImplementedError } from '../util/errors/NotImplementedError'; import { UnsupportedHttpError } from '../util/errors/UnsupportedHttpError'; -import { ensureTrailingSlash, getParentContainer, trimTrailingSlashes } from '../util/PathUtil'; +import { + ensureTrailingSlash, + getParentContainer, + isContainerIdentifier, + isContainerPath, + trimTrailingSlashes, +} from '../util/PathUtil'; import { parseQuads } from '../util/QuadUtil'; import { generateResourceQuads } from '../util/ResourceUtil'; import { CONTENT_TYPE, HTTP, LDP, RDF } from '../util/UriConstants'; @@ -93,7 +99,7 @@ export class DataAccessorBasedStore implements ResourceStore { // When a POST method request targets a non-container resource without an existing representation, // the server MUST respond with the 404 status code. - if (!parentMetadata && !container.path.endsWith('/')) { + if (!parentMetadata && !isContainerIdentifier(container)) { throw new NotFoundHttpError(); } @@ -104,7 +110,7 @@ export class DataAccessorBasedStore implements ResourceStore { const newID = this.createSafeUri(container, representation.metadata, parentMetadata); // Write the data. New containers will need to be created if there is no parent. - await this.writeData(newID, representation, newID.path.endsWith('/'), !parentMetadata); + await this.writeData(newID, representation, isContainerIdentifier(newID), !parentMetadata); return newID; } @@ -128,7 +134,7 @@ export class DataAccessorBasedStore implements ResourceStore { if (oldMetadata && isContainer !== this.isExistingContainer(oldMetadata)) { throw new ConflictHttpError('Input resource type does not match existing resource type.'); } - if (isContainer !== identifier.path.endsWith('/')) { + if (isContainer !== isContainerIdentifier(identifier)) { throw new UnsupportedHttpError('Containers should have a `/` at the end of their path, resources should not.'); } @@ -172,7 +178,7 @@ export class DataAccessorBasedStore implements ResourceStore { * @param identifier - Identifier that needs to be checked. */ protected async getNormalizedMetadata(identifier: ResourceIdentifier): Promise { - const hasSlash = identifier.path.endsWith('/'); + const hasSlash = isContainerIdentifier(identifier); try { return await this.accessor.getMetadata(identifier); } catch (error: unknown) { @@ -312,7 +318,7 @@ export class DataAccessorBasedStore implements ResourceStore { isContainer = this.isExistingContainer(metadata); } catch { const slug = suffix ?? metadata.get(HTTP.slug)?.value; - isContainer = Boolean(slug?.endsWith('/')); + isContainer = Boolean(slug && isContainerPath(slug)); } return isContainer; } diff --git a/src/storage/ExtensionBasedMapper.ts b/src/storage/ExtensionBasedMapper.ts index dcafebfd2..0ab52786f 100644 --- a/src/storage/ExtensionBasedMapper.ts +++ b/src/storage/ExtensionBasedMapper.ts @@ -10,6 +10,7 @@ import { decodeUriPathComponents, encodeUriPathComponents, ensureTrailingSlash, + isContainerIdentifier, trimTrailingSlashes, } from '../util/PathUtil'; import type { FileIdentifierMapper, ResourceLink } from './FileIdentifierMapper'; @@ -74,7 +75,7 @@ export class ExtensionBasedMapper implements FileIdentifierMapper { let filePath = this.getAbsolutePath(path); // Container - if (identifier.path.endsWith('/')) { + if (isContainerIdentifier(identifier)) { this.logger.debug(`URL ${identifier.path} points to the container ${filePath}`); return { identifier, diff --git a/src/storage/accessors/FileDataAccessor.ts b/src/storage/accessors/FileDataAccessor.ts index b81e58075..11c4c4af7 100644 --- a/src/storage/accessors/FileDataAccessor.ts +++ b/src/storage/accessors/FileDataAccessor.ts @@ -12,6 +12,7 @@ import { ConflictHttpError } from '../../util/errors/ConflictHttpError'; import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; import { isSystemError } from '../../util/errors/SystemError'; import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError'; +import { isContainerIdentifier } from '../../util/PathUtil'; import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil'; import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil'; import { CONTENT_TYPE, DCTERMS, POSIX, RDF, XSD } from '../../util/UriConstants'; @@ -62,10 +63,10 @@ export class FileDataAccessor implements DataAccessor { public async getMetadata(identifier: ResourceIdentifier): Promise { const link = await this.resourceMapper.mapUrlToFilePath(identifier); const stats = await this.getStats(link.filePath); - if (!identifier.path.endsWith('/') && stats.isFile()) { + if (!isContainerIdentifier(identifier) && stats.isFile()) { return this.getFileMetadata(link, stats); } - if (identifier.path.endsWith('/') && stats.isDirectory()) { + if (isContainerIdentifier(identifier) && stats.isDirectory()) { return this.getDirectoryMetadata(link, stats); } throw new NotFoundHttpError(); @@ -131,9 +132,9 @@ export class FileDataAccessor implements DataAccessor { } } - if (!identifier.path.endsWith('/') && stats.isFile()) { + if (!isContainerIdentifier(identifier) && stats.isFile()) { await fsPromises.unlink(link.filePath); - } else if (identifier.path.endsWith('/') && stats.isDirectory()) { + } else if (isContainerIdentifier(identifier) && stats.isDirectory()) { await fsPromises.rmdir(link.filePath); } else { throw new NotFoundHttpError(); diff --git a/src/storage/accessors/InMemoryDataAccessor.ts b/src/storage/accessors/InMemoryDataAccessor.ts index 6602585f5..5b6ed4fa0 100644 --- a/src/storage/accessors/InMemoryDataAccessor.ts +++ b/src/storage/accessors/InMemoryDataAccessor.ts @@ -5,7 +5,7 @@ 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 { ensureTrailingSlash } from '../../util/PathUtil'; +import { ensureTrailingSlash, isContainerIdentifier } from '../../util/PathUtil'; import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil'; import type { DataAccessor } from './DataAccessor'; @@ -66,7 +66,7 @@ export class InMemoryDataAccessor implements DataAccessor { public async getMetadata(identifier: ResourceIdentifier): Promise { const entry = this.getEntry(identifier); - if (this.isDataEntry(entry) === identifier.path.endsWith('/')) { + if (this.isDataEntry(entry) === isContainerIdentifier(identifier)) { throw new NotFoundHttpError(); } return this.generateMetadata(identifier, entry); diff --git a/src/storage/accessors/SparqlDataAccessor.ts b/src/storage/accessors/SparqlDataAccessor.ts index 01431da24..d4f114197 100644 --- a/src/storage/accessors/SparqlDataAccessor.ts +++ b/src/storage/accessors/SparqlDataAccessor.ts @@ -23,7 +23,7 @@ import { ConflictHttpError } from '../../util/errors/ConflictHttpError'; import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError'; -import { ensureTrailingSlash, getParentContainer } from '../../util/PathUtil'; +import { ensureTrailingSlash, getParentContainer, isContainerIdentifier } from '../../util/PathUtil'; import { generateResourceQuads } from '../../util/ResourceUtil'; import { CONTENT_TYPE, LDP } from '../../util/UriConstants'; import { toNamedNode } from '../../util/UriUtil'; @@ -81,7 +81,7 @@ export class SparqlDataAccessor implements DataAccessor { */ public async getMetadata(identifier: ResourceIdentifier): Promise { const name = namedNode(identifier.path); - const query = identifier.path.endsWith('/') ? + const query = isContainerIdentifier(identifier) ? this.sparqlConstructContainer(name) : this.sparqlConstruct(this.getMetadataNode(name)); const stream = await this.sendSparqlConstruct(query); diff --git a/src/util/PathUtil.ts b/src/util/PathUtil.ts index c4aedd2f3..fda439ae5 100644 --- a/src/util/PathUtil.ts +++ b/src/util/PathUtil.ts @@ -58,3 +58,15 @@ export const getParentContainer = (id: ResourceIdentifier): ResourceIdentifier = return { path: parentPath }; }; + +/** + * Checks if the path corresponds to a container path (ending in a /). + * @param path - Path to check. + */ +export const isContainerPath = (path: string): boolean => path.endsWith('/'); + +/** + * Checks if the identifier corresponds to a container identifier. + * @param identifier - Identifier to check. + */ +export const isContainerIdentifier = (identifier: ResourceIdentifier): boolean => isContainerPath(identifier.path);