CommunitySolidServer/src/storage/BinarySliceResourceStore.ts
2023-11-02 09:49:17 +01:00

72 lines
3.3 KiB
TypeScript

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 { termToInt } from '../util/QuadUtil';
import { SliceStream } from '../util/SliceStream';
import { toLiteral } from '../util/TermUtil';
import { POSIX, SOLID_HTTP, XSD } from '../util/Vocabularies';
import type { Conditions } from './conditions/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 {
const size = termToInt(result.metadata.get(POSIX.terms.size));
// 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, size, 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;
}
}