backupVerifier.mjs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. // @ts-check
  2. import OError from '@overleaf/o-error'
  3. import chunkStore from '../lib/chunk_store/index.js'
  4. import {
  5. backupPersistor,
  6. chunksBucket,
  7. projectBlobsBucket,
  8. } from './backupPersistor.mjs'
  9. import { Blob, Chunk, History } from 'overleaf-editor-core'
  10. import { BlobStore, GLOBAL_BLOBS, makeProjectKey } from './blob_store/index.js'
  11. import blobHash from './blob_hash.js'
  12. import { NotFoundError } from '@overleaf/object-persistor/src/Errors.js'
  13. import logger from '@overleaf/logger'
  14. import path from 'node:path'
  15. import projectKey from './project_key.js'
  16. import streams from './streams.js'
  17. import objectPersistor from '@overleaf/object-persistor'
  18. import { getEndDateForRPO } from '../../backupVerifier/utils.mjs'
  19. /**
  20. * @typedef {import("@overleaf/object-persistor/src/PerProjectEncryptedS3Persistor.js").CachedPerProjectEncryptedS3Persistor} CachedPerProjectEncryptedS3Persistor
  21. */
  22. /**
  23. * @param {string} historyId
  24. * @param {string} hash
  25. */
  26. export async function verifyBlob(historyId, hash) {
  27. return await verifyBlobs(historyId, [hash])
  28. }
  29. /**
  30. *
  31. * @param {string} historyId
  32. * @return {Promise<CachedPerProjectEncryptedS3Persistor>}
  33. */
  34. async function getProjectPersistor(historyId) {
  35. try {
  36. return await backupPersistor.forProjectRO(
  37. projectBlobsBucket,
  38. makeProjectKey(historyId, '')
  39. )
  40. } catch (err) {
  41. if (err instanceof NotFoundError) {
  42. throw new BackupCorruptedError('dek does not exist', {}, err)
  43. }
  44. throw err
  45. }
  46. }
  47. /**
  48. * @param {string} historyId
  49. * @param {Array<string>} hashes
  50. * @param {CachedPerProjectEncryptedS3Persistor} [projectCache]
  51. */
  52. export async function verifyBlobs(historyId, hashes, projectCache) {
  53. if (hashes.length === 0) throw new Error('bug: empty hashes')
  54. if (!projectCache) {
  55. projectCache = await getProjectPersistor(historyId)
  56. }
  57. const blobStore = new BlobStore(historyId)
  58. for (const hash of hashes) {
  59. const path = makeProjectKey(historyId, hash)
  60. const blob = await blobStore.getBlob(hash)
  61. if (!blob) throw new Blob.NotFoundError(hash)
  62. let stream
  63. try {
  64. stream = await projectCache.getObjectStream(projectBlobsBucket, path, {
  65. autoGunzip: true,
  66. })
  67. } catch (err) {
  68. if (err instanceof NotFoundError) {
  69. throw new BackupCorruptedMissingBlobError('missing blob', {
  70. path,
  71. hash,
  72. })
  73. }
  74. throw err
  75. }
  76. const backupHash = await blobHash.fromStream(blob.getByteLength(), stream)
  77. if (backupHash !== hash) {
  78. throw new BackupCorruptedInvalidBlobError(
  79. 'hash mismatch for backed up blob',
  80. {
  81. path,
  82. hash,
  83. backupHash,
  84. }
  85. )
  86. }
  87. }
  88. }
  89. /**
  90. * @param {string} historyId
  91. * @param {Date} [endTimestamp]
  92. */
  93. export async function verifyProjectWithErrorContext(
  94. historyId,
  95. endTimestamp = getEndDateForRPO()
  96. ) {
  97. try {
  98. await verifyProject(historyId, endTimestamp)
  99. } catch (err) {
  100. // @ts-ignore err is Error instance
  101. throw OError.tag(err, 'verifyProject', { historyId, endTimestamp })
  102. }
  103. }
  104. /**
  105. *
  106. * @param {string} historyId
  107. * @param {number} startVersion
  108. * @param {CachedPerProjectEncryptedS3Persistor} backupPersistorForProject
  109. * @return {Promise<any>}
  110. */
  111. export async function loadChunk(
  112. historyId,
  113. startVersion,
  114. backupPersistorForProject
  115. ) {
  116. const key = path.join(
  117. projectKey.format(historyId),
  118. projectKey.pad(startVersion)
  119. )
  120. try {
  121. const buf = await streams.gunzipStreamToBuffer(
  122. await backupPersistorForProject.getObjectStream(chunksBucket, key)
  123. )
  124. return JSON.parse(buf.toString('utf-8'))
  125. } catch (err) {
  126. if (err instanceof objectPersistor.Errors.NotFoundError) {
  127. throw new Chunk.NotPersistedError(historyId)
  128. }
  129. if (err instanceof Error) {
  130. throw OError.tag(err, 'Failed to load chunk', { historyId, startVersion })
  131. }
  132. throw err
  133. }
  134. }
  135. /**
  136. * @param {string} historyId
  137. * @param {Date} endTimestamp
  138. */
  139. export async function verifyProject(historyId, endTimestamp) {
  140. const backend = chunkStore.getBackend(historyId)
  141. const [first, last] = await Promise.all([
  142. backend.getFirstChunkBeforeTimestamp(historyId, endTimestamp),
  143. backend.getLastActiveChunkBeforeTimestamp(historyId, endTimestamp),
  144. ])
  145. const chunksRecordsToVerify = [
  146. {
  147. chunkId: first.id,
  148. chunkLabel: 'first',
  149. },
  150. ]
  151. if (first.startVersion !== last.startVersion) {
  152. chunksRecordsToVerify.push({
  153. chunkId: last.id,
  154. chunkLabel: 'last before RPO',
  155. })
  156. }
  157. const projectCache = await getProjectPersistor(historyId)
  158. const chunks = await Promise.all(
  159. chunksRecordsToVerify.map(async chunk => {
  160. try {
  161. return History.fromRaw(
  162. await loadChunk(historyId, chunk.startVersion, projectCache)
  163. )
  164. } catch (err) {
  165. if (err instanceof Chunk.NotPersistedError) {
  166. throw new BackupRPOViolationChunkNotBackedUpError(
  167. 'BackupRPOviolation: chunk not backed up',
  168. chunk
  169. )
  170. }
  171. throw err
  172. }
  173. })
  174. )
  175. const seenBlobs = new Set()
  176. const blobsToVerify = []
  177. for (const chunk of chunks) {
  178. /** @type {Set<string>} */
  179. const chunkBlobs = new Set()
  180. chunk.findBlobHashes(chunkBlobs)
  181. let hasAddedBlobFromThisChunk = false
  182. for (const blobHash of chunkBlobs) {
  183. if (seenBlobs.has(blobHash)) continue // old blob
  184. if (GLOBAL_BLOBS.has(blobHash)) continue // global blob
  185. seenBlobs.add(blobHash)
  186. if (!hasAddedBlobFromThisChunk) {
  187. blobsToVerify.push(blobHash)
  188. hasAddedBlobFromThisChunk = true
  189. }
  190. }
  191. }
  192. if (blobsToVerify.length === 0) {
  193. logger.debug(
  194. {
  195. historyId,
  196. chunksRecordsToVerify: chunksRecordsToVerify.map(c => c.chunkId),
  197. },
  198. 'chunks contain no blobs to verify'
  199. )
  200. return
  201. }
  202. await verifyBlobs(historyId, blobsToVerify, projectCache)
  203. }
  204. export class BackupCorruptedError extends OError {}
  205. export class BackupRPOViolationError extends OError {}
  206. export class BackupCorruptedMissingBlobError extends BackupCorruptedError {}
  207. export class BackupCorruptedInvalidBlobError extends BackupCorruptedError {}
  208. export class BackupRPOViolationChunkNotBackedUpError extends OError {}