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 { guardStream } from '../../util/GuardedStream';
|
||||||
import type { Guarded } from '../../util/GuardedStream';
|
import type { Guarded } from '../../util/GuardedStream';
|
||||||
import { parseContentType } from '../../util/HeaderUtil';
|
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 { parseQuads, serializeQuads } from '../../util/QuadUtil';
|
||||||
import { addResourceMetadata, updateModifiedDate } from '../../util/ResourceUtil';
|
import { addResourceMetadata, updateModifiedDate } from '../../util/ResourceUtil';
|
||||||
import { toLiteral, toNamedTerm } from '../../util/TermUtil';
|
import { toLiteral, toNamedTerm } from '../../util/TermUtil';
|
||||||
@ -159,8 +159,14 @@ export class FileDataAccessor implements DataAccessor {
|
|||||||
*/
|
*/
|
||||||
private async getFileMetadata(link: ResourceLink, stats: Stats):
|
private async getFileMetadata(link: ResourceLink, stats: Stats):
|
||||||
Promise<RepresentationMetadata> {
|
Promise<RepresentationMetadata> {
|
||||||
return (await this.getBaseMetadata(link, stats, false))
|
const metadata = await this.getBaseMetadata(link, stats, false);
|
||||||
.set(CONTENT_TYPE_TERM, link.contentType);
|
// 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.Container);
|
||||||
metadata.remove(RDF.terms.type, LDP.terms.BasicContainer);
|
metadata.remove(RDF.terms.type, LDP.terms.BasicContainer);
|
||||||
metadata.removeAll(DC.terms.modified);
|
metadata.removeAll(DC.terms.modified);
|
||||||
|
// 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);
|
metadata.removeAll(CONTENT_TYPE_TERM);
|
||||||
|
}
|
||||||
const quads = metadata.quads();
|
const quads = metadata.quads();
|
||||||
const metadataLink = await this.resourceMapper.mapUrlToFilePath(link.identifier, true);
|
const metadataLink = await this.resourceMapper.mapUrlToFilePath(link.identifier, true);
|
||||||
let wroteMetadata: boolean;
|
let wroteMetadata: boolean;
|
||||||
|
@ -23,6 +23,8 @@ export class BaseFileIdentifierMapper implements FileIdentifierMapper {
|
|||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
protected readonly baseRequestURI: string;
|
protected readonly baseRequestURI: string;
|
||||||
protected readonly rootFilepath: 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) {
|
public constructor(base: string, rootFilepath: string) {
|
||||||
this.baseRequestURI = trimTrailingSlashes(base);
|
this.baseRequestURI = trimTrailingSlashes(base);
|
||||||
@ -85,7 +87,10 @@ export class BaseFileIdentifierMapper implements FileIdentifierMapper {
|
|||||||
*/
|
*/
|
||||||
protected async mapUrlToDocumentPath(identifier: ResourceIdentifier, filePath: string, contentType?: string):
|
protected async mapUrlToDocumentPath(identifier: ResourceIdentifier, filePath: string, contentType?: string):
|
||||||
Promise<ResourceLink> {
|
Promise<ResourceLink> {
|
||||||
|
// 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);
|
contentType = await this.getContentTypeFromUrl(identifier, contentType);
|
||||||
|
}
|
||||||
this.logger.debug(`The path for ${identifier.path} is ${filePath}`);
|
this.logger.debug(`The path for ${identifier.path} is ${filePath}`);
|
||||||
return { identifier, filePath, contentType, isMetadata: this.isMetadataPath(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,
|
// 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.
|
// we need to add a new extension to match the correct type.
|
||||||
} else if (contentType !== await this.getContentTypeFromPath(filePath)) {
|
} 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) {
|
if (!extension) {
|
||||||
this.logger.warn(`No extension found for ${contentType}`);
|
// When no extension is found for the provided content-type, use a fallback extension.
|
||||||
throw new NotImplementedHttpError(`Unsupported content type ${contentType}`);
|
extension = this.unknownMediaTypeExtension;
|
||||||
|
// Signal the fallback by setting the content-type to undefined in the output link.
|
||||||
|
contentType = undefined;
|
||||||
}
|
}
|
||||||
filePath += `$.${extension}`;
|
filePath += `$.${extension}`;
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,10 @@ import {
|
|||||||
removeFolder,
|
removeFolder,
|
||||||
} from './Config';
|
} from './Config';
|
||||||
|
|
||||||
const port = getPort('FileBackendEncodedSlashHandling');
|
const port = getPort('FileBackend');
|
||||||
const baseUrl = `http://localhost:${port}/`;
|
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 => {
|
describe('A server with a file backend storage', (): void => {
|
||||||
let app: App;
|
let app: App;
|
||||||
@ -146,4 +146,35 @@ describe('A server with a file backend storage', (): void => {
|
|||||||
const check4 = await pathExists(`${rootFilePath}/bar%2Ffoo$.txt`);
|
const check4 = await pathExists(`${rootFilePath}/bar%2Ffoo$.txt`);
|
||||||
expect(check4).toBe(true);
|
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,10 +125,15 @@ describe('An ExtensionBasedMapper', (): void => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws 501 if the given content-type is not recognized.', async(): Promise<void> => {
|
it('falls back to custom extension for unknown types (for which no custom mapping exists).',
|
||||||
const result = mapper.mapUrlToFilePath({ path: `${base}test.txt` }, false, 'fake/data');
|
async(): Promise<void> => {
|
||||||
await expect(result).rejects.toThrow(NotImplementedHttpError);
|
const result = mapper.mapUrlToFilePath({ path: `${base}test` }, false, 'unknown/content-type');
|
||||||
await expect(result).rejects.toThrow('Unsupported content type fake/data');
|
await expect(result).resolves.toEqual({
|
||||||
|
identifier: { path: `${base}test` },
|
||||||
|
filePath: `${rootFilepath}test$.unknown`,
|
||||||
|
contentType: undefined,
|
||||||
|
isMetadata: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports custom types.', async(): Promise<void> => {
|
it('supports custom types.', async(): Promise<void> => {
|
||||||
|
@ -8,7 +8,7 @@ const portNames = [
|
|||||||
'ContentNegotiation',
|
'ContentNegotiation',
|
||||||
'DynamicPods',
|
'DynamicPods',
|
||||||
'ExpiringDataCleanup',
|
'ExpiringDataCleanup',
|
||||||
'FileBackendEncodedSlashHandling',
|
'FileBackend',
|
||||||
'GlobalQuota',
|
'GlobalQuota',
|
||||||
'Identity',
|
'Identity',
|
||||||
'LpdHandlerWithAuth',
|
'LpdHandlerWithAuth',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user