FSPersistorTests.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. const sinon = require('sinon')
  2. const chai = require('chai')
  3. const { expect } = chai
  4. const SandboxedModule = require('sandboxed-module')
  5. const Errors = require('../../src/Errors')
  6. const StreamModule = require('stream')
  7. const modulePath = '../../src/FSPersistor.js'
  8. describe('FSPersistorTests', function () {
  9. const stat = { size: 4, isFile: sinon.stub().returns(true) }
  10. const fd = 1234
  11. const writeStream = 'writeStream'
  12. const remoteStream = 'remoteStream'
  13. const location = '/foo'
  14. const error = new Error('guru meditation error')
  15. const md5 = 'ffffffff'
  16. const files = ['animals/wombat.tex', 'vegetables/potato.tex']
  17. const globs = [`${location}/${files[0]}`, `${location}/${files[1]}`]
  18. const filteredFilenames = ['animals_wombat.tex', 'vegetables_potato.tex']
  19. let fs, stream, FSPersistor, glob, readStream, crypto, Hash, uuid, tempFile
  20. beforeEach(function () {
  21. const randomNumber = Math.random().toString()
  22. readStream = {
  23. name: 'readStream',
  24. on: sinon.stub().yields(),
  25. pipe: sinon.stub()
  26. }
  27. uuid = {
  28. v1: () => randomNumber
  29. }
  30. tempFile = `/tmp/${randomNumber}`
  31. fs = {
  32. createReadStream: sinon.stub().returns(readStream),
  33. createWriteStream: sinon.stub().returns(writeStream),
  34. unlink: sinon.stub().yields(),
  35. open: sinon.stub().yields(null, fd),
  36. stat: sinon.stub().yields(null, stat)
  37. }
  38. glob = sinon.stub().yields(null, globs)
  39. stream = {
  40. pipeline: sinon.stub().yields(),
  41. Transform: StreamModule.Transform
  42. }
  43. Hash = {
  44. end: sinon.stub(),
  45. read: sinon.stub().returns(md5),
  46. digest: sinon.stub().returns(md5),
  47. setEncoding: sinon.stub()
  48. }
  49. crypto = {
  50. createHash: sinon.stub().returns(Hash)
  51. }
  52. FSPersistor = new (SandboxedModule.require(modulePath, {
  53. requires: {
  54. './Errors': Errors,
  55. fs,
  56. glob,
  57. stream,
  58. crypto,
  59. 'node-uuid': uuid,
  60. // imported by PersistorHelper but otherwise unused here
  61. 'logger-sharelatex': {}
  62. },
  63. globals: { console }
  64. }))({ paths: { uploadFolder: '/tmp' } })
  65. })
  66. describe('sendFile', function () {
  67. const localFilesystemPath = '/path/to/local/file'
  68. it('should copy the file', async function () {
  69. await FSPersistor.sendFile(location, files[0], localFilesystemPath)
  70. expect(fs.createReadStream).to.have.been.calledWith(localFilesystemPath)
  71. expect(fs.createWriteStream).to.have.been.calledWith(
  72. `${location}/${filteredFilenames[0]}`
  73. )
  74. expect(stream.pipeline).to.have.been.calledWith(readStream, writeStream)
  75. })
  76. it('should return an error if the file cannot be stored', async function () {
  77. stream.pipeline.yields(error)
  78. await expect(
  79. FSPersistor.sendFile(location, files[0], localFilesystemPath)
  80. ).to.eventually.be.rejected.and.have.property('cause', error)
  81. })
  82. })
  83. describe('sendStream', function () {
  84. it('should write the stream to disk', async function () {
  85. await FSPersistor.sendStream(location, files[0], remoteStream)
  86. expect(stream.pipeline).to.have.been.calledWith(remoteStream, writeStream)
  87. })
  88. it('should delete the temporary file', async function () {
  89. await FSPersistor.sendStream(location, files[0], remoteStream)
  90. expect(fs.unlink).to.have.been.calledWith(tempFile)
  91. })
  92. it('should wrap the error from the filesystem', async function () {
  93. stream.pipeline.yields(error)
  94. await expect(FSPersistor.sendStream(location, files[0], remoteStream))
  95. .to.eventually.be.rejected.and.be.instanceOf(Errors.WriteError)
  96. .and.have.property('cause', error)
  97. })
  98. it('should send the temporary file to the filestore', async function () {
  99. await FSPersistor.sendStream(location, files[0], remoteStream)
  100. expect(fs.createReadStream).to.have.been.calledWith(tempFile)
  101. })
  102. describe('when the md5 hash does not match', function () {
  103. it('should return a write error', async function () {
  104. await expect(
  105. FSPersistor.sendStream(location, files[0], remoteStream, '00000000')
  106. )
  107. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.WriteError)
  108. .and.have.property('message', 'md5 hash mismatch')
  109. })
  110. it('deletes the copied file', async function () {
  111. try {
  112. await FSPersistor.sendStream(
  113. location,
  114. files[0],
  115. remoteStream,
  116. '00000000'
  117. )
  118. } catch (_) {}
  119. expect(fs.unlink).to.have.been.calledWith(
  120. `${location}/${filteredFilenames[0]}`
  121. )
  122. })
  123. })
  124. })
  125. describe('getObjectStream', function () {
  126. it('should use correct file location', async function () {
  127. await FSPersistor.getObjectStream(location, files[0], {})
  128. expect(fs.open).to.have.been.calledWith(
  129. `${location}/${filteredFilenames[0]}`
  130. )
  131. })
  132. it('should pass the options to createReadStream', async function () {
  133. await FSPersistor.getObjectStream(location, files[0], {
  134. start: 0,
  135. end: 8
  136. })
  137. expect(fs.createReadStream).to.have.been.calledWith(null, {
  138. start: 0,
  139. end: 8,
  140. fd
  141. })
  142. })
  143. it('should give a NotFoundError if the file does not exist', async function () {
  144. const err = new Error()
  145. err.code = 'ENOENT'
  146. fs.open.yields(err)
  147. await expect(FSPersistor.getObjectStream(location, files[0], {}))
  148. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
  149. .and.have.property('cause', err)
  150. })
  151. it('should wrap any other error', async function () {
  152. fs.open.yields(error)
  153. await expect(FSPersistor.getObjectStream(location, files[0], {}))
  154. .to.eventually.be.rejectedWith('failed to open file for streaming')
  155. .and.be.an.instanceOf(Errors.ReadError)
  156. .and.have.property('cause', error)
  157. })
  158. })
  159. describe('getObjectSize', function () {
  160. const badFilename = 'neenaw.tex'
  161. const size = 65536
  162. const noentError = new Error('not found')
  163. noentError.code = 'ENOENT'
  164. beforeEach(function () {
  165. fs.stat
  166. .yields(error)
  167. .withArgs(`${location}/${filteredFilenames[0]}`)
  168. .yields(null, { size })
  169. .withArgs(`${location}/${badFilename}`)
  170. .yields(noentError)
  171. })
  172. it('should return the file size', async function () {
  173. expect(await FSPersistor.getObjectSize(location, files[0])).to.equal(size)
  174. })
  175. it('should throw a NotFoundError if the file does not exist', async function () {
  176. await expect(
  177. FSPersistor.getObjectSize(location, badFilename)
  178. ).to.eventually.be.rejected.and.be.an.instanceOf(Errors.NotFoundError)
  179. })
  180. it('should wrap any other error', async function () {
  181. await expect(FSPersistor.getObjectSize(location, 'raccoon'))
  182. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
  183. .and.have.property('cause', error)
  184. })
  185. })
  186. describe('copyObject', function () {
  187. it('Should open the source for reading', async function () {
  188. await FSPersistor.copyObject(location, files[0], files[1])
  189. expect(fs.createReadStream).to.have.been.calledWith(
  190. `${location}/${filteredFilenames[0]}`
  191. )
  192. })
  193. it('Should open the target for writing', async function () {
  194. await FSPersistor.copyObject(location, files[0], files[1])
  195. expect(fs.createWriteStream).to.have.been.calledWith(
  196. `${location}/${filteredFilenames[1]}`
  197. )
  198. })
  199. it('Should pipe the source to the target', async function () {
  200. await FSPersistor.copyObject(location, files[0], files[1])
  201. expect(stream.pipeline).to.have.been.calledWith(readStream, writeStream)
  202. })
  203. })
  204. describe('deleteObject', function () {
  205. it('Should call unlink with correct options', async function () {
  206. await FSPersistor.deleteObject(location, files[0])
  207. expect(fs.unlink).to.have.been.calledWith(
  208. `${location}/${filteredFilenames[0]}`
  209. )
  210. })
  211. it('Should propagate the error', async function () {
  212. fs.unlink.yields(error)
  213. await expect(
  214. FSPersistor.deleteObject(location, files[0])
  215. ).to.eventually.be.rejected.and.have.property('cause', error)
  216. })
  217. })
  218. describe('deleteDirectory', function () {
  219. it('Should call glob with correct options', async function () {
  220. await FSPersistor.deleteDirectory(location, files[0])
  221. expect(glob).to.have.been.calledWith(
  222. `${location}/${filteredFilenames[0]}_*`
  223. )
  224. })
  225. it('Should call unlink on the returned files', async function () {
  226. await FSPersistor.deleteDirectory(location, files[0])
  227. for (const filename of globs) {
  228. expect(fs.unlink).to.have.been.calledWith(filename)
  229. }
  230. })
  231. it('Should propagate the error', async function () {
  232. glob.yields(error)
  233. await expect(
  234. FSPersistor.deleteDirectory(location, files[0])
  235. ).to.eventually.be.rejected.and.have.property('cause', error)
  236. })
  237. })
  238. describe('checkIfObjectExists', function () {
  239. const badFilename = 'pototo'
  240. const noentError = new Error('not found')
  241. noentError.code = 'ENOENT'
  242. beforeEach(function () {
  243. fs.stat
  244. .yields(error)
  245. .withArgs(`${location}/${filteredFilenames[0]}`)
  246. .yields(null, {})
  247. .withArgs(`${location}/${badFilename}`)
  248. .yields(noentError)
  249. })
  250. it('Should call stat with correct options', async function () {
  251. await FSPersistor.checkIfObjectExists(location, files[0])
  252. expect(fs.stat).to.have.been.calledWith(
  253. `${location}/${filteredFilenames[0]}`
  254. )
  255. })
  256. it('Should return true for existing files', async function () {
  257. expect(
  258. await FSPersistor.checkIfObjectExists(location, files[0])
  259. ).to.equal(true)
  260. })
  261. it('Should return false for non-existing files', async function () {
  262. expect(
  263. await FSPersistor.checkIfObjectExists(location, badFilename)
  264. ).to.equal(false)
  265. })
  266. it('should wrap the error if there is a problem', async function () {
  267. await expect(FSPersistor.checkIfObjectExists(location, 'llama'))
  268. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
  269. .and.have.property('cause', error)
  270. })
  271. })
  272. describe('directorySize', function () {
  273. it('should wrap the error', async function () {
  274. glob.yields(error)
  275. await expect(FSPersistor.directorySize(location, files[0]))
  276. .to.eventually.be.rejected.and.be.an.instanceOf(Errors.ReadError)
  277. .and.include({ cause: error })
  278. .and.have.property('info')
  279. .which.includes({ location, name: files[0] })
  280. })
  281. it('should filter the directory name', async function () {
  282. await FSPersistor.directorySize(location, files[0])
  283. expect(glob).to.have.been.calledWith(
  284. `${location}/${filteredFilenames[0]}_*`
  285. )
  286. })
  287. it('should sum directory files size', async function () {
  288. expect(await FSPersistor.directorySize(location, files[0])).to.equal(
  289. stat.size * files.length
  290. )
  291. })
  292. })
  293. })