-
Notifications
You must be signed in to change notification settings - Fork 121
/
ExtensionBasedMapper.ts
105 lines (95 loc) · 4.34 KB
/
ExtensionBasedMapper.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import { promises as fsPromises } from 'fs';
import * as mime from 'mime-types';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
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 customTypes: Record<string, string>;
private readonly customExtensions: Record<string, string>;
public constructor(
base: string,
rootFilepath: string,
customTypes?: Record<string, string>,
) {
super(base, rootFilepath);
// 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):
Promise<ResourceLink> {
// Would conflict with how new extensions are stored
if (/\$\.\w+$/u.test(filePath)) {
this.logger.warn(`Identifier ${identifier.path} contains a dollar sign before its extension`);
throw new NotImplementedHttpError('Identifiers cannot contain a dollar sign before their extension');
}
// Existing file
if (!contentType) {
// Find a matching file
const [ , folder, documentName ] = /^(.*\/)(.*)$/u.exec(filePath)!;
let fileName: string | undefined;
try {
const files = await fsPromises.readdir(folder);
fileName = files.find((file): boolean =>
file.startsWith(documentName) && /^(?:\$\..+)?$/u.test(file.slice(documentName.length)));
} catch {
// Parent folder does not exist (or is not a folder)
}
if (fileName) {
filePath = joinFilePath(folder, fileName);
}
contentType = await this.getContentTypeFromPath(filePath);
// 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: string = mime.extension(contentType) || this.customExtensions[contentType];
if (!extension) {
this.logger.warn(`No extension found for ${contentType}`);
throw new NotImplementedHttpError(`Unsupported content type ${contentType}`);
}
filePath += `$.${extension}`;
}
return super.mapUrlToDocumentPath(identifier, filePath, contentType);
}
protected async getDocumentUrl(relative: string): Promise<string> {
return super.getDocumentUrl(this.stripExtension(relative));
}
protected async getContentTypeFromPath(filePath: string): Promise<string> {
const extension = getExtension(filePath).toLowerCase();
return mime.lookup(extension) ||
this.customTypes[extension] ||
await super.getContentTypeFromPath(filePath);
}
/**
* Helper function that removes the internal extension, one starting with $., from the given path.
* Nothing happens if no such extension is present.
*/
protected stripExtension(path: string): string {
const extension = getExtension(path);
if (extension && path.endsWith(`$.${extension}`)) {
path = path.slice(0, -(extension.length + 2));
}
return path;
}
}
export class ExtensionBasedMapperFactory implements FileIdentifierMapperFactory<ExtensionBasedMapper> {
public async create(base: string, rootFilePath: string): Promise<ExtensionBasedMapper> {
return new ExtensionBasedMapper(base, rootFilePath);
}
}