Skip to content

Commit

Permalink
feat: Update ExtensionBasedMapper custom types
Browse files Browse the repository at this point in the history
  • Loading branch information
rubensworks authored and joachimvh committed Jul 28, 2021
1 parent c01e33e commit 3f8f822
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 19 deletions.
4 changes: 1 addition & 3 deletions config/util/identifiers/subdomain.json
Expand Up @@ -19,9 +19,7 @@
"@type": "SubdomainExtensionBasedMapper",
"base": { "@id": "urn:solid-server:default:variable:baseUrl" },
"rootFilepath": { "@id": "urn:solid-server:default:variable:rootFilePath" },
"baseSubdomain": "www",
"overrideTypes_acl": "text/turtle",
"overrideTypes_meta": "text/turtle"
"baseSubdomain": "www"
}
]
}
4 changes: 1 addition & 3 deletions config/util/identifiers/suffix.json
Expand Up @@ -18,9 +18,7 @@
"@id": "urn:solid-server:default:FileIdentifierMapper",
"@type": "ExtensionBasedMapper",
"base": { "@id": "urn:solid-server:default:variable:baseUrl" },
"rootFilepath": { "@id": "urn:solid-server:default:variable:rootFilePath" },
"overrideTypes_acl": "text/turtle",
"overrideTypes_meta": "text/turtle"
"rootFilepath": { "@id": "urn:solid-server:default:variable:rootFilePath" }
}
]
}
38 changes: 31 additions & 7 deletions src/storage/mapping/ExtensionBasedMapper.ts
@@ -1,18 +1,40 @@
import { promises as fsPromises } from 'fs';
import * as mime from 'mime-types';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import { TEXT_TURTLE } from '../../util/ContentTypes';
import { DEFAULT_CUSTOM_TYPES } from '../../util/ContentTypes';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { joinFilePath, getExtension } from '../../util/PathUtil';
import { BaseFileIdentifierMapper } from './BaseFileIdentifierMapper';
import type { FileIdentifierMapperFactory, ResourceLink } from './FileIdentifierMapper';

/**
* Supports the behaviour described in https://www.w3.org/DesignIssues/HTTPFilenameMapping.html
* Determines content-type based on the file extension.
* In case an identifier does not end on an extension matching its content-type,
* the corresponding file will be appended with the correct extension, preceded by $.
*/
export class ExtensionBasedMapper extends BaseFileIdentifierMapper {
private readonly types: Record<string, any>;
private readonly customTypes: Record<string, string>;
private readonly customExtensions: Record<string, string>;

public constructor(base: string, rootFilepath: string, overrideTypes = { acl: TEXT_TURTLE, meta: TEXT_TURTLE }) {
public constructor(
base: string,
rootFilepath: string,
customTypes?: Record<string, string>,
) {
super(base, rootFilepath);
this.types = { ...mime.types, ...overrideTypes };

// Workaround for https://github.com/LinkedSoftwareDependencies/Components.js/issues/20
if (!customTypes || Object.keys(customTypes).length === 0) {
this.customTypes = DEFAULT_CUSTOM_TYPES;
} else {
this.customTypes = customTypes;
}

this.customExtensions = {};
for (const [ extension, contentType ] of Object.entries(this.customTypes)) {
this.customExtensions[contentType] = extension;
}
}

protected async mapUrlToDocumentPath(identifier: ResourceIdentifier, filePath: string, contentType?: string):
Expand Down Expand Up @@ -42,7 +64,7 @@ export class ExtensionBasedMapper extends BaseFileIdentifierMapper {
// If the extension of the identifier matches a different content-type than the one that is given,
// we need to add a new extension to match the correct type.
} else if (contentType !== await this.getContentTypeFromPath(filePath)) {
const extension = mime.extension(contentType);
const extension: string = mime.extension(contentType) || this.customExtensions[contentType];
if (!extension) {
this.logger.warn(`No extension found for ${contentType}`);
throw new NotImplementedHttpError(`Unsupported content type ${contentType}`);
Expand All @@ -57,8 +79,10 @@ export class ExtensionBasedMapper extends BaseFileIdentifierMapper {
}

protected async getContentTypeFromPath(filePath: string): Promise<string> {
return this.types[getExtension(filePath).toLowerCase()] ||
super.getContentTypeFromPath(filePath);
const extension = getExtension(filePath).toLowerCase();
return mime.lookup(extension) ||
this.customTypes[extension] ||
await super.getContentTypeFromPath(filePath);
}

/**
Expand Down
5 changes: 2 additions & 3 deletions src/storage/mapping/SubdomainExtensionBasedMapper.ts
@@ -1,6 +1,5 @@
import { toASCII, toUnicode } from 'punycode/';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import { TEXT_TURTLE } from '../../util/ContentTypes';
import { ForbiddenHttpError } from '../../util/errors/ForbiddenHttpError';
import { InternalServerError } from '../../util/errors/InternalServerError';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
Expand Down Expand Up @@ -36,8 +35,8 @@ export class SubdomainExtensionBasedMapper extends ExtensionBasedMapper {
private readonly baseParts: { scheme: string; rest: string };

public constructor(base: string, rootFilepath: string, baseSubdomain = 'www',
overrideTypes = { acl: TEXT_TURTLE, meta: TEXT_TURTLE }) {
super(base, rootFilepath, overrideTypes);
customTypes?: Record<string, string>) {
super(base, rootFilepath, customTypes);
this.baseSubdomain = baseSubdomain;
this.regex = createSubdomainRegexp(ensureTrailingSlash(base));
this.baseParts = extractScheme(ensureTrailingSlash(base));
Expand Down
8 changes: 8 additions & 0 deletions src/util/ContentTypes.ts
Expand Up @@ -2,6 +2,7 @@
export const APPLICATION_JSON = 'application/json';
export const APPLICATION_OCTET_STREAM = 'application/octet-stream';
export const APPLICATION_SPARQL_UPDATE = 'application/sparql-update';
export const APPLICATION_TRIG = 'application/trig';
export const APPLICATION_X_WWW_FORM_URLENCODED = 'application/x-www-form-urlencoded';
export const TEXT_HTML = 'text/html';
export const TEXT_MARKDOWN = 'text/markdown';
Expand All @@ -11,3 +12,10 @@ export const TEXT_TURTLE = 'text/turtle';
export const INTERNAL_ALL = 'internal/*';
export const INTERNAL_QUADS = 'internal/quads';
export const INTERNAL_ERROR = 'internal/error';

// Trig can be removed once the mime-types library is updated with the latest mime-db version
export const DEFAULT_CUSTOM_TYPES = {
acl: TEXT_TURTLE,
meta: TEXT_TURTLE,
trig: APPLICATION_TRIG,
};
4 changes: 1 addition & 3 deletions templates/config/filesystem.json
Expand Up @@ -16,9 +16,7 @@
},
"rootFilepath": {
"@id": "urn:solid-server:template:variable:rootFilePath"
},
"overrideTypes_acl": "text/turtle",
"overrideTypes_meta": "text/turtle"
}
},

{
Expand Down
33 changes: 33 additions & 0 deletions test/unit/storage/mapping/ExtensionBasedMapper.test.ts
Expand Up @@ -130,6 +130,28 @@ describe('An ExtensionBasedMapper', (): void => {
await expect(result).rejects.toThrow(NotImplementedHttpError);
await expect(result).rejects.toThrow('Unsupported content type fake/data');
});

it('supports custom types.', async(): Promise<void> => {
const customMapper = new ExtensionBasedMapper(base, rootFilepath, { cstm: 'text/custom' });
await expect(customMapper.mapUrlToFilePath({ path: `${base}test.cstm` }, false))
.resolves.toEqual({
identifier: { path: `${base}test.cstm` },
filePath: `${rootFilepath}test.cstm`,
contentType: 'text/custom',
isMetadata: false,
});
});

it('supports custom extensions.', async(): Promise<void> => {
const customMapper = new ExtensionBasedMapper(base, rootFilepath, { cstm: 'text/custom' });
await expect(customMapper.mapUrlToFilePath({ path: `${base}test` }, false, 'text/custom'))
.resolves.toEqual({
identifier: { path: `${base}test` },
filePath: `${rootFilepath}test$.cstm`,
contentType: 'text/custom',
isMetadata: false,
});
});
});

describe('mapFilePathToUrl', (): void => {
Expand Down Expand Up @@ -180,6 +202,17 @@ describe('An ExtensionBasedMapper', (): void => {
isMetadata: false,
});
});

it('supports custom extensions.', async(): Promise<void> => {
const customMapper = new ExtensionBasedMapper(base, rootFilepath, { cstm: 'text/custom' });
await expect(customMapper.mapFilePathToUrl(`${rootFilepath}test$.cstm`, false))
.resolves.toEqual({
identifier: { path: `${base}test` },
filePath: `${rootFilepath}test$.cstm`,
contentType: 'text/custom',
isMetadata: false,
});
});
});

describe('An ExtensionBasedMapperFactory', (): void => {
Expand Down

0 comments on commit 3f8f822

Please sign in to comment.