find_malformed_filetrees.mjs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. // @ts-check
  2. import { db, ObjectId } from '../app/src/infrastructure/mongodb.js'
  3. import { batchedUpdate } from '@overleaf/mongo-utils/batchedUpdate.js'
  4. import { scriptRunner } from './lib/ScriptRunner.mjs'
  5. /**
  6. * @typedef {Object} Doc
  7. * @property {ObjectId} _id
  8. * @property {string} name
  9. */
  10. /**
  11. * @typedef {Object} FileRef
  12. * @property {ObjectId} _id
  13. * @property {string} name
  14. * @property {string} hash
  15. */
  16. /**
  17. * @typedef {Object} Folder
  18. * @property {ObjectId} _id
  19. * @property {string} name
  20. * @property {Array<Doc>} docs
  21. * @property {Array<Folder>} folders
  22. * @property {Array<FileRef>} fileRefs
  23. */
  24. /**
  25. * @typedef {Object} Project
  26. * @property {ObjectId} _id
  27. * @property {Array<Folder>} rootFolder
  28. */
  29. /**
  30. * @param {(progress: string) => Promise<void>} trackProgress
  31. * @returns {Promise<void>}
  32. * @async
  33. */
  34. async function main(trackProgress) {
  35. let projectsProcessed = 0
  36. await batchedUpdate(
  37. db.projects,
  38. {},
  39. /**
  40. * @param {Array<Project>} projects
  41. * @return {Promise<void>}
  42. */
  43. async function projects(projects) {
  44. for (const project of projects) {
  45. projectsProcessed += 1
  46. if (projectsProcessed % 100000 === 0) {
  47. console.log(projectsProcessed, 'projects processed')
  48. }
  49. const projectId = project._id.toString()
  50. for (const { reason, path, _id } of processProject(project)) {
  51. console.log(
  52. JSON.stringify({
  53. msg: 'bad file-tree path',
  54. projectId,
  55. reason,
  56. path,
  57. _id,
  58. })
  59. )
  60. }
  61. }
  62. },
  63. { _id: 1, rootFolder: 1 },
  64. undefined,
  65. { trackProgress }
  66. )
  67. }
  68. /**
  69. * @param {Project} project
  70. * @return {Generator<{path: string, reason: string, _id: any}, void, *>}
  71. */
  72. function* processProject(project) {
  73. if (!project.rootFolder || !Array.isArray(project.rootFolder)) {
  74. yield { reason: 'bad rootFolder', path: 'rootFolder', _id: null }
  75. } else if (!project.rootFolder[0]) {
  76. yield { reason: 'missing rootFolder', path: 'rootFolder.0', _id: null }
  77. } else {
  78. for (const { path, reason, _id } of findBadPaths(project.rootFolder[0])) {
  79. yield { reason, path: `rootFolder.0${path}`, _id }
  80. }
  81. }
  82. }
  83. /**
  84. * @param {Folder} folder
  85. * @return {Generator<{path: string, reason: string, _id: any}, void, *>}
  86. */
  87. function* findBadPaths(folder) {
  88. const folderId = folder._id
  89. if (!(folderId instanceof ObjectId)) {
  90. yield { path: '._id', reason: 'bad folder id', _id: folderId }
  91. }
  92. if (typeof folder.name !== 'string' || !folder.name) {
  93. yield { path: '.name', reason: 'bad folder name', _id: folderId }
  94. }
  95. if (folder.folders && Array.isArray(folder.folders)) {
  96. for (const [i, subfolder] of folder.folders.entries()) {
  97. if (!subfolder || typeof subfolder !== 'object') {
  98. yield { path: `.folders.${i}`, reason: 'bad folder', _id: folderId }
  99. continue
  100. }
  101. for (const { path, reason, _id } of findBadPaths(subfolder)) {
  102. yield { path: `.folders.${i}${path}`, reason, _id }
  103. }
  104. }
  105. } else {
  106. yield { path: '.folders', reason: 'missing .folders', _id: folderId }
  107. }
  108. if (folder.docs && Array.isArray(folder.docs)) {
  109. for (const [i, doc] of folder.docs.entries()) {
  110. if (!doc || typeof doc !== 'object') {
  111. yield { path: `.docs.${i}`, reason: 'bad doc', _id: folderId }
  112. continue
  113. }
  114. const docId = doc._id
  115. if (!(docId instanceof ObjectId)) {
  116. yield { path: `.docs.${i}._id`, reason: 'bad doc id', _id: docId }
  117. // no need to check further: this doc can be deleted
  118. continue
  119. }
  120. if (typeof doc.name !== 'string' || !doc.name) {
  121. yield { path: `.docs.${i}.name`, reason: 'bad doc name', _id: docId }
  122. }
  123. }
  124. } else {
  125. yield { path: '.docs', reason: 'missing .docs', _id: folderId }
  126. }
  127. if (folder.fileRefs && Array.isArray(folder.fileRefs)) {
  128. for (const [i, file] of folder.fileRefs.entries()) {
  129. if (!file || typeof file !== 'object') {
  130. yield { path: `.fileRefs.${i}`, reason: 'bad file', _id: folderId }
  131. continue
  132. }
  133. const fileId = file._id
  134. if (!(fileId instanceof ObjectId)) {
  135. yield { path: `.fileRefs.${i}._id`, reason: 'bad file id', _id: fileId }
  136. // no need to check further: this file can be deleted
  137. continue
  138. }
  139. if (typeof file.name !== 'string' || !file.name) {
  140. yield {
  141. path: `.fileRefs.${i}.name`,
  142. reason: 'bad file name',
  143. _id: fileId,
  144. }
  145. }
  146. if (typeof file.hash !== 'string' || !file.hash) {
  147. yield {
  148. path: `.fileRefs.${i}.hash`,
  149. reason: 'bad file hash',
  150. _id: fileId,
  151. }
  152. }
  153. }
  154. } else {
  155. yield { path: '.fileRefs', reason: 'missing .fileRefs', _id: folderId }
  156. }
  157. }
  158. try {
  159. await scriptRunner(main)
  160. process.exit(0)
  161. } catch (error) {
  162. console.error(error)
  163. process.exit(1)
  164. }