Skip to content

Commit

Permalink
Added range handling (#40)
Browse files Browse the repository at this point in the history
* Initial range request handling

* Document the change
  • Loading branch information
mischnic authored and leo committed Aug 14, 2018
1 parent 48d0eb1 commit 11ea13f
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 8 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,14 @@ These are the methods used by the package (they can all return a `Promise` or be
```js
await handler(request, response, undefined, {
stat(path) {},
createReadStream(path) {},
createReadStream(path, config) {}
readdir(path) {},
sendError(absolutePath, response, acceptsJSON, root, handlers, config, error) {}
});
```

**NOTE:** It's important that – for native methods like `createReadStream` – all arguments are passed on to the native call.

## Use Cases

There are two environments in which [ZEIT](https://zeit.co) uses this package:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"mime-types": "2.1.18",
"minimatch": "3.0.4",
"path-is-inside": "1.0.2",
"path-to-regexp": "2.2.1"
"path-to-regexp": "2.2.1",
"range-parser": "1.2.0"
}
}
32 changes: 30 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const mime = require('mime-types');
const bytes = require('bytes');
const contentDisposition = require('content-disposition');
const isPathInside = require('path-is-inside');
const parseRange = require('range-parser');

// Other
const directoryTemplate = require('./directory');
Expand Down Expand Up @@ -204,7 +205,8 @@ const getHeaders = async (customHeaders = [], current, absolutePath, stats) => {
// only happens if it cannot find a appropiate value.
'Content-Disposition': contentDisposition(base, {
type: 'inline'
})
}),
'Accept-Ranges': 'bytes'
};

const contentType = mime.contentType(base);
Expand Down Expand Up @@ -644,9 +646,35 @@ module.exports = async (request, response, config = {}, methods = {}) => {
});
}

const stream = await handlers.createReadStream(absolutePath);
const streamOpts = {};

// TODO ? if-range
if (request.headers.range) {
const range = parseRange(stats.size, request.headers.range);

if (typeof range === 'object' && range.type === 'bytes') {
const {start, end} = range[0];
streamOpts.start = start;
streamOpts.end = end;

response.statusCode = 206;
} else {
response.statusCode = 416;
response.setHeader('Content-Range', `bytes */${stats.size}`);
}
}

// TODO ? multiple ranges

const stream = await handlers.createReadStream(absolutePath, streamOpts);
const headers = await getHeaders(config.headers, current, absolutePath, stats);

// eslint-disable-next-line no-undefined
if (streamOpts.start !== undefined && streamOpts.end !== undefined) {
headers['Content-Range'] = `bytes ${streamOpts.start}-${streamOpts.end}/${stats.size}`;
headers['Content-Length'] = streamOpts.end - streamOpts.start + 1;
}

// We need to check for `headers.ETag` being truthy first, otherwise it will
// match `undefined` being equal to `undefined`, which is true.
//
Expand Down
54 changes: 50 additions & 4 deletions test/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -888,9 +888,9 @@ test('set `createReadStream` handler to async function', async t => {

// eslint-disable-next-line no-undefined
const url = await getUrl(undefined, {
createReadStream: async file => {
createReadStream: async (file, opts) => {
await sleep(2000);
return fs.createReadStream(file);
return fs.createReadStream(file, opts);
}
});

Expand Down Expand Up @@ -1032,13 +1032,13 @@ test('modify config in `createReadStream` handler', async t => {
};

const url = await getUrl(config, {
createReadStream: async file => {
createReadStream: async (file, opts) => {
config.headers.unshift({
source: name,
headers: [header]
});

return fs.createReadStream(file);
return fs.createReadStream(file, opts);
}
});

Expand Down Expand Up @@ -1088,3 +1088,49 @@ test('automatically handle ETag headers for normal files', async t => {

t.is(cacheResponse.status, 304);
});

test('range request', async t => {
const name = 'docs.md';
const related = path.join(fixturesFull, name);

const content = await fs.readFile(related);
const url = await getUrl();
const response = await fetch(`${url}/${name}`, {
headers: {
Range: 'bytes=0-10'
}
});

const range = response.headers.get('content-range');
const length = Number(response.headers.get('content-length'));
t.is(range, `bytes 0-10/${content.length}`);
t.is(length, 11);
t.is(response.status, 206);

const text = await response.text();
const spec = content.toString().substr(0, 11);
t.is(text, spec);
});

test('range request not satisfiable', async t => {
const name = 'docs.md';
const related = path.join(fixturesFull, name);

const content = await fs.readFile(related);
const url = await getUrl();
const response = await fetch(`${url}/${name}`, {
headers: {
Range: 'bytes=10-1'
}
});

const range = response.headers.get('content-range');
const length = Number(response.headers.get('content-length'));
t.is(range, `bytes */${content.length}`);
t.is(length, content.length);
t.is(response.status, 416);

const text = await response.text();
const spec = content.toString();
t.is(text, spec);
});
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3228,6 +3228,10 @@ randomatic@^3.0.0:
kind-of "^6.0.0"
math-random "^1.0.1"

[email protected]:
version "1.2.0"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"

[email protected]:
version "2.3.2"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"
Expand Down

0 comments on commit 11ea13f

Please sign in to comment.