back_fill_deleted_files.mjs 3.3 KB

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