backupVerifier.mjs 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. // @ts-check
  2. import config from 'config'
  3. import OError from '@overleaf/o-error'
  4. import { backupPersistor, projectBlobsBucket } from './backupPersistor.mjs'
  5. import { Blob } from 'overleaf-editor-core'
  6. import { BlobStore, makeProjectKey } from './blob_store/index.js'
  7. import blobHash from './blob_hash.js'
  8. import { NotFoundError } from '@overleaf/object-persistor/src/Errors.js'
  9. /**
  10. * @typedef {import("@overleaf/object-persistor/src/PerProjectEncryptedS3Persistor").CachedPerProjectEncryptedS3Persistor} CachedPerProjectEncryptedS3Persistor
  11. */
  12. /**
  13. * @param {string} historyId
  14. * @param {string} hash
  15. */
  16. export async function verifyBlob(historyId, hash) {
  17. return await verifyBlobs(historyId, [hash])
  18. }
  19. /**
  20. * @param {string} historyId
  21. * @param {Array<string>} hashes
  22. */
  23. export async function verifyBlobs(historyId, hashes) {
  24. let projectCache
  25. try {
  26. projectCache = await backupPersistor.forProjectRO(
  27. projectBlobsBucket,
  28. makeProjectKey(historyId, '')
  29. )
  30. } catch (err) {
  31. if (err instanceof NotFoundError) {
  32. throw new BackupCorruptedError('dek does not exist', {}, err)
  33. }
  34. throw err
  35. }
  36. await verifyBlobsWithCache(historyId, projectCache, hashes)
  37. }
  38. /**
  39. * @param {string} historyId
  40. * @param {CachedPerProjectEncryptedS3Persistor} projectCache
  41. * @param {Array<string>} hashes
  42. */
  43. export async function verifyBlobsWithCache(historyId, projectCache, hashes) {
  44. if (hashes.length === 0) throw new Error('bug: empty hashes')
  45. const blobStore = new BlobStore(historyId)
  46. for (const hash of hashes) {
  47. const path = makeProjectKey(historyId, hash)
  48. const blob = await blobStore.getBlob(hash)
  49. if (!blob) throw new Blob.NotFoundError(hash)
  50. let stream
  51. try {
  52. stream = await projectCache.getObjectStream(projectBlobsBucket, path, {
  53. autoGunzip: true,
  54. })
  55. } catch (err) {
  56. if (err instanceof NotFoundError) {
  57. throw new BackupCorruptedError('missing blob')
  58. }
  59. throw err
  60. }
  61. const backupHash = await blobHash.fromStream(blob.getByteLength(), stream)
  62. if (backupHash !== hash) {
  63. throw new BackupCorruptedError('hash mismatch for backed up blob', {
  64. path,
  65. hash,
  66. backupHash,
  67. })
  68. }
  69. }
  70. }
  71. export class BackupCorruptedError extends OError {}
  72. export async function healthCheck() {
  73. /** @type {Array<string>} */
  74. const HEALTH_CHECK_BLOBS = JSON.parse(config.get('healthCheckBlobs'))
  75. if (HEALTH_CHECK_BLOBS.length !== 2) {
  76. throw new Error('expected 2 healthCheckBlobs')
  77. }
  78. if (!HEALTH_CHECK_BLOBS.some(path => path.split('/')[0].length === 24)) {
  79. throw new Error('expected mongo id in healthCheckBlobs')
  80. }
  81. if (!HEALTH_CHECK_BLOBS.some(path => path.split('/')[0].length < 24)) {
  82. throw new Error('expected postgres id in healthCheckBlobs')
  83. }
  84. if (HEALTH_CHECK_BLOBS.some(path => path.split('/')[1]?.length !== 40)) {
  85. throw new Error('expected hash in healthCheckBlobs')
  86. }
  87. for (const path of HEALTH_CHECK_BLOBS) {
  88. const [historyId, hash] = path.split('/')
  89. await verifyBlob(historyId, hash)
  90. }
  91. }