back_fill_deleted_files.mjs 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import { batchedUpdate } from '@overleaf/mongo-utils/batchedUpdate.js'
  2. import { promiseMapWithLimit, promisify } from '@overleaf/promise-utils'
  3. import { db } from '../app/src/infrastructure/mongodb.js'
  4. import _ from 'lodash'
  5. import { fileURLToPath } from 'node:url'
  6. const sleep = promisify(setTimeout)
  7. async function main(options) {
  8. if (!options) {
  9. options = {}
  10. }
  11. _.defaults(options, {
  12. writeConcurrency: parseInt(process.env.WRITE_CONCURRENCY, 10) || 10,
  13. performCleanup: process.argv.includes('--perform-cleanup'),
  14. fixPartialInserts: process.argv.includes('--fix-partial-inserts'),
  15. letUserDoubleCheckInputsFor: parseInt(
  16. process.env.LET_USER_DOUBLE_CHECK_INPUTS_FOR || 10 * 1000,
  17. 10
  18. ),
  19. })
  20. await letUserDoubleCheckInputs(options)
  21. await batchedUpdate(
  22. db.projects,
  23. // array is not empty ~ array has one item
  24. { 'deletedFiles.0': { $exists: true } },
  25. async projects => {
  26. await processBatch(projects, options)
  27. },
  28. { _id: 1, deletedFiles: 1 }
  29. )
  30. }
  31. async function processBatch(projects, options) {
  32. await promiseMapWithLimit(
  33. options.writeConcurrency,
  34. projects,
  35. async project => {
  36. await processProject(project, options)
  37. }
  38. )
  39. }
  40. async function processProject(project, options) {
  41. await backFillFiles(project, options)
  42. if (options.performCleanup) {
  43. await cleanupProject(project)
  44. }
  45. }
  46. async function backFillFiles(project, options) {
  47. const projectId = project._id
  48. filterDuplicatesInPlace(project)
  49. project.deletedFiles.forEach(file => {
  50. file.projectId = projectId
  51. })
  52. if (options.fixPartialInserts) {
  53. await fixPartialInserts(project)
  54. } else {
  55. await db.deletedFiles.insertMany(project.deletedFiles)
  56. }
  57. }
  58. function filterDuplicatesInPlace(project) {
  59. const fileIds = new Set()
  60. project.deletedFiles = project.deletedFiles.filter(file => {
  61. const id = file._id.toString()
  62. if (fileIds.has(id)) return false
  63. fileIds.add(id)
  64. return true
  65. })
  66. }
  67. async function fixPartialInserts(project) {
  68. const seenFileIds = new Set(
  69. (
  70. await db.deletedFiles
  71. .find(
  72. { _id: { $in: project.deletedFiles.map(file => file._id) } },
  73. { projection: { _id: 1 } }
  74. )
  75. .toArray()
  76. ).map(file => file._id.toString())
  77. )
  78. project.deletedFiles = project.deletedFiles.filter(file => {
  79. const id = file._id.toString()
  80. if (seenFileIds.has(id)) return false
  81. seenFileIds.add(id)
  82. return true
  83. })
  84. if (project.deletedFiles.length > 0) {
  85. await db.deletedFiles.insertMany(project.deletedFiles)
  86. }
  87. }
  88. async function cleanupProject(project) {
  89. await db.projects.updateOne(
  90. { _id: project._id },
  91. { $set: { deletedFiles: [] } }
  92. )
  93. }
  94. async function letUserDoubleCheckInputs(options) {
  95. if (options.performCleanup) {
  96. console.error('BACK FILLING AND PERFORMING CLEANUP')
  97. } else {
  98. console.error(
  99. 'BACK FILLING ONLY - You will need to rerun with --perform-cleanup'
  100. )
  101. }
  102. console.error(
  103. 'Waiting for you to double check inputs for',
  104. options.letUserDoubleCheckInputsFor,
  105. 'ms'
  106. )
  107. await sleep(options.letUserDoubleCheckInputsFor)
  108. }
  109. export default main
  110. if (fileURLToPath(import.meta.url) === process.argv[1]) {
  111. try {
  112. await main()
  113. process.exit(0)
  114. } catch (error) {
  115. console.error({ error })
  116. process.exit(1)
  117. }
  118. }