storage.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  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 chai = require('chai')
  20. const chaiAsPromised = require('chai-as-promised')
  21. chai.use(chaiAsPromised)
  22. const expect = chai.expect
  23. const fs = require('fs')
  24. const { Storage } = require('@joystream/storage-node-backend')
  25. const IPFS_CID_REGEX = /^Qm[1-9A-HJ-NP-Za-km-z]{44}$/
  26. function write(store, contentId, contents, callback) {
  27. store
  28. .open(contentId, 'w')
  29. .then((stream) => {
  30. stream.on('end', () => {
  31. stream.commit()
  32. })
  33. stream.on('committed', callback)
  34. if (!stream.write(contents)) {
  35. stream.once('drain', () => stream.end())
  36. } else {
  37. process.nextTick(() => stream.end())
  38. }
  39. })
  40. .catch((err) => {
  41. expect.fail(err)
  42. })
  43. }
  44. function readAll(stream) {
  45. return new Promise((resolve, reject) => {
  46. const chunks = []
  47. stream.on('data', (chunk) => chunks.push(chunk))
  48. stream.on('end', () => resolve(Buffer.concat(chunks)))
  49. stream.on('error', (err) => reject(err))
  50. stream.resume()
  51. })
  52. }
  53. function createKnownObject(contentId, contents, callback) {
  54. let hash
  55. const store = Storage.create({
  56. resolve_content_id: () => {
  57. return hash
  58. },
  59. })
  60. write(store, contentId, contents, (theHash) => {
  61. hash = theHash
  62. callback(store, hash)
  63. })
  64. }
  65. describe('storage/storage', () => {
  66. let storage
  67. before(async () => {
  68. storage = await Storage.create({ timeout: 1900 })
  69. })
  70. describe('open()', () => {
  71. it('can write a stream', (done) => {
  72. write(storage, 'foobar', 'test-content', (hash) => {
  73. expect(hash).to.not.be.undefined
  74. expect(hash).to.match(IPFS_CID_REGEX)
  75. done()
  76. })
  77. })
  78. it('detects the MIME type of a write stream', (done) => {
  79. const contents = fs.readFileSync('../../storage-node_new.svg')
  80. storage
  81. .open('mime-test', 'w')
  82. .then((stream) => {
  83. let fileInfo
  84. stream.on('fileInfo', (info) => {
  85. // Could filter & abort here now, but we're just going to set this,
  86. // and expect it to be set later...
  87. fileInfo = info
  88. })
  89. stream.on('end', () => {
  90. stream.info()
  91. })
  92. stream.once('info', async (info) => {
  93. fileInfo = info
  94. stream.commit()
  95. })
  96. stream.on('committed', () => {
  97. // ... if fileInfo is not set here, there's an issue.
  98. expect(fileInfo).to.have.property('mimeType', 'application/xml')
  99. expect(fileInfo).to.have.property('ext', 'xml')
  100. done()
  101. })
  102. if (!stream.write(contents)) {
  103. stream.once('drain', () => stream.end())
  104. } else {
  105. process.nextTick(() => stream.end())
  106. }
  107. })
  108. .catch((err) => {
  109. expect.fail(err)
  110. })
  111. })
  112. it('can read a stream', (done) => {
  113. const contents = 'test-for-reading'
  114. createKnownObject('foobar', contents, (store) => {
  115. store
  116. .open('foobar', 'r')
  117. .then(async (stream) => {
  118. const data = await readAll(stream)
  119. expect(Buffer.compare(data, Buffer.from(contents))).to.equal(0)
  120. done()
  121. })
  122. .catch((err) => {
  123. expect.fail(err)
  124. })
  125. })
  126. })
  127. it('detects the MIME type of a read stream', (done) => {
  128. const contents = fs.readFileSync('../../storage-node_new.svg')
  129. createKnownObject('foobar', contents, (store) => {
  130. store
  131. .open('foobar', 'r')
  132. .then(async (stream) => {
  133. const data = await readAll(stream)
  134. expect(contents.length).to.equal(data.length)
  135. expect(Buffer.compare(data, contents)).to.equal(0)
  136. expect(stream).to.have.property('fileInfo')
  137. // application/xml+svg would be better, but this is good-ish.
  138. expect(stream.fileInfo).to.have.property('mimeType', 'application/xml')
  139. expect(stream.fileInfo).to.have.property('ext', 'xml')
  140. done()
  141. })
  142. .catch((err) => {
  143. expect.fail(err)
  144. })
  145. })
  146. })
  147. it('provides default MIME type for read streams', (done) => {
  148. const contents = 'test-for-reading'
  149. createKnownObject('foobar', contents, (store) => {
  150. store
  151. .open('foobar', 'r')
  152. .then(async (stream) => {
  153. const data = await readAll(stream)
  154. expect(Buffer.compare(data, Buffer.from(contents))).to.equal(0)
  155. expect(stream.fileInfo).to.have.property('mimeType', 'application/octet-stream')
  156. expect(stream.fileInfo).to.have.property('ext', 'bin')
  157. done()
  158. })
  159. .catch((err) => {
  160. expect.fail(err)
  161. })
  162. })
  163. })
  164. })
  165. describe('stat()', () => {
  166. it('times out for unknown content', async () => {
  167. const content = Buffer.from('this-should-not-exist')
  168. const x = await storage.ipfs.add(content, { onlyHash: true })
  169. const hash = x[0].hash
  170. // Try to stat this entry, it should timeout.
  171. expect(storage.stat(hash)).to.eventually.be.rejectedWith('timed out')
  172. })
  173. it('returns stats for a known object', (done) => {
  174. const content = 'stat-test'
  175. const expectedSize = content.length
  176. createKnownObject('foobar', content, (store, hash) => {
  177. expect(store.stat(hash)).to.eventually.have.property('size', expectedSize)
  178. done()
  179. })
  180. })
  181. })
  182. describe('size()', () => {
  183. it('times out for unknown content', async () => {
  184. const content = Buffer.from('this-should-not-exist')
  185. const x = await storage.ipfs.add(content, { onlyHash: true })
  186. const hash = x[0].hash
  187. // Try to stat this entry, it should timeout.
  188. expect(storage.size(hash)).to.eventually.be.rejectedWith('timed out')
  189. })
  190. it('returns the size of a known object', (done) => {
  191. createKnownObject('foobar', 'stat-test', (store, hash) => {
  192. expect(store.size(hash)).to.eventually.equal(15)
  193. done()
  194. })
  195. })
  196. })
  197. })