Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/http/middleware/handlers/constant-headers.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
{
"HeaderHandler:_headers_key": "X-Powered-By",
"HeaderHandler:_headers_value": "Community Solid Server"
},
{
"HeaderHandler:_headers_key": "Accept-Ranges",
"HeaderHandler:_headers_value": "bytes"
}
]
}
Expand Down
3 changes: 2 additions & 1 deletion config/storage/middleware/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"comment": "Slices part of binary streams based on the range preferences.",
"@id": "urn:solid-server:default:ResourceStore_BinarySlice",
"@type": "BinarySliceResourceStore",
"source": { "@id": "urn:solid-server:default:ResourceStore_Index" }
"source": { "@id": "urn:solid-server:default:ResourceStore_Index" },
"defaultSliceSize": 10000000
},
{
"comment": "When a container with an index.html document is accessed, serve that HTML document instead of the container.",
Expand Down
19 changes: 17 additions & 2 deletions src/storage/BinarySliceResourceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,18 @@ import type { ResourceStore } from './ResourceStore';
* 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.
*
* The `defaultSliceSize` parameter can be used to set how large a slice should be if the end of a range is not defined.
* Setting this to 0, which is the default, will cause the end of the stream to be used as the end of the slice.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*/
*
* The `defaultSliceSize` parameter can be used to set how large a slice should be if the end of a range is not defined.
* Setting this to 0, which is the default, will cause the end of the stream to be used as the end of the slice.
*/

export class BinarySliceResourceStore<T extends ResourceStore = ResourceStore> extends PassthroughStore<T> {
protected readonly logger = getLoggerFor(this);
private readonly defaultSliceSize: number;

public constructor(source: T, defaultSliceSize = 0) {
super(source);
this.defaultSliceSize = defaultSliceSize;
}

public async getRepresentation(
identifier: ResourceIdentifier,
Expand All @@ -47,15 +56,21 @@ export class BinarySliceResourceStore<T extends ResourceStore = ResourceStore> e
throw new RangeNotSatisfiedHttpError('Multipart range requests are not supported.');
}

const [{ start, end }] = preferences.range.parts;
let [{ start, end }] = preferences.range.parts;
const size = termToInt(result.metadata.get(POSIX.terms.size));

// Set the default end size if not set already
if (this.defaultSliceSize > 0 && typeof end !== 'number' && typeof size === 'number' && start >= 0) {
end = Math.min(size, start + this.defaultSliceSize) - 1;
}

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.
Expand Down
26 changes: 25 additions & 1 deletion test/unit/storage/BinarySliceResourceStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('A BinarySliceResourceStore', (): void => {
getRepresentation: jest.fn().mockResolvedValue(representation),
} satisfies Partial<ResourceStore> as any;

store = new BinarySliceResourceStore(source);
store = new BinarySliceResourceStore(source, 5);
});

it('slices the data stream and stores the metadata.', async(): Promise<void> => {
Expand All @@ -39,6 +39,24 @@ describe('A BinarySliceResourceStore', (): void => {
expect(result.metadata.get(SOLID_HTTP.terms.start)?.value).toBe('-4');
});

it('limits response size to default slice size.', async(): Promise<void> => {
representation.metadata.set(POSIX.terms.size, '10');
const result = await store.getRepresentation(identifier, { range: { unit: 'bytes', parts: [{ start: 0 }]}});
await expect(readableToString(result.data)).resolves.toBe('01234');
expect(result.metadata.get(SOLID_HTTP.terms.unit)?.value).toBe('bytes');
expect(result.metadata.get(SOLID_HTTP.terms.start)?.value).toBe('0');
expect(result.metadata.get(SOLID_HTTP.terms.end)?.value).toBe('4');
});

it('does not go out of range when default slice size extends beyond the resource size.', async(): Promise<void> => {
representation.metadata.set(POSIX.terms.size, '10');
const result = await store.getRepresentation(identifier, { range: { unit: 'bytes', parts: [{ start: 8 }]}});
await expect(readableToString(result.data)).resolves.toBe('89');
expect(result.metadata.get(SOLID_HTTP.terms.unit)?.value).toBe('bytes');
expect(result.metadata.get(SOLID_HTTP.terms.start)?.value).toBe('8');
expect(result.metadata.get(SOLID_HTTP.terms.end)?.value).toBe('9');
});

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');
Expand Down Expand Up @@ -86,4 +104,10 @@ describe('A BinarySliceResourceStore', (): void => {
.rejects.toThrow(RangeNotSatisfiedHttpError);
expect(representation.data.destroy).toHaveBeenCalledTimes(1);
});

it('returns all bytes when a defaultSliceSize is not provided.', async(): Promise<void> => {
store = new BinarySliceResourceStore(source);
const result = await store.getRepresentation(identifier, { range: { unit: 'bytes', parts: [{ start: 0 }]}});
await expect(readableToString(result.data)).resolves.toBe('0123456789');
});
});