FilestoreTests.js 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564
  1. const chai = require('chai')
  2. const { expect } = chai
  3. const fs = require('node:fs')
  4. const Stream = require('node:stream')
  5. const Settings = require('@overleaf/settings')
  6. const Path = require('node:path')
  7. const FilestoreApp = require('./FilestoreApp')
  8. const TestHelper = require('./TestHelper')
  9. const fetch = require('node-fetch')
  10. const { promisify } = require('node:util')
  11. const { Storage } = require('@google-cloud/storage')
  12. const streamifier = require('streamifier')
  13. chai.use(require('chai-as-promised'))
  14. const { ObjectId } = require('mongodb')
  15. const ChildProcess = require('node:child_process')
  16. const fsWriteFile = promisify(fs.writeFile)
  17. const fsStat = promisify(fs.stat)
  18. const exec = promisify(ChildProcess.exec)
  19. const msleep = promisify(setTimeout)
  20. if (!process.env.AWS_ACCESS_KEY_ID) {
  21. throw new Error('please provide credentials for the AWS S3 test server')
  22. }
  23. process.on('unhandledRejection', e => {
  24. // eslint-disable-next-line no-console
  25. console.log('** Unhandled Promise Rejection **\n', e)
  26. throw e
  27. })
  28. // store settings for multiple backends, so that we can test each one.
  29. // fs will always be available - add others if they are configured
  30. const {
  31. BackendSettings,
  32. s3Config,
  33. s3SSECConfig,
  34. AWS_S3_USER_FILES_STORAGE_CLASS,
  35. } = require('./TestConfig')
  36. const {
  37. AlreadyWrittenError,
  38. NotFoundError,
  39. NotImplementedError,
  40. NoKEKMatchedError,
  41. } = require('@overleaf/object-persistor/src/Errors')
  42. const {
  43. PerProjectEncryptedS3Persistor,
  44. RootKeyEncryptionKey,
  45. } = require('@overleaf/object-persistor/src/PerProjectEncryptedS3Persistor')
  46. const { S3Persistor } = require('@overleaf/object-persistor/src/S3Persistor')
  47. const crypto = require('node:crypto')
  48. const { WritableBuffer } = require('@overleaf/stream-utils')
  49. const { gzipSync } = require('node:zlib')
  50. describe('Filestore', function () {
  51. this.timeout(1000 * 10)
  52. const filestoreUrl = `http://127.0.0.1:${Settings.internal.filestore.port}`
  53. const seenSockets = []
  54. async function expectNoSockets() {
  55. try {
  56. await msleep(1000)
  57. const { stdout } = await exec('ss -tn')
  58. const lines = stdout.split('\n')
  59. const header = lines.shift()
  60. const badSockets = []
  61. for (const socket of lines) {
  62. const fields = socket.split(' ').filter(part => part !== '')
  63. if (
  64. fields.length > 2 &&
  65. parseInt(fields[1]) &&
  66. !seenSockets.includes(socket)
  67. ) {
  68. badSockets.push(socket)
  69. seenSockets.push(socket)
  70. }
  71. }
  72. if (badSockets.length) {
  73. // eslint-disable-next-line no-console
  74. console.error(
  75. 'ERR: Sockets still have receive buffer after connection closed'
  76. )
  77. console.error(header)
  78. for (const socket of badSockets) {
  79. // eslint-disable-next-line no-console
  80. console.error(socket)
  81. }
  82. throw new Error('Sockets still open after connection closed')
  83. }
  84. } catch (err) {
  85. expect(err).not.to.exist
  86. }
  87. }
  88. // redefine the test suite for every available backend
  89. for (const [backendVariantWithShardNumber, backendSettings] of Object.entries(
  90. BackendSettings
  91. )) {
  92. describe(backendVariantWithShardNumber, function () {
  93. let app,
  94. previousEgress,
  95. previousIngress,
  96. metricPrefix,
  97. projectId,
  98. otherProjectId
  99. const dataEncryptionKeySize =
  100. backendSettings.backend === 's3SSEC' ? 32 : 0
  101. const BUCKET_NAMES = [
  102. process.env.GCS_USER_FILES_BUCKET_NAME,
  103. process.env.GCS_TEMPLATE_FILES_BUCKET_NAME,
  104. `${process.env.GCS_USER_FILES_BUCKET_NAME}-deleted`,
  105. `${process.env.GCS_TEMPLATE_FILES_BUCKET_NAME}-deleted`,
  106. ]
  107. before('start filestore with new settings', async function () {
  108. // create the app with the relevant filestore settings
  109. Settings.filestore = backendSettings
  110. app = new FilestoreApp()
  111. await app.runServer()
  112. })
  113. if (backendSettings.gcs) {
  114. before('create gcs buckets', async function () {
  115. // create test buckets for gcs
  116. const storage = new Storage(Settings.filestore.gcs.endpoint)
  117. for (const bucketName of BUCKET_NAMES) {
  118. await storage.createBucket(bucketName)
  119. }
  120. })
  121. after('delete gcs buckets', async function () {
  122. // tear down all the gcs buckets
  123. const storage = new Storage(Settings.filestore.gcs.endpoint)
  124. for (const bucketName of BUCKET_NAMES) {
  125. const bucket = storage.bucket(bucketName)
  126. await bucket.deleteFiles()
  127. await bucket.delete()
  128. }
  129. })
  130. }
  131. after('stop filestore app', async function () {
  132. await app.stop()
  133. })
  134. beforeEach('fetch previous egress metric', async function () {
  135. // retrieve previous metrics from the app
  136. if (['s3', 's3SSEC', 'gcs'].includes(Settings.filestore.backend)) {
  137. metricPrefix = Settings.filestore.backend.replace('SSEC', '')
  138. previousEgress = await TestHelper.getMetric(
  139. filestoreUrl,
  140. `${metricPrefix}_egress`
  141. )
  142. }
  143. projectId = new ObjectId().toString()
  144. otherProjectId = new ObjectId().toString()
  145. })
  146. it('should send a 200 for the status endpoint', async function () {
  147. const response = await fetch(`${filestoreUrl}/status`)
  148. expect(response.status).to.equal(200)
  149. const body = await response.text()
  150. expect(body).to.contain('filestore')
  151. expect(body).to.contain('up')
  152. })
  153. describe('with a file on the server', function () {
  154. let fileId, fileUrl, constantFileContent
  155. const localFileReadPath =
  156. '/tmp/filestore_acceptance_tests_file_read.txt'
  157. beforeEach('upload file', async function () {
  158. fileId = new ObjectId().toString()
  159. fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  160. constantFileContent = [
  161. 'hello world',
  162. `line 2 goes here ${Math.random()}`,
  163. 'there are 3 lines in all',
  164. ].join('\n')
  165. await fsWriteFile(localFileReadPath, constantFileContent)
  166. const readStream = fs.createReadStream(localFileReadPath)
  167. const res = await fetch(fileUrl, { method: 'POST', body: readStream })
  168. if (!res.ok) throw new Error(res.statusText)
  169. })
  170. beforeEach('retrieve previous ingress metric', async function () {
  171. // The upload request can bump the ingress metric.
  172. // The content hash validation might require a full download
  173. // in case the ETag field of the upload response is not a md5 sum.
  174. if (['s3', 's3SSEC', 'gcs'].includes(Settings.filestore.backend)) {
  175. previousIngress = await TestHelper.getMetric(
  176. filestoreUrl,
  177. `${metricPrefix}_ingress`
  178. )
  179. }
  180. })
  181. it('should return 404 for a non-existant id', async function () {
  182. const url = fileUrl + '___this_is_clearly_wrong___'
  183. const response = await fetch(url)
  184. expect(response.status).to.equal(404)
  185. })
  186. it('should return the file size on a HEAD request', async function () {
  187. const expectedLength = Buffer.byteLength(constantFileContent)
  188. const res = await fetch(fileUrl, { method: 'HEAD' })
  189. expect(res.status).to.equal(200)
  190. expect(res.headers.get('Content-Length')).to.equal(
  191. expectedLength.toString()
  192. )
  193. })
  194. it('should be able get the file back', async function () {
  195. const res = await fetch(fileUrl)
  196. const body = await res.text()
  197. expect(body).to.equal(constantFileContent)
  198. })
  199. it('should send a 200 for the health-check endpoint using the file', async function () {
  200. Settings.health_check = {
  201. project_id: projectId,
  202. file_id: fileId,
  203. }
  204. const response = await fetch(`${filestoreUrl}/health_check`)
  205. expect(response.status).to.equal(200)
  206. const body = await response.text()
  207. expect(body).to.equal('OK')
  208. })
  209. it('should not leak a socket', async function () {
  210. const res = await fetch(fileUrl)
  211. if (!res.ok) throw new Error(res.statusText)
  212. await res.text()
  213. await expectNoSockets()
  214. })
  215. it('should be able to get back the first 9 bytes of the file', async function () {
  216. const res = await fetch(fileUrl, { headers: { Range: 'bytes=0-8' } })
  217. const body = await res.text()
  218. expect(body).to.equal('hello wor')
  219. })
  220. it('should be able to get back bytes 4 through 10 of the file', async function () {
  221. const res = await fetch(fileUrl, { headers: { Range: 'bytes=4-10' } })
  222. const body = await res.text()
  223. expect(body).to.equal('o world')
  224. })
  225. it('should be able to delete the file', async function () {
  226. const response = await fetch(fileUrl, { method: 'DELETE' })
  227. expect(response.status).to.equal(204)
  228. const response2 = await fetch(fileUrl)
  229. expect(response2.status).to.equal(404)
  230. })
  231. it('should be able to copy files', async function () {
  232. const newProjectID = new ObjectId().toString()
  233. const newFileId = new ObjectId().toString()
  234. const newFileUrl = `${filestoreUrl}/project/${newProjectID}/file/${newFileId}`
  235. let response = await fetch(newFileUrl, {
  236. method: 'PUT',
  237. body: JSON.stringify({
  238. source: {
  239. project_id: projectId,
  240. file_id: fileId,
  241. },
  242. }),
  243. headers: {
  244. 'Content-Type': 'application/json',
  245. },
  246. })
  247. expect(response.status).to.equal(200)
  248. response = await fetch(fileUrl, { method: 'DELETE' })
  249. expect(response.status).to.equal(204)
  250. response = await fetch(newFileUrl)
  251. const body = await response.text()
  252. expect(body).to.equal(constantFileContent)
  253. })
  254. it('should be able to overwrite the file', async function () {
  255. const newContent = `here is some different content, ${Math.random()}`
  256. const readStream = streamifier.createReadStream(newContent)
  257. await fetch(fileUrl, { method: 'POST', body: readStream })
  258. const response = await fetch(fileUrl)
  259. const body = await response.text()
  260. expect(body).to.equal(newContent)
  261. })
  262. describe('IfNoneMatch', function () {
  263. if (backendSettings.backend === 'fs') {
  264. it('should refuse to handle IfNoneMatch', async function () {
  265. await expect(
  266. app.persistor.sendStream(
  267. Settings.filestore.stores.user_files,
  268. `${projectId}/${fileId}`,
  269. fs.createReadStream(localFileReadPath),
  270. { ifNoneMatch: '*' }
  271. )
  272. ).to.be.rejectedWith(NotImplementedError)
  273. })
  274. } else {
  275. it('should reject sendStream on the same key with IfNoneMatch', async function () {
  276. await expect(
  277. app.persistor.sendStream(
  278. Settings.filestore.stores.user_files,
  279. `${projectId}/${fileId}`,
  280. fs.createReadStream(localFileReadPath),
  281. { ifNoneMatch: '*' }
  282. )
  283. ).to.be.rejectedWith(AlreadyWrittenError)
  284. })
  285. it('should allow sendStream on a different key with IfNoneMatch', async function () {
  286. await app.persistor.sendStream(
  287. Settings.filestore.stores.user_files,
  288. `${projectId}/${fileId}-other`,
  289. fs.createReadStream(localFileReadPath),
  290. { ifNoneMatch: '*' }
  291. )
  292. })
  293. }
  294. })
  295. if (backendSettings.backend !== 'fs') {
  296. it('should record an egress metric for the upload', async function () {
  297. const metric = await TestHelper.getMetric(
  298. filestoreUrl,
  299. `${metricPrefix}_egress`
  300. )
  301. expect(metric - previousEgress).to.equal(
  302. constantFileContent.length + dataEncryptionKeySize
  303. )
  304. })
  305. it('should record an ingress metric when downloading the file', async function () {
  306. const response = await fetch(fileUrl)
  307. expect(response.ok).to.be.true
  308. await response.text()
  309. const metric = await TestHelper.getMetric(
  310. filestoreUrl,
  311. `${metricPrefix}_ingress`
  312. )
  313. expect(metric - previousIngress).to.equal(
  314. constantFileContent.length + dataEncryptionKeySize
  315. )
  316. })
  317. it('should record an ingress metric for a partial download', async function () {
  318. const response = await fetch(fileUrl, {
  319. headers: { Range: 'bytes=0-8' },
  320. })
  321. expect(response.ok).to.be.true
  322. await response.text()
  323. const metric = await TestHelper.getMetric(
  324. filestoreUrl,
  325. `${metricPrefix}_ingress`
  326. )
  327. expect(metric - previousIngress).to.equal(9 + dataEncryptionKeySize)
  328. })
  329. }
  330. })
  331. describe('with multiple files', function () {
  332. let fileIds, fileUrls, otherFileUrls, projectUrl, otherProjectUrl
  333. const localFileReadPaths = [
  334. '/tmp/filestore_acceptance_tests_file_read_1.txt',
  335. '/tmp/filestore_acceptance_tests_file_read_2.txt',
  336. '/tmp/filestore_acceptance_tests_file_read_3.txt',
  337. ]
  338. const constantFileContents = [
  339. [
  340. 'hello world',
  341. `line 2 goes here ${Math.random()}`,
  342. 'there are 3 lines in all',
  343. ].join('\n'),
  344. [
  345. `for reference: ${Math.random()}`,
  346. 'cats are the best animals',
  347. 'wombats are a close second',
  348. ].join('\n'),
  349. [
  350. `another file: ${Math.random()}`,
  351. 'with multiple lines',
  352. 'the end',
  353. ].join('\n'),
  354. ]
  355. before('create local files', async function () {
  356. return await Promise.all([
  357. fsWriteFile(localFileReadPaths[0], constantFileContents[0]),
  358. fsWriteFile(localFileReadPaths[1], constantFileContents[1]),
  359. fsWriteFile(localFileReadPaths[2], constantFileContents[2]),
  360. ])
  361. })
  362. beforeEach('upload two files', async function () {
  363. projectUrl = `${filestoreUrl}/project/${projectId}`
  364. otherProjectUrl = `${filestoreUrl}/project/${otherProjectId}`
  365. fileIds = [
  366. new ObjectId().toString(),
  367. new ObjectId().toString(),
  368. new ObjectId().toString(),
  369. ]
  370. fileUrls = [
  371. `${projectUrl}/file/${fileIds[0]}`,
  372. `${projectUrl}/file/${fileIds[1]}`,
  373. ]
  374. otherFileUrls = [`${otherProjectUrl}/file/${fileIds[2]}`]
  375. await Promise.all([
  376. fetch(fileUrls[0], {
  377. method: 'POST',
  378. body: fs.createReadStream(localFileReadPaths[0]),
  379. }),
  380. fetch(fileUrls[1], {
  381. method: 'POST',
  382. body: fs.createReadStream(localFileReadPaths[1]),
  383. }),
  384. fetch(otherFileUrls[0], {
  385. method: 'POST',
  386. body: fs.createReadStream(localFileReadPaths[2]),
  387. }),
  388. ])
  389. })
  390. it('should get the directory size', async function () {
  391. const response = await fetch(
  392. `${filestoreUrl}/project/${projectId}/size`
  393. )
  394. const body = await response.text()
  395. expect(parseInt(JSON.parse(body)['total bytes'])).to.equal(
  396. constantFileContents[0].length + constantFileContents[1].length
  397. )
  398. })
  399. it('should store the files', async function () {
  400. for (const index in fileUrls) {
  401. const response = await fetch(fileUrls[index])
  402. const body = await response.text()
  403. expect(body).to.equal(constantFileContents[index])
  404. }
  405. })
  406. it('should be able to delete the project', async function () {
  407. let response = await fetch(projectUrl, { method: 'DELETE' })
  408. expect(response.status).to.equal(204)
  409. for (const index in fileUrls) {
  410. response = await fetch(fileUrls[index])
  411. expect(response.status).to.equal(404)
  412. }
  413. })
  414. it('should not delete files in other projects', async function () {
  415. for (const index in otherFileUrls) {
  416. const response = await fetch(otherFileUrls[index])
  417. expect(response.status).to.equal(200)
  418. }
  419. })
  420. it('should not delete a partial project id', async function () {
  421. const response = await fetch(`${filestoreUrl}/project/5`, {
  422. method: 'DELETE',
  423. })
  424. expect(response.status).to.equal(400)
  425. })
  426. })
  427. describe('with a large file', function () {
  428. this.timeout(1000 * 20)
  429. let fileId, fileUrl, largeFileContent, error
  430. beforeEach('upload large file', async function () {
  431. fileId = new ObjectId().toString()
  432. fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  433. largeFileContent = '_wombat_'.repeat(1024 * 1024) // 8 megabytes
  434. largeFileContent += Math.random()
  435. const readStream = streamifier.createReadStream(largeFileContent)
  436. const res = await fetch(fileUrl, { method: 'POST', body: readStream })
  437. if (!res.ok) throw new Error(res.statusText)
  438. })
  439. it('should be able to get the file back', async function () {
  440. const response = await fetch(fileUrl)
  441. const body = await response.text()
  442. expect(body).to.equal(largeFileContent)
  443. })
  444. it('should not throw an error', function () {
  445. expect(error).not.to.exist
  446. })
  447. it('should not leak a socket', async function () {
  448. const response = await fetch(fileUrl)
  449. await response.text()
  450. await expectNoSockets()
  451. })
  452. it('should not leak a socket if the connection is aborted', async function () {
  453. const controller = new AbortController()
  454. const response = await fetch(fileUrl, { signal: controller.signal })
  455. expect(response.ok).to.be.true
  456. controller.abort()
  457. await expectNoSockets()
  458. })
  459. })
  460. if (
  461. (backendSettings.backend === 's3' && !backendSettings.fallback) ||
  462. (backendSettings.backend === 'gcs' &&
  463. backendSettings.fallback?.backend === 's3')
  464. ) {
  465. describe('with a file in a specific bucket', function () {
  466. let constantFileContent, fileId, fileUrl, bucketName
  467. beforeEach('upload file into random bucket', async function () {
  468. constantFileContent = `This is a file in a different S3 bucket ${Math.random()}`
  469. fileId = new ObjectId().toString()
  470. bucketName = `random-bucket-${new ObjectId().toString()}`
  471. fileUrl = `${filestoreUrl}/bucket/${bucketName}/key/${fileId}`
  472. const s3 = new S3Persistor({
  473. ...s3Config(),
  474. key: process.env.MINIO_ROOT_USER,
  475. secret: process.env.MINIO_ROOT_PASSWORD,
  476. })._getClientForBucket(bucketName)
  477. await s3
  478. .createBucket({
  479. Bucket: bucketName,
  480. })
  481. .promise()
  482. await s3
  483. .upload({
  484. Bucket: bucketName,
  485. Key: fileId,
  486. Body: constantFileContent,
  487. })
  488. .promise()
  489. })
  490. it('should get the file from the specified bucket', async function () {
  491. const response = await fetch(fileUrl)
  492. const body = await response.text()
  493. expect(body).to.equal(constantFileContent)
  494. })
  495. })
  496. }
  497. if (backendSettings.backend === 'gcs') {
  498. describe('when deleting a file in GCS', function () {
  499. let fileId, fileUrl, content, error, dateBefore, dateAfter
  500. beforeEach('upload and delete file', async function () {
  501. fileId = new ObjectId()
  502. fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  503. content = '_wombat_' + Math.random()
  504. const readStream = streamifier.createReadStream(content)
  505. let res = await fetch(fileUrl, { method: 'POST', body: readStream })
  506. if (!res.ok) throw new Error(res.statusText)
  507. dateBefore = new Date()
  508. res = await fetch(fileUrl, { method: 'DELETE' })
  509. dateAfter = new Date()
  510. if (!res.ok) throw new Error(res.statusText)
  511. })
  512. it('should not throw an error', function () {
  513. expect(error).not.to.exist
  514. })
  515. it('should copy the file to the deleted-files bucket', async function () {
  516. let date = dateBefore
  517. const keys = []
  518. while (date <= dateAfter) {
  519. keys.push(`${projectId}/${fileId}-${date.toISOString()}`)
  520. date = new Date(date.getTime() + 1)
  521. }
  522. await TestHelper.expectPersistorToHaveSomeFile(
  523. app.persistor,
  524. `${Settings.filestore.stores.user_files}-deleted`,
  525. keys,
  526. content
  527. )
  528. })
  529. it('should remove the file from the original bucket', async function () {
  530. await TestHelper.expectPersistorNotToHaveFile(
  531. app.persistor,
  532. Settings.filestore.stores.user_files,
  533. `${projectId}/${fileId}`
  534. )
  535. })
  536. })
  537. }
  538. if (backendSettings.fallback) {
  539. describe('with a fallback', function () {
  540. let constantFileContent,
  541. fileId,
  542. fileKey,
  543. fileUrl,
  544. bucket,
  545. fallbackBucket
  546. beforeEach('prepare fallback', function () {
  547. constantFileContent = `This is yet more file content ${Math.random()}`
  548. fileId = new ObjectId().toString()
  549. fileKey = `${projectId}/${fileId}`
  550. fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  551. bucket = Settings.filestore.stores.user_files
  552. fallbackBucket = Settings.filestore.fallback.buckets[bucket]
  553. })
  554. describe('with a file in the fallback bucket', function () {
  555. beforeEach('upload into fallback', async function () {
  556. await TestHelper.uploadStringToPersistor(
  557. app.persistor.fallbackPersistor,
  558. fallbackBucket,
  559. fileKey,
  560. constantFileContent
  561. )
  562. })
  563. it('should not find file in the primary', async function () {
  564. await TestHelper.expectPersistorNotToHaveFile(
  565. app.persistor.primaryPersistor,
  566. bucket,
  567. fileKey
  568. )
  569. })
  570. it('should find the file in the fallback', async function () {
  571. await TestHelper.expectPersistorToHaveFile(
  572. app.persistor.fallbackPersistor,
  573. fallbackBucket,
  574. fileKey,
  575. constantFileContent
  576. )
  577. })
  578. describe('when copyOnMiss is disabled', function () {
  579. beforeEach('swap copyOnMiss=false', function () {
  580. app.persistor.settings.copyOnMiss = false
  581. })
  582. it('should fetch the file', async function () {
  583. const res = await fetch(fileUrl)
  584. const body = await res.text()
  585. expect(body).to.equal(constantFileContent)
  586. })
  587. it('should not copy the file to the primary', async function () {
  588. const response = await fetch(fileUrl)
  589. expect(response.ok).to.be.true
  590. await response.text()
  591. await TestHelper.expectPersistorNotToHaveFile(
  592. app.persistor.primaryPersistor,
  593. bucket,
  594. fileKey
  595. )
  596. })
  597. })
  598. describe('when copyOnMiss is enabled', function () {
  599. beforeEach('swap copyOnMiss=true', function () {
  600. app.persistor.settings.copyOnMiss = true
  601. })
  602. it('should fetch the file', async function () {
  603. const res = await fetch(fileUrl)
  604. const body = await res.text()
  605. expect(body).to.equal(constantFileContent)
  606. })
  607. it('copies the file to the primary', async function () {
  608. const response = await fetch(fileUrl)
  609. expect(response.ok).to.be.true
  610. await response.text()
  611. // wait for the file to copy in the background
  612. await msleep(1000)
  613. await TestHelper.expectPersistorToHaveFile(
  614. app.persistor.primaryPersistor,
  615. bucket,
  616. fileKey,
  617. constantFileContent
  618. )
  619. })
  620. })
  621. describe('when copying a file', function () {
  622. let newFileId, newFileUrl, newFileKey, opts
  623. beforeEach('prepare to copy file', function () {
  624. const newProjectID = new ObjectId().toString()
  625. newFileId = new ObjectId().toString()
  626. newFileUrl = `${filestoreUrl}/project/${newProjectID}/file/${newFileId}`
  627. newFileKey = `${newProjectID}/${newFileId}`
  628. opts = {
  629. method: 'put',
  630. body: JSON.stringify({
  631. source: {
  632. project_id: projectId,
  633. file_id: fileId,
  634. },
  635. }),
  636. headers: {
  637. 'Content-Type': 'application/json',
  638. },
  639. }
  640. })
  641. describe('when copyOnMiss is false', function () {
  642. beforeEach('copy with copyOnMiss=false', async function () {
  643. app.persistor.settings.copyOnMiss = false
  644. const response = await fetch(newFileUrl, opts)
  645. expect(response.status).to.equal(200)
  646. })
  647. it('should leave the old file in the old bucket', async function () {
  648. await TestHelper.expectPersistorToHaveFile(
  649. app.persistor.fallbackPersistor,
  650. fallbackBucket,
  651. fileKey,
  652. constantFileContent
  653. )
  654. })
  655. it('should not create a new file in the old bucket', async function () {
  656. await TestHelper.expectPersistorNotToHaveFile(
  657. app.persistor.fallbackPersistor,
  658. fallbackBucket,
  659. newFileKey
  660. )
  661. })
  662. it('should create a new file in the new bucket', async function () {
  663. await TestHelper.expectPersistorToHaveFile(
  664. app.persistor.primaryPersistor,
  665. bucket,
  666. newFileKey,
  667. constantFileContent
  668. )
  669. })
  670. it('should not copy the old file to the primary with the old key', async function () {
  671. // wait for the file to copy in the background
  672. await msleep(1000)
  673. await TestHelper.expectPersistorNotToHaveFile(
  674. app.persistor.primaryPersistor,
  675. bucket,
  676. fileKey
  677. )
  678. })
  679. })
  680. describe('when copyOnMiss is true', function () {
  681. beforeEach('copy with copyOnMiss=false', async function () {
  682. app.persistor.settings.copyOnMiss = true
  683. const response = await fetch(newFileUrl, opts)
  684. expect(response.status).to.equal(200)
  685. })
  686. it('should leave the old file in the old bucket', async function () {
  687. await TestHelper.expectPersistorToHaveFile(
  688. app.persistor.fallbackPersistor,
  689. fallbackBucket,
  690. fileKey,
  691. constantFileContent
  692. )
  693. })
  694. it('should not create a new file in the old bucket', async function () {
  695. await TestHelper.expectPersistorNotToHaveFile(
  696. app.persistor.fallbackPersistor,
  697. fallbackBucket,
  698. newFileKey
  699. )
  700. })
  701. it('should create a new file in the new bucket', async function () {
  702. await TestHelper.expectPersistorToHaveFile(
  703. app.persistor.primaryPersistor,
  704. bucket,
  705. newFileKey,
  706. constantFileContent
  707. )
  708. })
  709. it('should copy the old file to the primary with the old key', async function () {
  710. // wait for the file to copy in the background
  711. await msleep(1000)
  712. await TestHelper.expectPersistorToHaveFile(
  713. app.persistor.primaryPersistor,
  714. bucket,
  715. fileKey,
  716. constantFileContent
  717. )
  718. })
  719. })
  720. })
  721. })
  722. describe('when sending a file', function () {
  723. beforeEach('upload file', async function () {
  724. const readStream =
  725. streamifier.createReadStream(constantFileContent)
  726. const res = await fetch(fileUrl, {
  727. method: 'POST',
  728. body: readStream,
  729. })
  730. if (!res.ok) throw new Error(res.statusText)
  731. })
  732. it('should store the file on the primary', async function () {
  733. await TestHelper.expectPersistorToHaveFile(
  734. app.persistor.primaryPersistor,
  735. bucket,
  736. fileKey,
  737. constantFileContent
  738. )
  739. })
  740. it('should not store the file on the fallback', async function () {
  741. await TestHelper.expectPersistorNotToHaveFile(
  742. app.persistor.fallbackPersistor,
  743. fallbackBucket,
  744. `${projectId}/${fileId}`
  745. )
  746. })
  747. })
  748. describe('when deleting a file', function () {
  749. describe('when the file exists on the primary', function () {
  750. beforeEach('upload into primary', async function () {
  751. await TestHelper.uploadStringToPersistor(
  752. app.persistor.primaryPersistor,
  753. bucket,
  754. fileKey,
  755. constantFileContent
  756. )
  757. })
  758. it('should delete the file', async function () {
  759. const response1 = await fetch(fileUrl, { method: 'DELETE' })
  760. expect(response1.status).to.equal(204)
  761. const response2 = await fetch(fileUrl)
  762. expect(response2.status).to.equal(404)
  763. })
  764. })
  765. describe('when the file exists on the fallback', function () {
  766. beforeEach('upload into fallback', async function () {
  767. await TestHelper.uploadStringToPersistor(
  768. app.persistor.fallbackPersistor,
  769. fallbackBucket,
  770. fileKey,
  771. constantFileContent
  772. )
  773. })
  774. it('should delete the file', async function () {
  775. const response1 = await fetch(fileUrl, { method: 'DELETE' })
  776. expect(response1.status).to.equal(204)
  777. const response2 = await fetch(fileUrl)
  778. expect(response2.status).to.equal(404)
  779. })
  780. })
  781. describe('when the file exists on both the primary and the fallback', function () {
  782. beforeEach(
  783. 'upload into both primary and fallback',
  784. async function () {
  785. await TestHelper.uploadStringToPersistor(
  786. app.persistor.primaryPersistor,
  787. bucket,
  788. fileKey,
  789. constantFileContent
  790. )
  791. await TestHelper.uploadStringToPersistor(
  792. app.persistor.fallbackPersistor,
  793. fallbackBucket,
  794. fileKey,
  795. constantFileContent
  796. )
  797. }
  798. )
  799. it('should delete the files', async function () {
  800. const response1 = await fetch(fileUrl, { method: 'DELETE' })
  801. expect(response1.status).to.equal(204)
  802. const response2 = await fetch(fileUrl)
  803. expect(response2.status).to.equal(404)
  804. })
  805. })
  806. describe('when the file does not exist', function () {
  807. it('should return return 204', async function () {
  808. // S3 doesn't give us a 404 when the object doesn't exist, so to stay
  809. // consistent we merrily return 204 ourselves here as well
  810. const response = await fetch(fileUrl, { method: 'DELETE' })
  811. expect(response.status).to.equal(204)
  812. })
  813. })
  814. })
  815. })
  816. }
  817. describe('with a pdf file', function () {
  818. let fileId, fileUrl, localFileSize
  819. const localFileReadPath = Path.resolve(
  820. __dirname,
  821. '../../fixtures/test.pdf'
  822. )
  823. beforeEach('upload test.pdf', async function () {
  824. fileId = new ObjectId().toString()
  825. fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  826. const stat = await fsStat(localFileReadPath)
  827. localFileSize = stat.size
  828. const readStream = fs.createReadStream(localFileReadPath)
  829. const res = await fetch(fileUrl, { method: 'POST', body: readStream })
  830. if (!res.ok) throw new Error(res.statusText)
  831. })
  832. it('should be able get the file back', async function () {
  833. const response = await fetch(fileUrl)
  834. const body = await response.text()
  835. expect(body.substring(0, 8)).to.equal('%PDF-1.5')
  836. })
  837. if (backendSettings.backend !== 'fs') {
  838. it('should record an egress metric for the upload', async function () {
  839. const metric = await TestHelper.getMetric(
  840. filestoreUrl,
  841. `${metricPrefix}_egress`
  842. )
  843. expect(metric - previousEgress).to.equal(
  844. localFileSize + dataEncryptionKeySize
  845. )
  846. })
  847. }
  848. describe('getting the preview image', function () {
  849. this.timeout(1000 * 20)
  850. let previewFileUrl
  851. beforeEach('prepare previewFileUrl for preview', function () {
  852. previewFileUrl = `${fileUrl}?style=preview`
  853. })
  854. it('should not time out', async function () {
  855. const response = await fetch(previewFileUrl)
  856. expect(response.status).to.equal(200)
  857. await response.arrayBuffer()
  858. })
  859. it('should respond with image data', async function () {
  860. // note: this test relies of the imagemagick conversion working
  861. const response = await fetch(previewFileUrl)
  862. expect(response.status).to.equal(200)
  863. const body = await response.text()
  864. expect(body.length).to.be.greaterThan(400)
  865. expect(body.substr(1, 3)).to.equal('PNG')
  866. })
  867. })
  868. describe('warming the cache', function () {
  869. this.timeout(1000 * 20)
  870. let previewFileUrl
  871. beforeEach('prepare previewFileUrl for cacheWarn', function () {
  872. previewFileUrl = `${fileUrl}?style=preview&cacheWarm=true`
  873. })
  874. it('should not time out', async function () {
  875. const response = await fetch(previewFileUrl)
  876. expect(response.status).to.equal(200)
  877. await response.arrayBuffer()
  878. })
  879. it('should not leak sockets', async function () {
  880. const response1 = await fetch(previewFileUrl)
  881. expect(response1.status).to.equal(200)
  882. // do not read the response body, should be destroyed immediately
  883. const response2 = await fetch(previewFileUrl)
  884. expect(response2.status).to.equal(200)
  885. // do not read the response body, should be destroyed immediately
  886. await expectNoSockets()
  887. })
  888. it("should respond with only an 'OK'", async function () {
  889. // note: this test relies of the imagemagick conversion working
  890. const response = await fetch(previewFileUrl)
  891. const body = await response.text()
  892. expect(body).to.equal('OK')
  893. })
  894. })
  895. })
  896. describe('with server side encryption', function () {
  897. if (backendSettings.backend !== 's3SSEC') return
  898. before('sanity check top-level variable', function () {
  899. expect(dataEncryptionKeySize).to.equal(32)
  900. })
  901. let fileId1,
  902. fileId2,
  903. fileKey1,
  904. fileKey2,
  905. fileKeyOtherProject,
  906. fileUrl1,
  907. fileUrl2
  908. beforeEach('prepare ids', function () {
  909. fileId1 = new ObjectId().toString()
  910. fileId2 = new ObjectId().toString()
  911. fileKey1 = `${projectId}/${fileId1}`
  912. fileKey2 = `${projectId}/${fileId2}`
  913. fileKeyOtherProject = `${new ObjectId().toString()}/${new ObjectId().toString()}`
  914. fileUrl1 = `${filestoreUrl}/project/${projectId}/file/${fileId1}`
  915. fileUrl2 = `${filestoreUrl}/project/${projectId}/file/${fileId2}`
  916. })
  917. beforeEach('ensure DEK is missing', async function () {
  918. // Cannot use test helper expectPersistorNotToHaveFile here, we need to use the KEK.
  919. await expect(
  920. app.persistor.getDataEncryptionKeySize(
  921. backendSettings.stores.user_files,
  922. fileKey1
  923. )
  924. ).to.rejectedWith(NotFoundError)
  925. })
  926. async function createRandomContent(url, suffix = '') {
  927. const content = Math.random().toString() + suffix
  928. const res = await fetch(url, {
  929. method: 'POST',
  930. body: Stream.Readable.from([content]),
  931. })
  932. if (!res.ok) throw new Error(res.statusText)
  933. return async () => {
  934. const res = await fetch(url, { method: 'GET' })
  935. if (!res.ok) throw new Error(res.statusText)
  936. expect(await res.text()).to.equal(content)
  937. }
  938. }
  939. it('should create a DEK when asked explicitly', async function () {
  940. await app.persistor.generateDataEncryptionKey(
  941. backendSettings.stores.user_files,
  942. fileKey1
  943. )
  944. expect(
  945. await app.persistor.getDataEncryptionKeySize(
  946. backendSettings.stores.user_files,
  947. fileKey1
  948. )
  949. ).to.equal(32)
  950. })
  951. it('should create a DEK from writes', async function () {
  952. await createRandomContent(fileUrl1)
  953. expect(
  954. await app.persistor.getDataEncryptionKeySize(
  955. backendSettings.stores.user_files,
  956. fileKey1
  957. )
  958. ).to.equal(32)
  959. })
  960. it('should not create a DEK from reads', async function () {
  961. const res = await fetch(fileUrl1, {
  962. method: 'GET',
  963. })
  964. if (res.status !== 404) throw new Error(`${res.status} should be 404`)
  965. // Cannot use test helper expectPersistorNotToHaveFile here, we need to use the KEK.
  966. await expect(
  967. app.persistor.getDataEncryptionKeySize(
  968. backendSettings.stores.user_files,
  969. fileKey1
  970. )
  971. ).to.rejectedWith(NotFoundError)
  972. })
  973. it('should never overwrite a data encryption key', async function () {
  974. const checkGET = await createRandomContent(fileUrl1)
  975. await expect(
  976. app.persistor.generateDataEncryptionKey(
  977. backendSettings.stores.user_files,
  978. fileKey1
  979. )
  980. ).to.rejectedWith(AlreadyWrittenError)
  981. await checkGET()
  982. })
  983. it('should re-use the data encryption key after a write', async function () {
  984. const checkGET1 = await createRandomContent(fileUrl1, '1')
  985. const checkGET2 = await createRandomContent(fileUrl2, '2')
  986. await checkGET1()
  987. await checkGET2()
  988. })
  989. describe('kek rotation', function () {
  990. const newKEK = new RootKeyEncryptionKey(
  991. crypto.generateKeySync('aes', { length: 256 }).export(),
  992. Buffer.alloc(32)
  993. )
  994. const oldKEK = new RootKeyEncryptionKey(
  995. crypto.generateKeySync('aes', { length: 256 }).export(),
  996. Buffer.alloc(32)
  997. )
  998. const migrationStep0 = new PerProjectEncryptedS3Persistor({
  999. ...s3SSECConfig(),
  1000. automaticallyRotateDEKEncryption: false,
  1001. async getRootKeyEncryptionKeys() {
  1002. return [oldKEK] // only old key
  1003. },
  1004. })
  1005. const migrationStep1 = new PerProjectEncryptedS3Persistor({
  1006. ...s3SSECConfig(),
  1007. automaticallyRotateDEKEncryption: false,
  1008. async getRootKeyEncryptionKeys() {
  1009. return [oldKEK, newKEK] // new key as fallback
  1010. },
  1011. })
  1012. const migrationStep2 = new PerProjectEncryptedS3Persistor({
  1013. ...s3SSECConfig(),
  1014. automaticallyRotateDEKEncryption: true, // <- different compared to partiallyRotated
  1015. async getRootKeyEncryptionKeys() {
  1016. return [newKEK, oldKEK] // old keys as fallback
  1017. },
  1018. })
  1019. const migrationStep3 = new PerProjectEncryptedS3Persistor({
  1020. ...s3SSECConfig(),
  1021. automaticallyRotateDEKEncryption: true,
  1022. async getRootKeyEncryptionKeys() {
  1023. return [newKEK] // only new key
  1024. },
  1025. })
  1026. async function checkWrites(
  1027. fileKey,
  1028. writer,
  1029. readersSuccess,
  1030. readersFailed
  1031. ) {
  1032. const content = Math.random().toString()
  1033. await writer.sendStream(
  1034. Settings.filestore.stores.user_files,
  1035. fileKey,
  1036. Stream.Readable.from([content])
  1037. )
  1038. for (const persistor of readersSuccess) {
  1039. await TestHelper.expectPersistorToHaveFile(
  1040. persistor,
  1041. backendSettings.stores.user_files,
  1042. fileKey,
  1043. content
  1044. )
  1045. }
  1046. for (const persistor of readersFailed) {
  1047. await expect(
  1048. TestHelper.expectPersistorToHaveFile(
  1049. persistor,
  1050. backendSettings.stores.user_files,
  1051. fileKey,
  1052. content
  1053. )
  1054. ).to.be.rejectedWith(NoKEKMatchedError)
  1055. }
  1056. }
  1057. const stages = [
  1058. {
  1059. name: 'stage 0 - [old]',
  1060. prev: migrationStep0,
  1061. cur: migrationStep0,
  1062. fail: [migrationStep3],
  1063. },
  1064. {
  1065. name: 'stage 1 - [old,new]',
  1066. prev: migrationStep0,
  1067. cur: migrationStep1,
  1068. fail: [],
  1069. },
  1070. {
  1071. name: 'stage 2 - [new,old]',
  1072. prev: migrationStep1,
  1073. cur: migrationStep2,
  1074. fail: [],
  1075. },
  1076. {
  1077. name: 'stage 3 - [new]',
  1078. prev: migrationStep2,
  1079. cur: migrationStep3,
  1080. fail: [migrationStep0],
  1081. },
  1082. ]
  1083. for (const { name, prev, cur, fail } of stages) {
  1084. describe(name, function () {
  1085. this.timeout(1000 * 30)
  1086. it('can read old writes', async function () {
  1087. await checkWrites(fileKey1, prev, [prev, cur], fail)
  1088. await checkWrites(fileKey2, prev, [prev, cur], fail) // check again after access
  1089. await checkWrites(fileKeyOtherProject, prev, [prev, cur], fail)
  1090. })
  1091. it('can read new writes', async function () {
  1092. await checkWrites(fileKey1, prev, [prev, cur], fail)
  1093. await checkWrites(fileKey2, cur, [prev, cur], fail) // check again after access
  1094. await checkWrites(fileKeyOtherProject, cur, [prev, cur], fail)
  1095. })
  1096. })
  1097. }
  1098. describe('full migration', function () {
  1099. it('can read old writes if rotated in sequence', async function () {
  1100. await checkWrites(
  1101. fileKey1,
  1102. migrationStep0,
  1103. [
  1104. migrationStep0,
  1105. migrationStep1,
  1106. migrationStep2, // migrates
  1107. migrationStep3,
  1108. ],
  1109. []
  1110. )
  1111. })
  1112. it('cannot read/write if not rotated', async function () {
  1113. await checkWrites(
  1114. fileKey1,
  1115. migrationStep0,
  1116. [migrationStep0],
  1117. [migrationStep3]
  1118. )
  1119. })
  1120. })
  1121. })
  1122. /** @type {import('aws-sdk/clients/s3')} */
  1123. let s3Client
  1124. before('create s3 client', function () {
  1125. s3Client = new S3Persistor(s3Config())._getClientForBucket('')
  1126. })
  1127. async function checkDEKStorage({
  1128. dekBucketKeys = [],
  1129. userFilesBucketKeys = [],
  1130. }) {
  1131. await createRandomContent(fileUrl1)
  1132. const { Contents: dekEntries } = await s3Client
  1133. .listObjectsV2({
  1134. Bucket: process.env.AWS_S3_USER_FILES_DEK_BUCKET_NAME,
  1135. Prefix: `${projectId}/`,
  1136. })
  1137. .promise()
  1138. expect(dekEntries).to.have.length(dekBucketKeys.length)
  1139. // Order is not predictable, use members
  1140. expect(dekEntries.map(o => o.Key)).to.have.members(dekBucketKeys)
  1141. const { Contents: userFilesEntries } = await s3Client
  1142. .listObjectsV2({
  1143. Bucket: backendSettings.stores.user_files,
  1144. Prefix: `${projectId}/`,
  1145. })
  1146. .promise()
  1147. expect(userFilesEntries).to.have.length(userFilesBucketKeys.length)
  1148. // Order is not predictable, use members
  1149. expect(userFilesEntries.map(o => o.Key)).to.have.members(
  1150. userFilesBucketKeys
  1151. )
  1152. }
  1153. it('should use a custom bucket for DEKs', async function () {
  1154. await checkDEKStorage({
  1155. dekBucketKeys: [`${projectId}/dek`],
  1156. userFilesBucketKeys: [fileKey1],
  1157. })
  1158. })
  1159. describe('deleteDirectory', function () {
  1160. let checkGET1, checkGET2
  1161. beforeEach('create files', async function () {
  1162. checkGET1 = await createRandomContent(fileUrl1, '1')
  1163. checkGET2 = await createRandomContent(fileUrl2, '2')
  1164. })
  1165. it('should refuse to delete top-level prefix', async function () {
  1166. await expect(
  1167. app.persistor.deleteDirectory(
  1168. Settings.filestore.stores.user_files,
  1169. projectId.slice(0, 3)
  1170. )
  1171. ).to.be.rejectedWith('not a project-folder')
  1172. expect(
  1173. await app.persistor.checkIfObjectExists(
  1174. Settings.filestore.stores.user_files,
  1175. fileKey1
  1176. )
  1177. ).to.equal(true)
  1178. await checkGET1()
  1179. expect(
  1180. await app.persistor.checkIfObjectExists(
  1181. Settings.filestore.stores.user_files,
  1182. fileKey2
  1183. )
  1184. ).to.equal(true)
  1185. expect(
  1186. await app.persistor.getDataEncryptionKeySize(
  1187. Settings.filestore.stores.user_files,
  1188. fileKey2
  1189. )
  1190. ).to.equal(32)
  1191. await checkGET2()
  1192. })
  1193. it('should delete sub-folder and keep DEK', async function () {
  1194. await app.persistor.deleteDirectory(
  1195. Settings.filestore.stores.user_files,
  1196. fileKey1 // not really a sub-folder, but it will do for this test.
  1197. )
  1198. expect(
  1199. await app.persistor.checkIfObjectExists(
  1200. Settings.filestore.stores.user_files,
  1201. fileKey1
  1202. )
  1203. ).to.equal(false)
  1204. expect(
  1205. await app.persistor.checkIfObjectExists(
  1206. Settings.filestore.stores.user_files,
  1207. fileKey2
  1208. )
  1209. ).to.equal(true)
  1210. expect(
  1211. await app.persistor.getDataEncryptionKeySize(
  1212. Settings.filestore.stores.user_files,
  1213. fileKey2
  1214. )
  1215. ).to.equal(32)
  1216. await checkGET2()
  1217. })
  1218. it('should delete project folder and DEK', async function () {
  1219. await app.persistor.deleteDirectory(
  1220. Settings.filestore.stores.user_files,
  1221. `${projectId}/`
  1222. )
  1223. expect(
  1224. await app.persistor.checkIfObjectExists(
  1225. Settings.filestore.stores.user_files,
  1226. fileKey1
  1227. )
  1228. ).to.equal(false)
  1229. expect(
  1230. await app.persistor.checkIfObjectExists(
  1231. Settings.filestore.stores.user_files,
  1232. fileKey2
  1233. )
  1234. ).to.equal(false)
  1235. await expect(
  1236. app.persistor.getDataEncryptionKeySize(
  1237. Settings.filestore.stores.user_files,
  1238. fileKey2
  1239. )
  1240. ).to.rejectedWith(NotFoundError)
  1241. })
  1242. })
  1243. })
  1244. describe('getObjectSize', function () {
  1245. it('should return a number', async function () {
  1246. const buf = Buffer.from('hello')
  1247. const fileId = new ObjectId().toString()
  1248. const fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  1249. const res = await fetch(fileUrl, {
  1250. method: 'POST',
  1251. body: Stream.Readable.from([buf]),
  1252. })
  1253. if (!res.ok) throw new Error(res.statusText)
  1254. expect(
  1255. await app.persistor.getObjectSize(
  1256. Settings.filestore.stores.user_files,
  1257. `${projectId}/${fileId}`
  1258. )
  1259. ).to.equal(buf.byteLength)
  1260. })
  1261. })
  1262. describe('checkIfObjectExists', function () {
  1263. it('should return false when the object does not exist', async function () {
  1264. expect(
  1265. await app.persistor.checkIfObjectExists(
  1266. Settings.filestore.stores.user_files,
  1267. `${projectId}/${new ObjectId().toString()}`
  1268. )
  1269. ).to.equal(false)
  1270. })
  1271. it('should return true when the object exists', async function () {
  1272. const fileId = new ObjectId().toString()
  1273. const fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  1274. const res = await fetch(fileUrl, {
  1275. method: 'POST',
  1276. body: Stream.Readable.from(['hello']),
  1277. })
  1278. if (!res.ok) throw new Error(res.statusText)
  1279. expect(
  1280. await app.persistor.checkIfObjectExists(
  1281. Settings.filestore.stores.user_files,
  1282. `${projectId}/${fileId}`
  1283. )
  1284. ).to.equal(true)
  1285. })
  1286. })
  1287. if (backendSettings.backend === 's3SSEC') {
  1288. describe('storageClass', function () {
  1289. it('should use the default storage class for dek', async function () {
  1290. const key = `${projectId}/${new ObjectId()}`
  1291. const dekBucket = process.env.AWS_S3_USER_FILES_DEK_BUCKET_NAME
  1292. await app.persistor.sendStream(
  1293. dekBucket,
  1294. key,
  1295. Stream.Readable.from(['hello'])
  1296. )
  1297. expect(
  1298. await app.persistor.getObjectStorageClass(dekBucket, key)
  1299. ).to.equal(undefined)
  1300. })
  1301. it('should use the custom storage class for user files', async function () {
  1302. const key = `${projectId}/${new ObjectId()}`
  1303. await app.persistor.sendStream(
  1304. Settings.filestore.stores.user_files,
  1305. key,
  1306. Stream.Readable.from(['hello'])
  1307. )
  1308. const sc = AWS_S3_USER_FILES_STORAGE_CLASS
  1309. expect(sc).to.exist
  1310. expect(
  1311. await app.persistor.getObjectStorageClass(
  1312. Settings.filestore.stores.user_files,
  1313. key
  1314. )
  1315. ).to.equal(sc)
  1316. })
  1317. })
  1318. }
  1319. describe('autoGunzip', function () {
  1320. let key
  1321. beforeEach('new key', function () {
  1322. key = `${projectId}/${new ObjectId().toString()}`
  1323. })
  1324. this.timeout(60 * 1000)
  1325. const body = Buffer.alloc(10 * 1024 * 1024, 'hello')
  1326. const gzippedBody = gzipSync(body)
  1327. /**
  1328. * @param {string} key
  1329. * @param {Buffer} wantBody
  1330. * @param {boolean} autoGunzip
  1331. * @return {Promise<void>}
  1332. */
  1333. async function checkBodyIsTheSame(key, wantBody, autoGunzip) {
  1334. const s = await app.persistor.getObjectStream(
  1335. Settings.filestore.stores.user_files,
  1336. key,
  1337. { autoGunzip }
  1338. )
  1339. const buf = new WritableBuffer()
  1340. await Stream.promises.pipeline(s, buf)
  1341. expect(buf.getContents()).to.deep.equal(wantBody)
  1342. }
  1343. if (backendSettings.backend === 'fs') {
  1344. it('should refuse to handle autoGunzip', async function () {
  1345. await expect(
  1346. app.persistor.getObjectStream(
  1347. Settings.filestore.stores.user_files,
  1348. key,
  1349. { autoGunzip: true }
  1350. )
  1351. ).to.be.rejectedWith(NotImplementedError)
  1352. })
  1353. } else {
  1354. it('should return the raw body with gzip', async function () {
  1355. await app.persistor.sendStream(
  1356. Settings.filestore.stores.user_files,
  1357. key,
  1358. Stream.Readable.from([gzippedBody]),
  1359. { contentEncoding: 'gzip' }
  1360. )
  1361. expect(
  1362. await app.persistor.getObjectSize(
  1363. Settings.filestore.stores.user_files,
  1364. key
  1365. )
  1366. ).to.equal(gzippedBody.byteLength)
  1367. // raw body with autoGunzip=true
  1368. await checkBodyIsTheSame(key, body, true)
  1369. // gzip body without autoGunzip=false
  1370. await checkBodyIsTheSame(key, gzippedBody, false)
  1371. })
  1372. it('should return the raw body without gzip compression', async function () {
  1373. await app.persistor.sendStream(
  1374. Settings.filestore.stores.user_files,
  1375. key,
  1376. Stream.Readable.from([body])
  1377. )
  1378. expect(
  1379. await app.persistor.getObjectSize(
  1380. Settings.filestore.stores.user_files,
  1381. key
  1382. )
  1383. ).to.equal(body.byteLength)
  1384. // raw body with both autoGunzip options
  1385. await checkBodyIsTheSame(key, body, true)
  1386. await checkBodyIsTheSame(key, body, false)
  1387. })
  1388. it('should return the gzip body without gzip header', async function () {
  1389. await app.persistor.sendStream(
  1390. Settings.filestore.stores.user_files,
  1391. key,
  1392. Stream.Readable.from([gzippedBody])
  1393. )
  1394. expect(
  1395. await app.persistor.getObjectSize(
  1396. Settings.filestore.stores.user_files,
  1397. key
  1398. )
  1399. ).to.equal(gzippedBody.byteLength)
  1400. // gzip body with both autoGunzip options
  1401. await checkBodyIsTheSame(key, gzippedBody, true)
  1402. await checkBodyIsTheSame(key, gzippedBody, false)
  1403. })
  1404. }
  1405. })
  1406. })
  1407. }
  1408. })