FSPersistorTests.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. const crypto = require('crypto')
  2. const { expect } = require('chai')
  3. const mockFs = require('mock-fs')
  4. const fs = require('fs')
  5. const fsPromises = require('fs/promises')
  6. const Path = require('path')
  7. const StreamPromises = require('stream/promises')
  8. const SandboxedModule = require('sandboxed-module')
  9. const Errors = require('../../src/Errors')
  10. const MODULE_PATH = '../../src/FSPersistor.js'
  11. describe('FSPersistorTests', function () {
  12. const localFilePath = '/uploads/info.txt'
  13. const localFileContents = Buffer.from('This information is critical', {
  14. encoding: 'utf-8',
  15. })
  16. const uploadFolder = '/tmp'
  17. const location = '/bucket'
  18. const files = {
  19. wombat: 'animals/wombat.tex',
  20. giraffe: 'animals/giraffe.tex',
  21. potato: 'vegetables/potato.tex',
  22. }
  23. const scenarios = [
  24. {
  25. description: 'default settings',
  26. settings: { paths: { uploadFolder } },
  27. fsPath: key => Path.join(location, key.replaceAll('/', '_')),
  28. },
  29. {
  30. description: 'with useSubdirectories = true',
  31. settings: { paths: { uploadFolder }, useSubdirectories: true },
  32. fsPath: key => Path.join(location, key),
  33. },
  34. ]
  35. for (const scenario of scenarios) {
  36. describe(scenario.description, function () {
  37. let persistor
  38. beforeEach(function () {
  39. const FSPersistor = SandboxedModule.require(MODULE_PATH, {
  40. requires: {
  41. 'fs/promises': fsPromises,
  42. 'stream/promises': StreamPromises,
  43. './Errors': Errors,
  44. },
  45. })
  46. persistor = new FSPersistor(scenario.settings)
  47. })
  48. beforeEach(function () {
  49. mockFs({
  50. [localFilePath]: localFileContents,
  51. [location]: {},
  52. '/not-a-dir':
  53. 'This regular file is meant to prevent using this path as a directory',
  54. '/directory/subdirectory': {},
  55. })
  56. })
  57. afterEach(function () {
  58. mockFs.restore()
  59. })
  60. describe('sendFile', function () {
  61. it('should copy the file', async function () {
  62. await persistor.sendFile(location, files.wombat, localFilePath)
  63. const contents = await fsPromises.readFile(
  64. scenario.fsPath(files.wombat)
  65. )
  66. expect(contents.equals(localFileContents)).to.be.true
  67. })
  68. it('should return an error if the file cannot be stored', async function () {
  69. await expect(
  70. persistor.sendFile('/not-a-dir', files.wombat, localFilePath)
  71. ).to.be.rejectedWith(Errors.WriteError)
  72. })
  73. })
  74. describe('sendStream', function () {
  75. let stream
  76. beforeEach(function () {
  77. stream = fs.createReadStream(localFilePath)
  78. })
  79. it('should write the stream to disk', async function () {
  80. await persistor.sendStream(location, files.wombat, stream)
  81. const contents = await fsPromises.readFile(
  82. scenario.fsPath(files.wombat)
  83. )
  84. expect(contents.equals(localFileContents)).to.be.true
  85. })
  86. it('should delete the temporary file', async function () {
  87. await persistor.sendStream(location, files.wombat, stream)
  88. const tempFiles = await fsPromises.readdir(uploadFolder)
  89. expect(tempFiles).to.be.empty
  90. })
  91. it('should wrap the error from the filesystem', async function () {
  92. await expect(
  93. persistor.sendStream('/not-a-dir', files.wombat, stream)
  94. ).to.be.rejectedWith(Errors.WriteError)
  95. })
  96. describe('when the md5 hash matches', function () {
  97. it('should write the stream to disk', async function () {
  98. await persistor.sendStream(location, files.wombat, stream, {
  99. sourceMd5: md5(localFileContents),
  100. })
  101. const contents = await fsPromises.readFile(
  102. scenario.fsPath(files.wombat)
  103. )
  104. expect(contents.equals(localFileContents)).to.be.true
  105. })
  106. })
  107. describe('when the md5 hash does not match', function () {
  108. let promise
  109. beforeEach(function () {
  110. promise = persistor.sendStream(location, files.wombat, stream, {
  111. sourceMd5: md5('wrong content'),
  112. })
  113. })
  114. it('should return a write error', async function () {
  115. await expect(promise).to.be.rejectedWith(
  116. Errors.WriteError,
  117. 'md5 hash mismatch'
  118. )
  119. })
  120. it('deletes the copied file', async function () {
  121. await expect(promise).to.be.rejected
  122. await expect(
  123. fsPromises.access(scenario.fsPath(files.wombat))
  124. ).to.be.rejected
  125. })
  126. })
  127. })
  128. describe('getObjectStream', function () {
  129. beforeEach(async function () {
  130. await persistor.sendFile(location, files.wombat, localFilePath)
  131. })
  132. it('should return a string with the object contents', async function () {
  133. const stream = await persistor.getObjectStream(location, files.wombat)
  134. const contents = await streamToBuffer(stream)
  135. expect(contents.equals(localFileContents)).to.be.true
  136. })
  137. it('should support ranges', async function () {
  138. const stream = await persistor.getObjectStream(
  139. location,
  140. files.wombat,
  141. {
  142. start: 5,
  143. end: 16,
  144. }
  145. )
  146. const contents = await streamToBuffer(stream)
  147. // end is inclusive in ranges, but exclusive in slice()
  148. expect(contents.equals(localFileContents.slice(5, 17))).to.be.true
  149. })
  150. it('should give a NotFoundError if the file does not exist', async function () {
  151. await expect(
  152. persistor.getObjectStream(location, 'does-not-exist')
  153. ).to.be.rejectedWith(Errors.NotFoundError)
  154. })
  155. })
  156. describe('getObjectSize', function () {
  157. beforeEach(async function () {
  158. await persistor.sendFile(location, files.wombat, localFilePath)
  159. })
  160. it('should return the file size', async function () {
  161. expect(
  162. await persistor.getObjectSize(location, files.wombat)
  163. ).to.equal(localFileContents.length)
  164. })
  165. it('should throw a NotFoundError if the file does not exist', async function () {
  166. await expect(
  167. persistor.getObjectSize(location, 'does-not-exist')
  168. ).to.be.rejectedWith(Errors.NotFoundError)
  169. })
  170. })
  171. describe('copyObject', function () {
  172. beforeEach(async function () {
  173. await persistor.sendFile(location, files.wombat, localFilePath)
  174. })
  175. it('Should copy the file to the new location', async function () {
  176. await persistor.copyObject(location, files.wombat, files.potato)
  177. const contents = await fsPromises.readFile(
  178. scenario.fsPath(files.potato)
  179. )
  180. expect(contents.equals(localFileContents)).to.be.true
  181. })
  182. })
  183. describe('deleteObject', function () {
  184. beforeEach(async function () {
  185. await persistor.sendFile(location, files.wombat, localFilePath)
  186. await fsPromises.access(scenario.fsPath(files.wombat))
  187. })
  188. it('should delete the file', async function () {
  189. await persistor.deleteObject(location, files.wombat)
  190. await expect(
  191. fsPromises.access(scenario.fsPath(files.wombat))
  192. ).to.be.rejected
  193. })
  194. it("should ignore files that don't exist", async function () {
  195. await persistor.deleteObject(location, 'does-not-exist')
  196. })
  197. })
  198. describe('deleteDirectory', function () {
  199. beforeEach(async function () {
  200. for (const file of Object.values(files)) {
  201. await persistor.sendFile(location, file, localFilePath)
  202. await fsPromises.access(scenario.fsPath(file))
  203. }
  204. })
  205. it('should delete all files under the directory', async function () {
  206. await persistor.deleteDirectory(location, 'animals')
  207. for (const file of [files.wombat, files.giraffe]) {
  208. await expect(fsPromises.access(scenario.fsPath(file))).to.be
  209. .rejected
  210. }
  211. })
  212. it('should not delete files under other directoris', async function () {
  213. await persistor.deleteDirectory(location, 'animals')
  214. await fsPromises.access(scenario.fsPath(files.potato))
  215. })
  216. it("should ignore directories that don't exist", async function () {
  217. await persistor.deleteDirectory(location, 'does-not-exist')
  218. for (const file of Object.values(files)) {
  219. await fsPromises.access(scenario.fsPath(file))
  220. }
  221. })
  222. })
  223. describe('checkIfObjectExists', function () {
  224. beforeEach(async function () {
  225. await persistor.sendFile(location, files.wombat, localFilePath)
  226. })
  227. it('should return true for existing files', async function () {
  228. expect(
  229. await persistor.checkIfObjectExists(location, files.wombat)
  230. ).to.equal(true)
  231. })
  232. it('should return false for non-existing files', async function () {
  233. expect(
  234. await persistor.checkIfObjectExists(location, 'does-not-exist')
  235. ).to.equal(false)
  236. })
  237. })
  238. describe('directorySize', function () {
  239. beforeEach(async function () {
  240. for (const file of Object.values(files)) {
  241. await persistor.sendFile(location, file, localFilePath)
  242. }
  243. })
  244. it('should sum directory files size', async function () {
  245. expect(await persistor.directorySize(location, 'animals')).to.equal(
  246. 2 * localFileContents.length
  247. )
  248. })
  249. it('should return 0 on non-existing directories', async function () {
  250. expect(
  251. await persistor.directorySize(location, 'does-not-exist')
  252. ).to.equal(0)
  253. })
  254. })
  255. })
  256. }
  257. })
  258. function md5(str) {
  259. return crypto.createHash('md5').update(str).digest('hex')
  260. }
  261. async function streamToBuffer(stream) {
  262. const chunks = []
  263. for await (const chunk of stream) {
  264. chunks.push(chunk)
  265. }
  266. return Buffer.concat(chunks)
  267. }