verify_backed_up_blobs.mjs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. // @ts-check
  2. import { ObjectId } from 'mongodb'
  3. import knex from '../lib/knex.js'
  4. import {
  5. batchedUpdate,
  6. objectIdFromInput,
  7. READ_PREFERENCE_SECONDARY,
  8. } from '@overleaf/mongo-utils/batchedUpdate.js'
  9. import {
  10. GLOBAL_BLOBS,
  11. loadGlobalBlobs,
  12. makeProjectKey,
  13. } from '../lib/blob_store/index.js'
  14. import {
  15. backedUpBlobs as backedUpBlobsCollection,
  16. db,
  17. client,
  18. } from '../lib/mongodb.js'
  19. import redis from '../lib/redis.js'
  20. import commandLineArgs from 'command-line-args'
  21. import fs from 'node:fs'
  22. const projectsCollection = db.collection('projects')
  23. // Enable caching for ObjectId.toString()
  24. ObjectId.cacheHexString = true
  25. function parseArgs() {
  26. const PUBLIC_LAUNCH_DATE = new Date('2012-01-01T00:00:00Z')
  27. const args = commandLineArgs([
  28. {
  29. name: 'BATCH_RANGE_START',
  30. type: String,
  31. defaultValue: PUBLIC_LAUNCH_DATE.toISOString(),
  32. },
  33. {
  34. name: 'BATCH_RANGE_END',
  35. type: String,
  36. defaultValue: new Date().toISOString(),
  37. },
  38. {
  39. name: 'output',
  40. type: String,
  41. alias: 'o',
  42. },
  43. ])
  44. const BATCH_RANGE_START = objectIdFromInput(
  45. args['BATCH_RANGE_START']
  46. ).toString()
  47. const BATCH_RANGE_END = objectIdFromInput(args['BATCH_RANGE_END']).toString()
  48. if (!args['output']) {
  49. throw new Error('missing --output')
  50. }
  51. const OUTPUT_STREAM = fs.createWriteStream(args['output'])
  52. return {
  53. BATCH_RANGE_START,
  54. BATCH_RANGE_END,
  55. OUTPUT_STREAM,
  56. }
  57. }
  58. const { BATCH_RANGE_START, BATCH_RANGE_END, OUTPUT_STREAM } = parseArgs()
  59. // We need to handle the start and end differently as ids of deleted projects are created at time of deletion.
  60. if (process.env.BATCH_RANGE_START || process.env.BATCH_RANGE_END) {
  61. throw new Error('use --BATCH_RANGE_START and --BATCH_RANGE_END')
  62. }
  63. let gracefulShutdownInitiated = false
  64. process.on('SIGINT', handleSignal)
  65. process.on('SIGTERM', handleSignal)
  66. function handleSignal() {
  67. gracefulShutdownInitiated = true
  68. console.warn('graceful shutdown initiated, draining queue')
  69. }
  70. async function processBatch(batch) {
  71. if (gracefulShutdownInitiated) {
  72. throw new Error('graceful shutdown: aborting batch processing')
  73. }
  74. const N = batch.length
  75. const firstId = batch[0]._id
  76. const lastId = batch[N - 1]._id
  77. const projectCursor = await projectsCollection.find(
  78. { _id: { $gte: firstId, $lte: lastId } },
  79. {
  80. projection: { _id: 1, 'overleaf.history.id': 1, lastUpdated: 1 },
  81. readPreference: READ_PREFERENCE_SECONDARY,
  82. }
  83. )
  84. const projectMap = new Map()
  85. for await (const project of projectCursor) {
  86. projectMap.set(project._id.toString(), project)
  87. }
  88. for (const project of batch) {
  89. const projectId = project._id.toString()
  90. const projectRecord = projectMap.get(projectId)
  91. if (!projectRecord) {
  92. console.error(`project not found: ${projectId}`)
  93. continue
  94. }
  95. if (!projectRecord.overleaf?.history?.id) {
  96. console.error(`project missing history: ${projectId}`)
  97. continue
  98. }
  99. const historyId = projectRecord.overleaf.history.id.toString()
  100. const prefix = `${projectId},${projectRecord.lastUpdated.toISOString()},`
  101. const hashes = project.blobs.map(blob => blob.toString('hex'))
  102. const projectBlobHashes = hashes.filter(hash => !GLOBAL_BLOBS.has(hash))
  103. if (projectBlobHashes.length < hashes.length) {
  104. console.warn(
  105. `project ${projectId} has ${hashes.length - projectBlobHashes.length} global blobs`
  106. )
  107. }
  108. const rows = projectBlobHashes.map(
  109. hash => prefix + makeProjectKey(historyId, hash) + '\n'
  110. )
  111. OUTPUT_STREAM.write(rows.join(''))
  112. }
  113. }
  114. async function main() {
  115. await loadGlobalBlobs()
  116. OUTPUT_STREAM.write('projectId,lastUpdated,path\n')
  117. await batchedUpdate(
  118. backedUpBlobsCollection,
  119. {},
  120. processBatch,
  121. {},
  122. {},
  123. { BATCH_RANGE_START, BATCH_RANGE_END }
  124. )
  125. }
  126. main()
  127. .then(() => console.log('Done.'))
  128. .catch(err => {
  129. console.error('Error:', err)
  130. process.exitCode = 1
  131. })
  132. .finally(() => {
  133. knex.destroy().catch(err => {
  134. console.error('Error closing Postgres connection:', err)
  135. })
  136. client.close().catch(err => console.error('Error closing MongoDB:', err))
  137. redis.disconnect().catch(err => {
  138. console.error('Error disconnecting Redis:', err)
  139. })
  140. })