feat: Track binary size of resources when possible

This commit is contained in:
Joachim Van Herwegen
2023-10-02 13:40:19 +02:00
parent 3e9adef4cf
commit 71e55690f3
15 changed files with 194 additions and 35 deletions

View File

@@ -2,7 +2,7 @@ import { createResponse } from 'node-mocks-http';
import { RangeMetadataWriter } from '../../../../../src/http/output/metadata/RangeMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { SOLID_HTTP } from '../../../../../src/util/Vocabularies';
import { POSIX, SOLID_HTTP } from '../../../../../src/util/Vocabularies';
describe('RangeMetadataWriter', (): void => {
let metadata: RepresentationMetadata;
@@ -15,17 +15,19 @@ describe('RangeMetadataWriter', (): void => {
writer = new RangeMetadataWriter();
});
it('adds the content-range header.', async(): Promise<void> => {
it('adds the content-range and content-length header.', async(): Promise<void> => {
metadata.set(SOLID_HTTP.terms.unit, 'bytes');
metadata.set(SOLID_HTTP.terms.start, '1');
metadata.set(SOLID_HTTP.terms.end, '5');
metadata.set(POSIX.terms.size, '10');
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'content-range': 'bytes 1-5/*',
'content-range': 'bytes 1-5/10',
'content-length': '5',
});
});
it('uses * if the value is unknown.', async(): Promise<void> => {
it('uses * if a value is unknown.', async(): Promise<void> => {
metadata.set(SOLID_HTTP.terms.unit, 'bytes');
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
@@ -37,4 +39,34 @@ describe('RangeMetadataWriter', (): void => {
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ });
});
it('adds a content-length header if the size is known.', async(): Promise<void> => {
metadata.set(POSIX.terms.size, '10');
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'content-length': '10',
});
});
it('correctly deduces end values if the size is known.', async(): Promise<void> => {
metadata.set(SOLID_HTTP.terms.unit, 'bytes');
metadata.set(SOLID_HTTP.terms.start, '4');
metadata.set(POSIX.terms.size, '10');
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'content-range': 'bytes 4-9/10',
'content-length': '6',
});
});
it('correctly handles negative start values.', async(): Promise<void> => {
metadata.set(SOLID_HTTP.terms.unit, 'bytes');
metadata.set(SOLID_HTTP.terms.start, '-4');
metadata.set(POSIX.terms.size, '10');
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'content-range': 'bytes 6-9/10',
'content-length': '4',
});
});
});

View File

@@ -5,7 +5,7 @@ import { ResourceStore } from '../../../src/storage/ResourceStore';
import { InternalServerError } from '../../../src/util/errors/InternalServerError';
import { RangeNotSatisfiedHttpError } from '../../../src/util/errors/RangeNotSatisfiedHttpError';
import { readableToString } from '../../../src/util/StreamUtil';
import { SOLID_HTTP } from '../../../src/util/Vocabularies';
import { POSIX, SOLID_HTTP } from '../../../src/util/Vocabularies';
describe('A BinarySliceResourceStore', (): void => {
const identifier = { path: 'path' };
@@ -31,6 +31,14 @@ describe('A BinarySliceResourceStore', (): void => {
expect(result.metadata.get(SOLID_HTTP.terms.end)?.value).toBe('4');
});
it('uses the stream size when slicing if available.', async(): Promise<void> => {
representation.metadata.set(POSIX.terms.size, '10');
const result = await store.getRepresentation(identifier, { range: { unit: 'bytes', parts: [{ start: -4 }]}});
await expect(readableToString(result.data)).resolves.toBe('6789');
expect(result.metadata.get(SOLID_HTTP.terms.unit)?.value).toBe('bytes');
expect(result.metadata.get(SOLID_HTTP.terms.start)?.value).toBe('-4');
});
it('does not add end metadata if there is none.', async(): Promise<void> => {
const result = await store.getRepresentation(identifier, { range: { unit: 'bytes', parts: [{ start: 5 }]}});
await expect(readableToString(result.data)).resolves.toBe('56789');

View File

@@ -9,7 +9,7 @@ import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError
import type { Guarded } from '../../../../src/util/GuardedStream';
import { BaseIdentifierStrategy } from '../../../../src/util/identifiers/BaseIdentifierStrategy';
import { guardedStreamFrom, readableToString } from '../../../../src/util/StreamUtil';
import { LDP, RDF } from '../../../../src/util/Vocabularies';
import { CONTENT_TYPE, LDP, POSIX, RDF } from '../../../../src/util/Vocabularies';
const { namedNode } = DataFactory;
class DummyStrategy extends BaseIdentifierStrategy {
@@ -104,13 +104,18 @@ describe('An InMemoryDataAccessor', (): void => {
it('adds stored metadata when requesting document metadata.', async(): Promise<void> => {
const identifier = { path: `${base}resource` };
const inputMetadata = new RepresentationMetadata(identifier, { [RDF.type]: LDP.terms.Resource });
const inputMetadata = new RepresentationMetadata(identifier, {
[RDF.type]: LDP.terms.Resource,
[CONTENT_TYPE]: 'text/turtle',
});
await expect(accessor.writeDocument(identifier, data, inputMetadata)).resolves.toBeUndefined();
metadata = await accessor.getMetadata(identifier);
expect(metadata.identifier.value).toBe(`${base}resource`);
const quads = metadata.quads();
expect(quads).toHaveLength(1);
expect(quads[0].object.value).toBe(LDP.Resource);
expect(quads).toHaveLength(3);
expect(metadata.get(RDF.terms.type)).toEqual(LDP.terms.Resource);
expect(metadata.contentType).toBe('text/turtle');
expect(metadata.get(POSIX.terms.size)?.value).toBe('4');
});
it('adds stored metadata when requesting container metadata.', async(): Promise<void> => {

View File

@@ -8,7 +8,7 @@ import { BaseTypedRepresentationConverter } from '../../../../src/storage/conver
import { ChainedConverter } from '../../../../src/storage/conversion/ChainedConverter';
import { matchesMediaType } from '../../../../src/storage/conversion/ConversionUtil';
import type { RepresentationConverterArgs } from '../../../../src/storage/conversion/RepresentationConverter';
import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';
import { CONTENT_TYPE, POSIX } from '../../../../src/util/Vocabularies';
class DummyConverter extends BaseTypedRepresentationConverter {
private readonly inTypes: ValuePreferences;
@@ -47,6 +47,7 @@ describe('A ChainedConverter', (): void => {
beforeEach(async(): Promise<void> => {
const metadata = new RepresentationMetadata('a/a');
metadata.set(POSIX.terms.size, '500');
representation = { metadata } as Representation;
preferences = { type: { 'x/x': 1, 'x/*': 0.8 }};
args = { representation, preferences, identifier: { path: 'path' }};
@@ -81,6 +82,7 @@ describe('A ChainedConverter', (): void => {
const result = await converter.handle(args);
expect(result.metadata.contentType).toBe('b/b');
expect(result.metadata.get(POSIX.terms.size)?.value).toBe('500');
});
it('converts input matching the output preferences if a better output can be found.', async(): Promise<void> => {
@@ -91,6 +93,7 @@ describe('A ChainedConverter', (): void => {
const result = await converter.handle(args);
expect(result.metadata.contentType).toBe('x/x');
expect(result.metadata.get(POSIX.terms.size)).toBeUndefined();
});
it('interprets no preferences as */*.', async(): Promise<void> => {
@@ -101,10 +104,12 @@ describe('A ChainedConverter', (): void => {
let result = await converter.handle(args);
expect(result.metadata.contentType).toBe('b/b');
expect(result.metadata.get(POSIX.terms.size)?.value).toBe('500');
args.preferences.type = { };
result = await converter.handle(args);
expect(result.metadata.contentType).toBe('b/b');
expect(result.metadata.get(POSIX.terms.size)?.value).toBe('500');
});
it('can find paths of length 1.', async(): Promise<void> => {
@@ -113,6 +118,7 @@ describe('A ChainedConverter', (): void => {
const result = await converter.handle(args);
expect(result.metadata.contentType).toBe('x/x');
expect(result.metadata.get(POSIX.terms.size)).toBeUndefined();
});
it('can find longer paths.', async(): Promise<void> => {
@@ -126,6 +132,7 @@ describe('A ChainedConverter', (): void => {
const result = await converter.handle(args);
expect(result.metadata.contentType).toBe('x/x');
expect(result.metadata.get(POSIX.terms.size)).toBeUndefined();
});
it('will use the shortest path among the best found.', async(): Promise<void> => {
@@ -147,6 +154,7 @@ describe('A ChainedConverter', (): void => {
}
const result = await converter.handle(args);
expect(result.metadata.contentType).toBe('x/x');
expect(result.metadata.get(POSIX.terms.size)).toBeUndefined();
expect(converters[0].handle).toHaveBeenCalledTimes(0);
expect(converters[1].handle).toHaveBeenCalledTimes(0);
expect(converters[2].handle).toHaveBeenCalledTimes(1);

View File

@@ -1,6 +1,6 @@
import 'jest-rdf';
import { DataFactory } from 'n3';
import { parseQuads, serializeQuads, uniqueQuads } from '../../../src/util/QuadUtil';
import { parseQuads, serializeQuads, termToInt, uniqueQuads } from '../../../src/util/QuadUtil';
import { guardedStreamFrom, readableToString } from '../../../src/util/StreamUtil';
const { literal, namedNode, quad } = DataFactory;
@@ -50,4 +50,15 @@ describe('QuadUtil', (): void => {
]);
});
});
describe('#termToInt', (): void => {
it('returns undefined if the input is undefined.', async(): Promise<void> => {
expect(termToInt()).toBeUndefined();
});
it('converts the term to a number.', async(): Promise<void> => {
expect(termToInt(namedNode('5'))).toBe(5);
expect(termToInt(namedNode('0xF'), 16)).toBe(15);
});
});
});

View File

@@ -4,7 +4,7 @@ import { SliceStream } from '../../../src/util/SliceStream';
import { readableToString } from '../../../src/util/StreamUtil';
describe('A SliceStream', (): void => {
it('does not support suffix slicing.', async(): Promise<void> => {
it('does not support suffix slicing if the size is unknown.', async(): Promise<void> => {
expect((): unknown => new SliceStream(Readable.from('0123456789'), { start: -5 }))
.toThrow(RangeNotSatisfiedHttpError);
});
@@ -16,6 +16,11 @@ describe('A SliceStream', (): void => {
.toThrow(RangeNotSatisfiedHttpError);
});
it('requires the end to be less than the size.', async(): Promise<void> => {
expect((): unknown => new SliceStream(Readable.from('0123456789'), { start: 5, end: 6, size: 6 }))
.toThrow(RangeNotSatisfiedHttpError);
});
it('can slice binary streams.', async(): Promise<void> => {
await expect(readableToString(new SliceStream(Readable.from('0123456789', { objectMode: false }),
{ start: 3, end: 7, objectMode: false }))).resolves.toBe('34567');
@@ -25,6 +30,9 @@ describe('A SliceStream', (): void => {
await expect(readableToString(new SliceStream(Readable.from('0123456789', { objectMode: false }),
{ start: 3, end: 20, objectMode: false }))).resolves.toBe('3456789');
await expect(readableToString(new SliceStream(Readable.from('0123456789', { objectMode: false }),
{ start: -3, size: 10, objectMode: false }))).resolves.toBe('789');
});
it('can slice object streams.', async(): Promise<void> => {
@@ -37,5 +45,8 @@ describe('A SliceStream', (): void => {
await expect(readableToString(new SliceStream(Readable.from(arr, { objectMode: true }),
{ start: 3, end: 20, objectMode: true }))).resolves.toBe('3456789');
await expect(readableToString(new SliceStream(Readable.from(arr, { objectMode: true }),
{ start: -3, size: 10, objectMode: true }))).resolves.toBe('789');
});
});