FilestoreTests.js 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536
  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 checkGET2
  1161. beforeEach('create files', async function () {
  1162. await createRandomContent(fileUrl1, '1')
  1163. checkGET2 = await createRandomContent(fileUrl2, '2')
  1164. })
  1165. it('should delete sub-folder and keep DEK', async function () {
  1166. await app.persistor.deleteDirectory(
  1167. Settings.filestore.stores.user_files,
  1168. fileKey1 // not really a sub-folder, but it will do for this test.
  1169. )
  1170. expect(
  1171. await app.persistor.checkIfObjectExists(
  1172. Settings.filestore.stores.user_files,
  1173. fileKey1
  1174. )
  1175. ).to.equal(false)
  1176. expect(
  1177. await app.persistor.checkIfObjectExists(
  1178. Settings.filestore.stores.user_files,
  1179. fileKey2
  1180. )
  1181. ).to.equal(true)
  1182. expect(
  1183. await app.persistor.getDataEncryptionKeySize(
  1184. Settings.filestore.stores.user_files,
  1185. fileKey2
  1186. )
  1187. ).to.equal(32)
  1188. await checkGET2()
  1189. })
  1190. it('should delete project folder and DEK', async function () {
  1191. await app.persistor.deleteDirectory(
  1192. Settings.filestore.stores.user_files,
  1193. `${projectId}/`
  1194. )
  1195. expect(
  1196. await app.persistor.checkIfObjectExists(
  1197. Settings.filestore.stores.user_files,
  1198. fileKey1
  1199. )
  1200. ).to.equal(false)
  1201. expect(
  1202. await app.persistor.checkIfObjectExists(
  1203. Settings.filestore.stores.user_files,
  1204. fileKey2
  1205. )
  1206. ).to.equal(false)
  1207. await expect(
  1208. app.persistor.getDataEncryptionKeySize(
  1209. Settings.filestore.stores.user_files,
  1210. fileKey2
  1211. )
  1212. ).to.rejectedWith(NotFoundError)
  1213. })
  1214. })
  1215. })
  1216. describe('getObjectSize', function () {
  1217. it('should return a number', async function () {
  1218. const buf = Buffer.from('hello')
  1219. const fileId = new ObjectId().toString()
  1220. const fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  1221. const res = await fetch(fileUrl, {
  1222. method: 'POST',
  1223. body: Stream.Readable.from([buf]),
  1224. })
  1225. if (!res.ok) throw new Error(res.statusText)
  1226. expect(
  1227. await app.persistor.getObjectSize(
  1228. Settings.filestore.stores.user_files,
  1229. `${projectId}/${fileId}`
  1230. )
  1231. ).to.equal(buf.byteLength)
  1232. })
  1233. })
  1234. describe('checkIfObjectExists', function () {
  1235. it('should return false when the object does not exist', async function () {
  1236. expect(
  1237. await app.persistor.checkIfObjectExists(
  1238. Settings.filestore.stores.user_files,
  1239. `${projectId}/${new ObjectId().toString()}`
  1240. )
  1241. ).to.equal(false)
  1242. })
  1243. it('should return true when the object exists', async function () {
  1244. const fileId = new ObjectId().toString()
  1245. const fileUrl = `${filestoreUrl}/project/${projectId}/file/${fileId}`
  1246. const res = await fetch(fileUrl, {
  1247. method: 'POST',
  1248. body: Stream.Readable.from(['hello']),
  1249. })
  1250. if (!res.ok) throw new Error(res.statusText)
  1251. expect(
  1252. await app.persistor.checkIfObjectExists(
  1253. Settings.filestore.stores.user_files,
  1254. `${projectId}/${fileId}`
  1255. )
  1256. ).to.equal(true)
  1257. })
  1258. })
  1259. if (backendSettings.backend === 's3SSEC') {
  1260. describe('storageClass', function () {
  1261. it('should use the default storage class for dek', async function () {
  1262. const key = `${projectId}/${new ObjectId()}`
  1263. const dekBucket = process.env.AWS_S3_USER_FILES_DEK_BUCKET_NAME
  1264. await app.persistor.sendStream(
  1265. dekBucket,
  1266. key,
  1267. Stream.Readable.from(['hello'])
  1268. )
  1269. expect(
  1270. await app.persistor.getObjectStorageClass(dekBucket, key)
  1271. ).to.equal(undefined)
  1272. })
  1273. it('should use the custom storage class for user files', async function () {
  1274. const key = `${projectId}/${new ObjectId()}`
  1275. await app.persistor.sendStream(
  1276. Settings.filestore.stores.user_files,
  1277. key,
  1278. Stream.Readable.from(['hello'])
  1279. )
  1280. const sc = AWS_S3_USER_FILES_STORAGE_CLASS
  1281. expect(sc).to.exist
  1282. expect(
  1283. await app.persistor.getObjectStorageClass(
  1284. Settings.filestore.stores.user_files,
  1285. key
  1286. )
  1287. ).to.equal(sc)
  1288. })
  1289. })
  1290. }
  1291. describe('autoGunzip', function () {
  1292. let key
  1293. beforeEach('new key', function () {
  1294. key = `${projectId}/${new ObjectId().toString()}`
  1295. })
  1296. this.timeout(60 * 1000)
  1297. const body = Buffer.alloc(10 * 1024 * 1024, 'hello')
  1298. const gzippedBody = gzipSync(body)
  1299. /**
  1300. * @param {string} key
  1301. * @param {Buffer} wantBody
  1302. * @param {boolean} autoGunzip
  1303. * @return {Promise<void>}
  1304. */
  1305. async function checkBodyIsTheSame(key, wantBody, autoGunzip) {
  1306. const s = await app.persistor.getObjectStream(
  1307. Settings.filestore.stores.user_files,
  1308. key,
  1309. { autoGunzip }
  1310. )
  1311. const buf = new WritableBuffer()
  1312. await Stream.promises.pipeline(s, buf)
  1313. expect(buf.getContents()).to.deep.equal(wantBody)
  1314. }
  1315. if (backendSettings.backend === 'fs') {
  1316. it('should refuse to handle autoGunzip', async function () {
  1317. await expect(
  1318. app.persistor.getObjectStream(
  1319. Settings.filestore.stores.user_files,
  1320. key,
  1321. { autoGunzip: true }
  1322. )
  1323. ).to.be.rejectedWith(NotImplementedError)
  1324. })
  1325. } else {
  1326. it('should return the raw body with gzip', async function () {
  1327. await app.persistor.sendStream(
  1328. Settings.filestore.stores.user_files,
  1329. key,
  1330. Stream.Readable.from([gzippedBody]),
  1331. { contentEncoding: 'gzip' }
  1332. )
  1333. expect(
  1334. await app.persistor.getObjectSize(
  1335. Settings.filestore.stores.user_files,
  1336. key
  1337. )
  1338. ).to.equal(gzippedBody.byteLength)
  1339. // raw body with autoGunzip=true
  1340. await checkBodyIsTheSame(key, body, true)
  1341. // gzip body without autoGunzip=false
  1342. await checkBodyIsTheSame(key, gzippedBody, false)
  1343. })
  1344. it('should return the raw body without gzip compression', async function () {
  1345. await app.persistor.sendStream(
  1346. Settings.filestore.stores.user_files,
  1347. key,
  1348. Stream.Readable.from([body])
  1349. )
  1350. expect(
  1351. await app.persistor.getObjectSize(
  1352. Settings.filestore.stores.user_files,
  1353. key
  1354. )
  1355. ).to.equal(body.byteLength)
  1356. // raw body with both autoGunzip options
  1357. await checkBodyIsTheSame(key, body, true)
  1358. await checkBodyIsTheSame(key, body, false)
  1359. })
  1360. it('should return the gzip body without gzip header', async function () {
  1361. await app.persistor.sendStream(
  1362. Settings.filestore.stores.user_files,
  1363. key,
  1364. Stream.Readable.from([gzippedBody])
  1365. )
  1366. expect(
  1367. await app.persistor.getObjectSize(
  1368. Settings.filestore.stores.user_files,
  1369. key
  1370. )
  1371. ).to.equal(gzippedBody.byteLength)
  1372. // gzip body with both autoGunzip options
  1373. await checkBodyIsTheSame(key, gzippedBody, true)
  1374. await checkBodyIsTheSame(key, gzippedBody, false)
  1375. })
  1376. }
  1377. })
  1378. })
  1379. }
  1380. })