mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
Merge branch 'main' into versions/next-major
This commit is contained in:
@@ -11,7 +11,7 @@ import type { RepresentationPreferences } from '../../representation/Representat
|
||||
import { PreferenceParser } from './PreferenceParser';
|
||||
|
||||
const parsers: {
|
||||
name: keyof RepresentationPreferences;
|
||||
name: Exclude<keyof RepresentationPreferences, 'range'>;
|
||||
header: string;
|
||||
parse: (value: string) => AcceptHeader[];
|
||||
}[] = [
|
||||
|
||||
48
src/http/input/preferences/RangePreferenceParser.ts
Normal file
48
src/http/input/preferences/RangePreferenceParser.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { HttpRequest } from '../../../server/HttpRequest';
|
||||
import { BadRequestHttpError } from '../../../util/errors/BadRequestHttpError';
|
||||
import type { RepresentationPreferences } from '../../representation/RepresentationPreferences';
|
||||
import { PreferenceParser } from './PreferenceParser';
|
||||
|
||||
/**
|
||||
* Parses the range header into range preferences.
|
||||
* If the range corresponds to a suffix-length range, it will be stored in `start` as a negative value.
|
||||
*/
|
||||
export class RangePreferenceParser extends PreferenceParser {
|
||||
public async handle({ request: { headers: { range }}}: { request: HttpRequest }): Promise<RepresentationPreferences> {
|
||||
if (!range) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const [ unit, rangeTail ] = range.split('=').map((entry): string => entry.trim());
|
||||
if (unit.length === 0) {
|
||||
throw new BadRequestHttpError(`Missing unit value from range header ${range}`);
|
||||
}
|
||||
if (!rangeTail) {
|
||||
throw new BadRequestHttpError(`Invalid range header format ${range}`);
|
||||
}
|
||||
|
||||
const ranges = rangeTail.split(',').map((entry): string => entry.trim());
|
||||
const parts: { start: number; end?: number }[] = [];
|
||||
for (const rangeEntry of ranges) {
|
||||
const [ start, end ] = rangeEntry.split('-').map((entry): string => entry.trim());
|
||||
// This can actually be undefined if the split results in less than 2 elements
|
||||
if (typeof end !== 'string') {
|
||||
throw new BadRequestHttpError(`Invalid range header format ${range}`);
|
||||
}
|
||||
if (start.length === 0) {
|
||||
if (end.length === 0) {
|
||||
throw new BadRequestHttpError(`Invalid range header format ${range}`);
|
||||
}
|
||||
parts.push({ start: -Number.parseInt(end, 10) });
|
||||
} else {
|
||||
const part: typeof parts[number] = { start: Number.parseInt(start, 10) };
|
||||
if (end.length > 0) {
|
||||
part.end = Number.parseInt(end, 10);
|
||||
}
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
return { range: { unit, parts }};
|
||||
}
|
||||
}
|
||||
32
src/http/input/preferences/UnionPreferenceParser.ts
Normal file
32
src/http/input/preferences/UnionPreferenceParser.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { InternalServerError } from '../../../util/errors/InternalServerError';
|
||||
import { UnionHandler } from '../../../util/handlers/UnionHandler';
|
||||
import type { RepresentationPreferences } from '../../representation/RepresentationPreferences';
|
||||
import type { PreferenceParser } from './PreferenceParser';
|
||||
|
||||
/**
|
||||
* Combines the results of multiple {@link PreferenceParser}s.
|
||||
* Will throw an error if multiple parsers return a range as these can't logically be combined.
|
||||
*/
|
||||
export class UnionPreferenceParser extends UnionHandler<PreferenceParser> {
|
||||
public constructor(parsers: PreferenceParser[]) {
|
||||
super(parsers, false, false);
|
||||
}
|
||||
|
||||
protected async combine(results: RepresentationPreferences[]): Promise<RepresentationPreferences> {
|
||||
const rangeCount = results.filter((result): boolean => Boolean(result.range)).length;
|
||||
if (rangeCount > 1) {
|
||||
throw new InternalServerError('Found multiple range values. This implies a misconfiguration.');
|
||||
}
|
||||
|
||||
return results.reduce<RepresentationPreferences>((acc, val): RepresentationPreferences => {
|
||||
for (const key of Object.keys(val) as (keyof RepresentationPreferences)[]) {
|
||||
if (key === 'range') {
|
||||
acc[key] = val[key];
|
||||
} else {
|
||||
acc[key] = { ...acc[key], ...val[key] };
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
50
src/http/output/metadata/RangeMetadataWriter.ts
Normal file
50
src/http/output/metadata/RangeMetadataWriter.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { getLoggerFor } from '../../../logging/LogUtil';
|
||||
import type { HttpResponse } from '../../../server/HttpResponse';
|
||||
import { addHeader } from '../../../util/HeaderUtil';
|
||||
import { termToInt } from '../../../util/QuadUtil';
|
||||
import { POSIX, SOLID_HTTP } from '../../../util/Vocabularies';
|
||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
import { MetadataWriter } from './MetadataWriter';
|
||||
|
||||
/**
|
||||
* Generates the necessary `content-range` header if there is range metadata.
|
||||
* If the start or end is unknown, a `*` will be used instead.
|
||||
* According to the RFC, this is incorrect,
|
||||
* but is all we can do as long as we don't know the full length of the representation in advance.
|
||||
* For the same reason, the total length of the representation will always be `*`.
|
||||
*
|
||||
* This class also adds the content-length header.
|
||||
* This will contain either the full size for standard requests,
|
||||
* or the size of the slice for range requests.
|
||||
*/
|
||||
export class RangeMetadataWriter extends MetadataWriter {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
|
||||
const size = termToInt(input.metadata.get(POSIX.terms.size));
|
||||
const unit = input.metadata.get(SOLID_HTTP.terms.unit)?.value;
|
||||
if (!unit) {
|
||||
if (typeof size === 'number') {
|
||||
addHeader(input.response, 'Content-Length', `${size}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let start = termToInt(input.metadata.get(SOLID_HTTP.terms.start));
|
||||
if (typeof start === 'number' && start < 0 && typeof size === 'number') {
|
||||
start = size + start;
|
||||
}
|
||||
let end = termToInt(input.metadata.get(SOLID_HTTP.terms.end));
|
||||
if (typeof end !== 'number' && typeof size === 'number') {
|
||||
end = size - 1;
|
||||
}
|
||||
|
||||
const rangeHeader = `${unit} ${start ?? '*'}-${end ?? '*'}/${size ?? '*'}`;
|
||||
addHeader(input.response, 'Content-Range', rangeHeader);
|
||||
if (typeof start === 'number' && typeof end === 'number') {
|
||||
addHeader(input.response, 'Content-Length', `${end - start + 1}`);
|
||||
} else {
|
||||
this.logger.warn(`Generating invalid content-range header due to missing size information: ${rangeHeader}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Readable } from 'stream';
|
||||
import type { Guarded } from '../../../util/GuardedStream';
|
||||
import { SOLID_HTTP } from '../../../util/Vocabularies';
|
||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
import { ResponseDescription } from './ResponseDescription';
|
||||
|
||||
/**
|
||||
* Corresponds to a 200 response, containing relevant metadata and potentially data.
|
||||
* Corresponds to a 200 or 206 response, containing relevant metadata and potentially data.
|
||||
* A 206 will be returned if range metadata is found in the metadata object.
|
||||
*/
|
||||
export class OkResponseDescription extends ResponseDescription {
|
||||
/**
|
||||
@@ -12,6 +14,6 @@ export class OkResponseDescription extends ResponseDescription {
|
||||
* @param data - Potential data. @ignored
|
||||
*/
|
||||
public constructor(metadata: RepresentationMetadata, data?: Guarded<Readable>) {
|
||||
super(200, metadata, data);
|
||||
super(metadata.has(SOLID_HTTP.terms.unit) ? 206 : 200, metadata, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,4 +31,6 @@ export interface RepresentationPreferences {
|
||||
datetime?: ValuePreferences;
|
||||
encoding?: ValuePreferences;
|
||||
language?: ValuePreferences;
|
||||
// `start` can be negative and implies the last X of a stream
|
||||
range?: { unit: string; parts: { start: number; end?: number }[] };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user