find_malformed_filetrees.mjs 4.6 KB

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