Skip to content

Commit

Permalink
feat(Server): support for stream range
Browse files Browse the repository at this point in the history
  • Loading branch information
hans00 committed Jul 29, 2022
1 parent 53ce8c6 commit 01303cf
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 29 deletions.
4 changes: 4 additions & 0 deletions docs/Response.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,8 @@ res.status(404).setHeader('TEST', 'CONTENT').writeHead()

```js
fs.createReadStream('/path/to/file').pipe(res)

// OR

res.pipeFrom(fs.createReadStream('/path/to/file')) // It's support Range
```
4 changes: 2 additions & 2 deletions packages/server/js/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ class Request {
return this._accepts.languages()
}

range (size, options) {
return parseRange(size, this.connection.headers.range, options)
range (size) {
return parseRange(size, this.connection.headers.range || '', { combine: true })
}
}

Expand Down
63 changes: 45 additions & 18 deletions packages/server/js/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const fs = require('fs')
const path = require('path')
const mime = require('mime-types')
const { Writable } = require('stream')
const parseRange = require('range-parser')
const rangeStream = require('ranges-stream')

const staticPath = path.resolve(process.cwd(), 'static')

Expand Down Expand Up @@ -69,7 +71,7 @@ class Response extends Writable {
this._status = 200
this._headers = {}
this.headersSent = false
this.on('pipe', (src) => this._pipeFrom(src))
this.on('pipe', (src) => this._pipe(src))
this.on('finish', () => this._end())
this.corkPipe = false
this.connection.onAborted(() => {
Expand Down Expand Up @@ -215,28 +217,34 @@ class Response extends Writable {
this.emit('error', error)
}

_pipeFrom (readable, contentType) {
if (this._writableState.destroyed) {
return
}
if (readable.headers) { // HTTP
this._totalSize = Number(readable.headers['content-length'])
if (!contentType && readable.headers['content-type']) {
contentType = readable.headers['content-type']
_setupStreamMeta (stream) {
if (this._streamMeta) return
this._streamMeta = true
let contentType = this._headers['content-type']
if (stream.headers) { // HTTP
if (!this._totalSize) {
this._totalSize = Number(stream.headers['content-length'])
}
if (!contentType && stream.headers['content-type']) {
contentType = stream.headers['content-type']
}
} else if (stream.path) { // FS
if (!this._totalSize) {
const { size } = fs.statSync(stream.path)
this._totalSize = size
}
} else if (readable.path) { // FS
const { size } = fs.statSync(readable.path)
this._totalSize = size
if (!contentType) {
contentType = mime.lookup(readable.path) || 'application/octet-stream'
contentType = mime.lookup(stream.path) || 'application/octet-stream'
}
} else if (readable.bodyLength) { // Known size body
this._totalSize = readable.bodyLength
} else if (stream.bodyLength) { // Known size body
this._totalSize = stream.bodyLength
}
if (!this.getHeader('content-type') && contentType) {
if (contentType) {
this.setHeader('Content-Type', contentType)
}
readable.on('error', this._pipeError.bind(this))
}

_pipe (stream) {
// In RFC these status code must not have body
if (this._status < 200 || this._status === 204 || this._status === 304) {
throw new ServerError({
Expand All @@ -245,8 +253,27 @@ class Response extends Writable {
httpCode: 500
})
}
this._setupStreamMeta(stream)
this.corkPipe = true
return this
stream.on('error', this._pipeError.bind(this))
}

pipeFrom (stream) {
if (this._writableState.destroyed) {
return
}
this._setupStreamMeta(stream)
const ranges = parseRange(this._totalSize, this.connection.headers.range || '', { combine: true })
// TODO: Support for multipart/byteranges
if (Array.isArray(ranges) && ranges.length === 1 && ranges.type === 'bytes') {
const [{ start, end }] = ranges
if (this._totalSize && end <= this._totalSize) {
this._totalSize = end - start
this._status = 206
return stream.pipe(rangeStream(ranges)).pipe(this)
}
}
return stream.pipe(this)
}

_end () {
Expand Down
1 change: 1 addition & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"multipart-formdata": "^1.1.0",
"qs": "^6.10.3",
"range-parser": "^1.2.1",
"ranges-stream": "^1.0.0",
"replicator": "^1.0.5",
"uWebSockets.js": "https://github.com/uNetworking/uWebSockets.js#binaries"
},
Expand Down
21 changes: 21 additions & 0 deletions test/cases/http-pipe-file-stream-range.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const axios = require('axios')

module.exports = async function ({ HTTP_PORT }) {
const res = await axios.get(`http://localhost:${HTTP_PORT}/stream/file`, {
headers: {
Range: 'bytes=0-4'
}
})
if (res.status !== 206) {
throw new Error(`Response ${res.status}`)
}
if (!res.headers['content-type']) {
throw new Error('Unknown Content-Type')
}
if (Number(res.headers['content-length']) !== 4) {
throw new Error(`Invalid Content-Length ${res.headers['content-length']}`)
}
if (res.data !== 'TEST') {
throw new Error('Response data mismatch')
}
}
2 changes: 1 addition & 1 deletion test/cases/http-pipe-file-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module.exports = async function ({ HTTP_PORT }) {
if (Number(res.headers['content-length']) < 4) {
throw new Error(`Invalid Content-Length ${res.headers['content-length']}`)
}
if (!res.data.startsWith('TEST')) {
if (!res.data.startsWith('TEST_DATA')) {
throw new Error('Response data mismatch')
}
}
3 changes: 2 additions & 1 deletion test/prepare/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ module.exports = function (app) {
})

app.get('/stream/file', (req, res) => {
fs.createReadStream(path.resolve('static/index.html')).pipe(res)
const file = path.resolve('static/index.html')
res.pipeFrom(fs.createReadStream(file))
})

app.get('/stream/http', (req, res) => {
Expand Down
2 changes: 1 addition & 1 deletion test/static/index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
TEST
TEST_DATA
59 changes: 53 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2530,13 +2530,27 @@ dateformat@^3.0.0:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==

debug@2.6.9, debug@4, debug@^2.6.9, debug@^3.2.7, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@~4.3.1, debug@~4.3.2:
debug@2.6.9, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"

debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@~4.3.1, debug@~4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"

debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
dependencies:
ms "^2.1.1"

debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
Expand Down Expand Up @@ -4497,7 +4511,7 @@ make-fetch-happen@^8.0.9:
socks-proxy-agent "^5.0.0"
ssri "^8.0.0"

make-fetch-happen@^9.0.1:
make-fetch-happen@^9.0.1, make-fetch-happen@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968"
integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==
Expand Down Expand Up @@ -4747,7 +4761,12 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==

ms@2.1.3, ms@^2.0.0:
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==

ms@2.1.3, ms@^2.0.0, ms@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
Expand Down Expand Up @@ -4821,7 +4840,23 @@ node-gyp-build@^4.3.0:
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40"
integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==

node-gyp@^8.4.1, node-gyp@^9.0.0:
node-gyp@^8.4.1:
version "8.4.1"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937"
integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==
dependencies:
env-paths "^2.2.0"
glob "^7.1.4"
graceful-fs "^4.2.6"
make-fetch-happen "^9.1.0"
nopt "^5.0.0"
npmlog "^6.0.0"
rimraf "^3.0.2"
semver "^7.3.5"
tar "^6.1.2"
which "^2.0.2"

node-gyp@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.0.0.tgz#e1da2067427f3eb5bb56820cb62bc6b1e4bd2089"
integrity sha512-Ma6p4s+XCTPxCuAMrOA/IJRmVy16R8Sdhtwl4PrCr7IBlj4cPawF0vg/l7nOT1jPbuNS7lIRJpBSvVsXwEZuzw==
Expand Down Expand Up @@ -5574,6 +5609,13 @@ range-parser@^1.2.1, range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==

ranges-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ranges-stream/-/ranges-stream-1.0.0.tgz#7d304c513d6a017018818a9d51e815c8645e9035"
integrity sha512-wQ2WoBcfqJuUVNE09lOHCa12ttGo0kBybgEBqoqw19sISdZh/FD7bpWuwA/5hCm7Z4rcJP5Qwaswk4YZwFkgdw==
dependencies:
through2 "^2.0.0"

raw-body@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
Expand Down Expand Up @@ -6366,7 +6408,7 @@ treeverse@^2.0.0:
resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-2.0.0.tgz#036dcef04bc3fd79a9b79a68d4da03e882d8a9ca"
integrity sha512-N5gJCkLu1aXccpOTtqV6ddSEi6ZmGkh3hjmbu1IjcavJK4qyOVQmi0myQKM7z5jVGmD68SJoliaVrMmVObhj6A==

trim-newlines@^3.0.0, trim-newlines@^3.0.1:
trim-newlines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
Expand Down Expand Up @@ -6549,7 +6591,12 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==

uuid@^3.3.2, uuid@^8.3.2:
uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==

uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
Expand Down

0 comments on commit 01303cf

Please sign in to comment.