ranges.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. /*
  2. * This file is part of the storage node for the Joystream project.
  3. * Copyright (C) 2019 Joystream Contributors
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. 'use strict'
  19. const expect = require('chai').expect
  20. const mockHttp = require('node-mocks-http')
  21. const streamBuffers = require('stream-buffers')
  22. const ranges = require('@joystream/storage-utils/ranges')
  23. describe('util/ranges', function () {
  24. describe('parse()', function () {
  25. it('should parse a full range', function () {
  26. // Range with unit
  27. let range = ranges.parse('bytes=0-100')
  28. expect(range.unit).to.equal('bytes')
  29. expect(range.rangeStr).to.equal('0-100')
  30. expect(range.ranges[0][0]).to.equal(0)
  31. expect(range.ranges[0][1]).to.equal(100)
  32. // Range without unit
  33. range = ranges.parse('0-100')
  34. expect(range.unit).to.equal('bytes')
  35. expect(range.rangeStr).to.equal('0-100')
  36. expect(range.ranges[0][0]).to.equal(0)
  37. expect(range.ranges[0][1]).to.equal(100)
  38. // Range with custom unit
  39. //
  40. range = ranges.parse('foo=0-100')
  41. expect(range.unit).to.equal('foo')
  42. expect(range.rangeStr).to.equal('0-100')
  43. expect(range.ranges[0][0]).to.equal(0)
  44. expect(range.ranges[0][1]).to.equal(100)
  45. })
  46. it('should error out on malformed strings', function () {
  47. expect(() => ranges.parse('foo')).to.throw()
  48. expect(() => ranges.parse('foo=bar')).to.throw()
  49. expect(() => ranges.parse('foo=100')).to.throw()
  50. expect(() => ranges.parse('foo=100-0')).to.throw()
  51. })
  52. it('should parse a range without end', function () {
  53. const range = ranges.parse('0-')
  54. expect(range.unit).to.equal('bytes')
  55. expect(range.rangeStr).to.equal('0-')
  56. expect(range.ranges[0][0]).to.equal(0)
  57. expect(range.ranges[0][1]).to.be.undefined
  58. })
  59. it('should parse a range without start', function () {
  60. const range = ranges.parse('-100')
  61. expect(range.unit).to.equal('bytes')
  62. expect(range.rangeStr).to.equal('-100')
  63. expect(range.ranges[0][0]).to.be.undefined
  64. expect(range.ranges[0][1]).to.equal(100)
  65. })
  66. it('should parse multiple ranges', function () {
  67. const range = ranges.parse('0-10,30-40,60-80')
  68. expect(range.unit).to.equal('bytes')
  69. expect(range.rangeStr).to.equal('0-10,30-40,60-80')
  70. expect(range.ranges[0][0]).to.equal(0)
  71. expect(range.ranges[0][1]).to.equal(10)
  72. expect(range.ranges[1][0]).to.equal(30)
  73. expect(range.ranges[1][1]).to.equal(40)
  74. expect(range.ranges[2][0]).to.equal(60)
  75. expect(range.ranges[2][1]).to.equal(80)
  76. })
  77. it('should merge overlapping ranges', function () {
  78. // Two overlapping ranges
  79. let range = ranges.parse('0-20,10-30')
  80. expect(range.unit).to.equal('bytes')
  81. expect(range.rangeStr).to.equal('0-20,10-30')
  82. expect(range.ranges).to.have.lengthOf(1)
  83. expect(range.ranges[0][0]).to.equal(0)
  84. expect(range.ranges[0][1]).to.equal(30)
  85. // Three overlapping ranges
  86. range = ranges.parse('0-15,10-25,20-30')
  87. expect(range.unit).to.equal('bytes')
  88. expect(range.rangeStr).to.equal('0-15,10-25,20-30')
  89. expect(range.ranges).to.have.lengthOf(1)
  90. expect(range.ranges[0][0]).to.equal(0)
  91. expect(range.ranges[0][1]).to.equal(30)
  92. // Three overlapping ranges, reverse order
  93. range = ranges.parse('20-30,10-25,0-15')
  94. expect(range.unit).to.equal('bytes')
  95. expect(range.rangeStr).to.equal('20-30,10-25,0-15')
  96. expect(range.ranges).to.have.lengthOf(1)
  97. expect(range.ranges[0][0]).to.equal(0)
  98. expect(range.ranges[0][1]).to.equal(30)
  99. // Adjacent ranges
  100. range = ranges.parse('0-10,11-20')
  101. expect(range.unit).to.equal('bytes')
  102. expect(range.rangeStr).to.equal('0-10,11-20')
  103. expect(range.ranges).to.have.lengthOf(1)
  104. expect(range.ranges[0][0]).to.equal(0)
  105. expect(range.ranges[0][1]).to.equal(20)
  106. })
  107. it('should sort ranges', function () {
  108. const range = ranges.parse('10-30,0-5')
  109. expect(range.unit).to.equal('bytes')
  110. expect(range.rangeStr).to.equal('10-30,0-5')
  111. expect(range.ranges).to.have.lengthOf(2)
  112. expect(range.ranges[0][0]).to.equal(0)
  113. expect(range.ranges[0][1]).to.equal(5)
  114. expect(range.ranges[1][0]).to.equal(10)
  115. expect(range.ranges[1][1]).to.equal(30)
  116. })
  117. })
  118. describe('send()', function () {
  119. it('should send full files on request', function (done) {
  120. const res = mockHttp.createResponse({})
  121. const inStream = new streamBuffers.ReadableStreamBuffer({})
  122. // End-of-stream callback
  123. const opts = {
  124. name: 'test.file',
  125. type: 'application/test',
  126. }
  127. ranges.send(res, inStream, opts, function (err) {
  128. expect(err).to.not.exist
  129. // HTTP handling
  130. expect(res.statusCode).to.equal(200)
  131. expect(res.getHeader('content-type')).to.equal('application/test')
  132. expect(res.getHeader('content-disposition')).to.equal('inline')
  133. // Data/stream handling
  134. expect(res._isEndCalled()).to.be.true
  135. expect(res._getBuffer().toString()).to.equal('Hello, world!')
  136. // Notify mocha that we're done.
  137. done()
  138. })
  139. // Simulate file stream
  140. inStream.emit('open')
  141. inStream.put('Hello, world!')
  142. inStream.stop()
  143. })
  144. it('should send a range spanning the entire file on request', function (done) {
  145. const res = mockHttp.createResponse({})
  146. const inStream = new streamBuffers.ReadableStreamBuffer({})
  147. // End-of-stream callback
  148. const opts = {
  149. name: 'test.file',
  150. type: 'application/test',
  151. ranges: {
  152. ranges: [[0, 12]],
  153. },
  154. }
  155. ranges.send(res, inStream, opts, function (err) {
  156. expect(err).to.not.exist
  157. // HTTP handling
  158. expect(res.statusCode).to.equal(206)
  159. expect(res.getHeader('content-type')).to.equal('application/test')
  160. expect(res.getHeader('content-disposition')).to.equal('inline')
  161. expect(res.getHeader('content-range')).to.equal('bytes 0-12/*')
  162. expect(res.getHeader('content-length')).to.equal('13')
  163. // Data/stream handling
  164. expect(res._isEndCalled()).to.be.true
  165. expect(res._getBuffer().toString()).to.equal('Hello, world!')
  166. // Notify mocha that we're done.
  167. done()
  168. })
  169. // Simulate file stream
  170. inStream.emit('open')
  171. inStream.put('Hello, world!')
  172. inStream.stop()
  173. })
  174. it('should send a small range on request', function (done) {
  175. const res = mockHttp.createResponse({})
  176. const inStream = new streamBuffers.ReadableStreamBuffer({})
  177. // End-of-stream callback
  178. const opts = {
  179. name: 'test.file',
  180. type: 'application/test',
  181. ranges: {
  182. ranges: [[1, 11]], // Cut off first and last letter
  183. },
  184. }
  185. ranges.send(res, inStream, opts, function (err) {
  186. expect(err).to.not.exist
  187. // HTTP handling
  188. expect(res.statusCode).to.equal(206)
  189. expect(res.getHeader('content-type')).to.equal('application/test')
  190. expect(res.getHeader('content-disposition')).to.equal('inline')
  191. expect(res.getHeader('content-range')).to.equal('bytes 1-11/*')
  192. expect(res.getHeader('content-length')).to.equal('11')
  193. // Data/stream handling
  194. expect(res._isEndCalled()).to.be.true
  195. expect(res._getBuffer().toString()).to.equal('ello, world')
  196. // Notify mocha that we're done.
  197. done()
  198. })
  199. // Simulate file stream
  200. inStream.emit('open')
  201. inStream.put('Hello, world!')
  202. inStream.stop()
  203. })
  204. it('should send ranges crossing buffer boundaries', function (done) {
  205. const res = mockHttp.createResponse({})
  206. const inStream = new streamBuffers.ReadableStreamBuffer({
  207. chunkSize: 3, // Setting a chunk size smaller than the range should
  208. // not impact the test.
  209. })
  210. // End-of-stream callback
  211. const opts = {
  212. name: 'test.file',
  213. type: 'application/test',
  214. ranges: {
  215. ranges: [[1, 11]], // Cut off first and last letter
  216. },
  217. }
  218. ranges.send(res, inStream, opts, function (err) {
  219. expect(err).to.not.exist
  220. // HTTP handling
  221. expect(res.statusCode).to.equal(206)
  222. expect(res.getHeader('content-type')).to.equal('application/test')
  223. expect(res.getHeader('content-disposition')).to.equal('inline')
  224. expect(res.getHeader('content-range')).to.equal('bytes 1-11/*')
  225. expect(res.getHeader('content-length')).to.equal('11')
  226. // Data/stream handling
  227. expect(res._isEndCalled()).to.be.true
  228. expect(res._getBuffer().toString()).to.equal('ello, world')
  229. // Notify mocha that we're done.
  230. done()
  231. })
  232. // Simulate file stream
  233. inStream.emit('open')
  234. inStream.put('Hello, world!')
  235. inStream.stop()
  236. })
  237. it('should send multiple ranges', function (done) {
  238. const res = mockHttp.createResponse({})
  239. const inStream = new streamBuffers.ReadableStreamBuffer({})
  240. // End-of-stream callback
  241. const opts = {
  242. name: 'test.file',
  243. type: 'application/test',
  244. ranges: {
  245. ranges: [
  246. [1, 3],
  247. [5, 7],
  248. ], // Slice two ranges out
  249. },
  250. }
  251. ranges.send(res, inStream, opts, function (err) {
  252. expect(err).to.not.exist
  253. // HTTP handling
  254. expect(res.statusCode).to.equal(206)
  255. expect(res.getHeader('content-type')).to.satisfy((str) => str.startsWith('multipart/byteranges'))
  256. expect(res.getHeader('content-disposition')).to.equal('inline')
  257. // Data/stream handling
  258. expect(res._isEndCalled()).to.be.true
  259. // The buffer should contain both ranges, but with all the That would be
  260. // "ell" and ", w".
  261. // It's pretty elaborate having to parse the entire multipart response
  262. // body, so we'll restrict ourselves to finding lines within it.
  263. const body = res._getBuffer().toString()
  264. expect(body).to.contain('\r\nContent-Range: bytes 1-3/*\r\n')
  265. expect(body).to.contain('\r\nell\r\n')
  266. expect(body).to.contain('\r\nContent-Range: bytes 5-7/*\r\n')
  267. expect(body).to.contain('\r\n, w')
  268. // Notify mocha that we're done.
  269. done()
  270. })
  271. // Simulate file stream
  272. inStream.emit('open')
  273. inStream.put('Hello, world!')
  274. inStream.stop()
  275. })
  276. it('should deal with ranges without end', function (done) {
  277. const res = mockHttp.createResponse({})
  278. const inStream = new streamBuffers.ReadableStreamBuffer({})
  279. // End-of-stream callback
  280. const opts = {
  281. name: 'test.file',
  282. type: 'application/test',
  283. ranges: {
  284. ranges: [[5, undefined]], // Skip the first part, but read until end
  285. },
  286. }
  287. ranges.send(res, inStream, opts, function (err) {
  288. expect(err).to.not.exist
  289. // HTTP handling
  290. expect(res.statusCode).to.equal(206)
  291. expect(res.getHeader('content-type')).to.equal('application/test')
  292. expect(res.getHeader('content-disposition')).to.equal('inline')
  293. expect(res.getHeader('content-range')).to.equal('bytes 5-/*')
  294. // Data/stream handling
  295. expect(res._isEndCalled()).to.be.true
  296. expect(res._getBuffer().toString()).to.equal(', world!')
  297. // Notify mocha that we're done.
  298. done()
  299. })
  300. // Simulate file stream
  301. inStream.emit('open')
  302. inStream.put('Hello, world!')
  303. inStream.stop()
  304. })
  305. it('should ignore ranges without start', function (done) {
  306. const res = mockHttp.createResponse({})
  307. const inStream = new streamBuffers.ReadableStreamBuffer({})
  308. // End-of-stream callback
  309. const opts = {
  310. name: 'test.file',
  311. type: 'application/test',
  312. ranges: {
  313. ranges: [[undefined, 5]], // Only last five
  314. },
  315. }
  316. ranges.send(res, inStream, opts, function (err) {
  317. expect(err).to.not.exist
  318. // HTTP handling
  319. expect(res.statusCode).to.equal(200)
  320. expect(res.getHeader('content-type')).to.equal('application/test')
  321. expect(res.getHeader('content-disposition')).to.equal('inline')
  322. // Data/stream handling
  323. expect(res._isEndCalled()).to.be.true
  324. expect(res._getBuffer().toString()).to.equal('Hello, world!')
  325. // Notify mocha that we're done.
  326. done()
  327. })
  328. // Simulate file stream
  329. inStream.emit('open')
  330. inStream.put('Hello, world!')
  331. inStream.stop()
  332. })
  333. })
  334. })