zip_store.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. 'use strict'
  2. const BPromise = require('bluebird')
  3. const config = require('config')
  4. const fs = require('node:fs')
  5. const path = require('node:path')
  6. const OError = require('@overleaf/o-error')
  7. const objectPersistor = require('@overleaf/object-persistor')
  8. const assert = require('./assert')
  9. const { BlobStore } = require('./blob_store')
  10. const persistor = require('./persistor')
  11. const ProjectArchive = require('./project_archive')
  12. const projectKey = require('@overleaf/object-persistor/src/ProjectKey.js')
  13. const temp = require('./temp')
  14. const BUCKET = config.get('zipStore.bucket')
  15. function getZipKey(projectId, version) {
  16. return path.join(
  17. projectKey.format(projectId),
  18. version.toString(),
  19. 'project.zip'
  20. )
  21. }
  22. /**
  23. * Store a zip of a given version of a project in bucket.
  24. *
  25. * @class
  26. */
  27. class ZipStore {
  28. /**
  29. * Generate signed link to access the zip file.
  30. *
  31. * @param {number | string} projectId
  32. * @param {number} version
  33. * @return {string}
  34. */
  35. async getSignedUrl(projectId, version) {
  36. assert.projectId(projectId, 'bad projectId')
  37. assert.integer(version, 'bad version')
  38. const key = getZipKey(projectId, version)
  39. return await persistor.getRedirectUrl(BUCKET, key)
  40. }
  41. /**
  42. * Generate a zip of the given snapshot.
  43. *
  44. * @param {number | string} projectId
  45. * @param {number} version
  46. * @param {Snapshot} snapshot
  47. */
  48. async storeZip(projectId, version, snapshot) {
  49. assert.projectId(projectId, 'bad projectId')
  50. assert.integer(version, 'bad version')
  51. assert.object(snapshot, 'bad snapshot')
  52. const zipKey = getZipKey(projectId, version)
  53. if (await isZipPresent()) return
  54. await BPromise.using(temp.open('zip'), async tempFileInfo => {
  55. await zipSnapshot(tempFileInfo.path, snapshot)
  56. await uploadZip(tempFileInfo.path)
  57. })
  58. // If the file is already there, we don't need to build the zip again. If we
  59. // just HEAD the file, there's a race condition, because the zip files
  60. // automatically expire. So, we try to copy the file from itself to itself,
  61. // and if it fails, we know the file didn't exist. If it succeeds, this has
  62. // the effect of re-extending its lifetime.
  63. async function isZipPresent() {
  64. try {
  65. await persistor.copyObject(BUCKET, zipKey, zipKey)
  66. return true
  67. } catch (error) {
  68. if (!(error instanceof objectPersistor.Errors.NotFoundError)) {
  69. console.error(
  70. 'storeZip: isZipPresent: unexpected error (except in dev): %s',
  71. error
  72. )
  73. }
  74. return false
  75. }
  76. }
  77. async function zipSnapshot(tempPathname, snapshot) {
  78. const blobStore = new BlobStore(projectId)
  79. const zipTimeoutMs = parseInt(config.get('zipStore.zipTimeoutMs'), 10)
  80. const archive = new ProjectArchive(snapshot, zipTimeoutMs)
  81. try {
  82. await archive.writeZip(blobStore, tempPathname)
  83. } catch (err) {
  84. throw new ZipStore.CreationError(projectId, version).withCause(err)
  85. }
  86. }
  87. async function uploadZip(tempPathname, snapshot) {
  88. const stream = fs.createReadStream(tempPathname)
  89. try {
  90. await persistor.sendStream(BUCKET, zipKey, stream, {
  91. contentType: 'application/zip',
  92. })
  93. } catch (err) {
  94. throw new ZipStore.UploadError(projectId, version).withCause(err)
  95. }
  96. }
  97. }
  98. }
  99. class CreationError extends OError {
  100. constructor(projectId, version) {
  101. super(`Zip creation failed for ${projectId} version ${version}`, {
  102. projectId,
  103. version,
  104. })
  105. }
  106. }
  107. ZipStore.CreationError = CreationError
  108. class UploadError extends OError {
  109. constructor(projectId, version) {
  110. super(`Zip upload failed for ${projectId} version ${version}`, {
  111. projectId,
  112. version,
  113. })
  114. }
  115. }
  116. ZipStore.UploadError = UploadError
  117. module.exports = new ZipStore()