mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: file-based backend fallback for unknown media types
This commit is contained in:
parent
fa78bc6856
commit
ff80079000
@ -12,7 +12,7 @@ import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMedi
|
||||
import { guardStream } from '../../util/GuardedStream';
|
||||
import type { Guarded } from '../../util/GuardedStream';
|
||||
import { parseContentType } from '../../util/HeaderUtil';
|
||||
import { joinFilePath, isContainerIdentifier } from '../../util/PathUtil';
|
||||
import { joinFilePath, isContainerIdentifier, isContainerPath } from '../../util/PathUtil';
|
||||
import { parseQuads, serializeQuads } from '../../util/QuadUtil';
|
||||
import { addResourceMetadata, updateModifiedDate } from '../../util/ResourceUtil';
|
||||
import { toLiteral, toNamedTerm } from '../../util/TermUtil';
|
||||
@ -159,8 +159,14 @@ export class FileDataAccessor implements DataAccessor {
|
||||
*/
|
||||
private async getFileMetadata(link: ResourceLink, stats: Stats):
|
||||
Promise<RepresentationMetadata> {
|
||||
return (await this.getBaseMetadata(link, stats, false))
|
||||
.set(CONTENT_TYPE_TERM, link.contentType);
|
||||
const metadata = await this.getBaseMetadata(link, stats, false);
|
||||
// If the resource is using an unsupported contentType, the original contentType was written to the metadata file.
|
||||
// As a result, we should only set the contentType derived from the file path,
|
||||
// when no previous metadata entry for contentType is present.
|
||||
if (typeof metadata.contentType === 'undefined') {
|
||||
metadata.set(CONTENT_TYPE_TERM, link.contentType);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -188,7 +194,12 @@ export class FileDataAccessor implements DataAccessor {
|
||||
metadata.remove(RDF.terms.type, LDP.terms.Container);
|
||||
metadata.remove(RDF.terms.type, LDP.terms.BasicContainer);
|
||||
metadata.removeAll(DC.terms.modified);
|
||||
metadata.removeAll(CONTENT_TYPE_TERM);
|
||||
// When writing metadata for a document, only remove the content-type when dealing with a supported media type.
|
||||
// A media type is supported if the FileIdentifierMapper can correctly store it.
|
||||
// This allows restoring the appropriate content-type on data read (see getFileMetadata).
|
||||
if (isContainerPath(link.filePath) || typeof link.contentType !== 'undefined') {
|
||||
metadata.removeAll(CONTENT_TYPE_TERM);
|
||||
}
|
||||
const quads = metadata.quads();
|
||||
const metadataLink = await this.resourceMapper.mapUrlToFilePath(link.identifier, true);
|
||||
let wroteMetadata: boolean;
|
||||
|
@ -23,6 +23,8 @@ export class BaseFileIdentifierMapper implements FileIdentifierMapper {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
protected readonly baseRequestURI: string;
|
||||
protected readonly rootFilepath: string;
|
||||
// Extension to use as a fallback when the media type is not supported (could be made configurable).
|
||||
protected readonly unknownMediaTypeExtension = 'unknown';
|
||||
|
||||
public constructor(base: string, rootFilepath: string) {
|
||||
this.baseRequestURI = trimTrailingSlashes(base);
|
||||
@ -85,7 +87,10 @@ export class BaseFileIdentifierMapper implements FileIdentifierMapper {
|
||||
*/
|
||||
protected async mapUrlToDocumentPath(identifier: ResourceIdentifier, filePath: string, contentType?: string):
|
||||
Promise<ResourceLink> {
|
||||
contentType = await this.getContentTypeFromUrl(identifier, contentType);
|
||||
// Don't try to get content-type from URL when the file path refers to a document with unknown media type.
|
||||
if (!filePath.endsWith(`.${this.unknownMediaTypeExtension}`)) {
|
||||
contentType = await this.getContentTypeFromUrl(identifier, contentType);
|
||||
}
|
||||
this.logger.debug(`The path for ${identifier.path} is ${filePath}`);
|
||||
return { identifier, filePath, contentType, isMetadata: this.isMetadataPath(filePath) };
|
||||
}
|
||||
|
@ -64,10 +64,12 @@ 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: string = mime.extension(contentType) || this.customExtensions[contentType];
|
||||
let 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}`);
|
||||
// When no extension is found for the provided content-type, use a fallback extension.
|
||||
extension = this.unknownMediaTypeExtension;
|
||||
// Signal the fallback by setting the content-type to undefined in the output link.
|
||||
contentType = undefined;
|
||||
}
|
||||
filePath += `$.${extension}`;
|
||||
}
|
||||
|
@ -10,10 +10,10 @@ import {
|
||||
removeFolder,
|
||||
} from './Config';
|
||||
|
||||
const port = getPort('FileBackendEncodedSlashHandling');
|
||||
const port = getPort('FileBackend');
|
||||
const baseUrl = `http://localhost:${port}/`;
|
||||
|
||||
const rootFilePath = getTestFolder('file-backend-encoded-slash-handling');
|
||||
const rootFilePath = getTestFolder('file-backend');
|
||||
|
||||
describe('A server with a file backend storage', (): void => {
|
||||
let app: App;
|
||||
@ -146,4 +146,35 @@ describe('A server with a file backend storage', (): void => {
|
||||
const check4 = await pathExists(`${rootFilePath}/bar%2Ffoo$.txt`);
|
||||
expect(check4).toBe(true);
|
||||
});
|
||||
|
||||
it('supports content types for which no extension mapping can be found (and falls back to using .unknown).',
|
||||
async(): Promise<void> => {
|
||||
const url = `${baseUrl}test`;
|
||||
const res = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'unknown/some-type',
|
||||
},
|
||||
body: 'abc',
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.headers.get('location')).toBe(`${baseUrl}test`);
|
||||
|
||||
// Check if the document can be retrieved
|
||||
const check1 = await fetch(`${baseUrl}test`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
accept: '*/*',
|
||||
},
|
||||
});
|
||||
const body = await check1.text();
|
||||
expect(check1.status).toBe(200);
|
||||
expect(body).toBe('abc');
|
||||
// The content-type should be unknown/some-type.
|
||||
expect(check1.headers.get('content-type')).toBe('unknown/some-type');
|
||||
|
||||
// Check if the expected file was created
|
||||
const check2 = await pathExists(`${rootFilePath}/test$.unknown`);
|
||||
expect(check2).toBe(true);
|
||||
});
|
||||
});
|
@ -125,11 +125,16 @@ describe('An ExtensionBasedMapper', (): void => {
|
||||
});
|
||||
});
|
||||
|
||||
it('throws 501 if the given content-type is not recognized.', async(): Promise<void> => {
|
||||
const result = mapper.mapUrlToFilePath({ path: `${base}test.txt` }, false, 'fake/data');
|
||||
await expect(result).rejects.toThrow(NotImplementedHttpError);
|
||||
await expect(result).rejects.toThrow('Unsupported content type fake/data');
|
||||
});
|
||||
it('falls back to custom extension for unknown types (for which no custom mapping exists).',
|
||||
async(): Promise<void> => {
|
||||
const result = mapper.mapUrlToFilePath({ path: `${base}test` }, false, 'unknown/content-type');
|
||||
await expect(result).resolves.toEqual({
|
||||
identifier: { path: `${base}test` },
|
||||
filePath: `${rootFilepath}test$.unknown`,
|
||||
contentType: undefined,
|
||||
isMetadata: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('supports custom types.', async(): Promise<void> => {
|
||||
const customMapper = new ExtensionBasedMapper(base, rootFilepath, { cstm: 'text/custom' });
|
||||
|
@ -8,7 +8,7 @@ const portNames = [
|
||||
'ContentNegotiation',
|
||||
'DynamicPods',
|
||||
'ExpiringDataCleanup',
|
||||
'FileBackendEncodedSlashHandling',
|
||||
'FileBackend',
|
||||
'GlobalQuota',
|
||||
'Identity',
|
||||
'LpdHandlerWithAuth',
|
||||
|
Loading…
x
Reference in New Issue
Block a user