Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
25 changed files
with
619 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld", | ||
"@graph": [ | ||
{ | ||
"comment": "Adds the Content-Range header if necessary.", | ||
"@id": "urn:solid-server:default:MetadataWriter_Range", | ||
"@type": "RangeMetadataWriter" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}, {}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import type { HttpResponse } from '../../../server/HttpResponse'; | ||
import { addHeader } from '../../../util/HeaderUtil'; | ||
import { 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 `*`. | ||
*/ | ||
export class RangeMetadataWriter extends MetadataWriter { | ||
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> { | ||
const unit = input.metadata.get(SOLID_HTTP.terms.unit); | ||
if (!unit) { | ||
return; | ||
} | ||
const start = input.metadata.get(SOLID_HTTP.terms.start); | ||
const end = input.metadata.get(SOLID_HTTP.terms.end); | ||
|
||
addHeader(input.response, 'Content-Range', `${unit.value} ${start?.value ?? '*'}-${end?.value ?? '*'}/*`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,19 @@ | ||
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 { | ||
/** | ||
* @param metadata - Metadata concerning the response. | ||
* @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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import type { Representation } from '../http/representation/Representation'; | ||
import type { RepresentationPreferences } from '../http/representation/RepresentationPreferences'; | ||
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier'; | ||
import { getLoggerFor } from '../logging/LogUtil'; | ||
import { InternalServerError } from '../util/errors/InternalServerError'; | ||
import { RangeNotSatisfiedHttpError } from '../util/errors/RangeNotSatisfiedHttpError'; | ||
import { guardStream } from '../util/GuardedStream'; | ||
import { SliceStream } from '../util/SliceStream'; | ||
import { toLiteral } from '../util/TermUtil'; | ||
import { SOLID_HTTP, XSD } from '../util/Vocabularies'; | ||
import type { Conditions } from './Conditions'; | ||
import { PassthroughStore } from './PassthroughStore'; | ||
import type { ResourceStore } from './ResourceStore'; | ||
|
||
/** | ||
* Resource store that slices the data stream if there are range preferences. | ||
* Only works for `bytes` range preferences on binary data streams. | ||
* Does not support multipart range requests. | ||
* | ||
* If the slice happens, unit/start/end values will be written to the metadata to indicate such. | ||
* The values are dependent on the preferences we got as an input, | ||
* as we don't know the actual size of the data stream. | ||
*/ | ||
export class BinarySliceResourceStore<T extends ResourceStore = ResourceStore> extends PassthroughStore<T> { | ||
protected readonly logger = getLoggerFor(this); | ||
|
||
public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences, | ||
conditions?: Conditions): Promise<Representation> { | ||
const result = await this.source.getRepresentation(identifier, preferences, conditions); | ||
|
||
if (!preferences.range || preferences.range.unit !== 'bytes' || preferences.range.parts.length === 0) { | ||
return result; | ||
} | ||
if (result.metadata.has(SOLID_HTTP.unit)) { | ||
this.logger.debug('Not slicing stream that has already been sliced.'); | ||
return result; | ||
} | ||
|
||
if (!result.binary) { | ||
throw new InternalServerError('Trying to slice a non-binary stream.'); | ||
} | ||
if (preferences.range.parts.length > 1) { | ||
throw new RangeNotSatisfiedHttpError('Multipart range requests are not supported.'); | ||
} | ||
|
||
const [{ start, end }] = preferences.range.parts; | ||
result.metadata.set(SOLID_HTTP.terms.unit, preferences.range.unit); | ||
result.metadata.set(SOLID_HTTP.terms.start, toLiteral(start, XSD.terms.integer)); | ||
if (typeof end === 'number') { | ||
result.metadata.set(SOLID_HTTP.terms.end, toLiteral(end, XSD.terms.integer)); | ||
} | ||
|
||
try { | ||
// The reason we don't determine the object mode based on the object mode of the parent stream | ||
// is that `guardedStreamFrom` does not create object streams when inputting streams/buffers. | ||
// Something to potentially update in the future. | ||
result.data = guardStream(new SliceStream(result.data, { start, end, objectMode: false })); | ||
} catch (error: unknown) { | ||
// Creating the slice stream can throw an error if some of the parameters are unacceptable. | ||
// Need to make sure the stream is closed in that case. | ||
result.data.destroy(); | ||
throw error; | ||
} | ||
return result; | ||
} | ||
} |
Oops, something went wrong.