check_project_files.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. const Path = require('path')
  2. const DocstoreManager = require('../app/src/Features/Docstore/DocstoreManager')
  3. const DocumentUpdaterHandler = require('../app/src/Features/DocumentUpdater/DocumentUpdaterHandler')
  4. const FileStoreHandler = require('../app/src/Features/FileStore/FileStoreHandler')
  5. const ProjectGetter = require('../app/src/Features/Project/ProjectGetter')
  6. const ProjectEntityMongoUpdateHandler = require('../app/src/Features/Project/ProjectEntityMongoUpdateHandler')
  7. const { waitForDb, db, ObjectId } = require('../app/src/infrastructure/mongodb')
  8. const logger = require('@overleaf/logger').logger
  9. const args = require('minimist')(process.argv.slice(2), {
  10. boolean: ['verbose', 'fix'],
  11. })
  12. const verbose = args.verbose
  13. if (!verbose) {
  14. logger.level('error')
  15. }
  16. // no remaining arguments, print usage
  17. if (args._.length === 0) {
  18. console.log(
  19. 'Usage: node services/web/scripts/check_project_docs.js [--verbose] [--fix] <projectId>...'
  20. )
  21. process.exit(1)
  22. }
  23. function logDoc(projectId, path, doc, message = '') {
  24. console.log(
  25. 'projectId:',
  26. projectId,
  27. 'doc:',
  28. JSON.stringify({
  29. _id: doc._id,
  30. name: doc.name,
  31. lines: doc.lines ? doc.lines.join('\n').length : 0,
  32. rev: doc.rev,
  33. version: doc.version,
  34. ranges: typeof doc.ranges,
  35. }),
  36. path,
  37. message
  38. )
  39. }
  40. function logFile(projectId, path, file, message = '') {
  41. console.log(
  42. 'projectId:',
  43. projectId,
  44. 'file:',
  45. JSON.stringify({
  46. _id: file._id,
  47. name: file.name,
  48. linkedFileData: file.linkedFileData,
  49. hash: file.hash,
  50. size: file.size,
  51. }),
  52. path,
  53. message
  54. )
  55. }
  56. function findPathCounts(projectId, docEntries, fileEntries) {
  57. const pathCounts = new Map()
  58. const docPaths = docEntries.map(({ path }) => path)
  59. const filePaths = fileEntries.map(({ path }) => path)
  60. const allPaths = docPaths.concat(filePaths)
  61. for (const path of allPaths) {
  62. pathCounts.set(path, (pathCounts.get(path) || 0) + 1)
  63. }
  64. return pathCounts
  65. }
  66. // copied from services/web/app/src/Features/Project/ProjectDuplicator.js
  67. function _getFolderEntries(folder, folderPath = '/') {
  68. const docEntries = []
  69. const fileEntries = []
  70. const docs = folder.docs || []
  71. const files = folder.fileRefs || []
  72. const subfolders = folder.folders || []
  73. for (const doc of docs) {
  74. if (doc == null || doc._id == null) {
  75. continue
  76. }
  77. const path = Path.join(folderPath, doc.name)
  78. docEntries.push({ doc, path })
  79. }
  80. for (const file of files) {
  81. if (file == null || file._id == null) {
  82. continue
  83. }
  84. const path = Path.join(folderPath, file.name)
  85. fileEntries.push({ file, path })
  86. }
  87. for (const subfolder of subfolders) {
  88. if (subfolder == null || subfolder._id == null) {
  89. continue
  90. }
  91. const subfolderPath = Path.join(folderPath, subfolder.name)
  92. const subfolderEntries = _getFolderEntries(subfolder, subfolderPath)
  93. for (const docEntry of subfolderEntries.docEntries) {
  94. docEntries.push(docEntry)
  95. }
  96. for (const fileEntry of subfolderEntries.fileEntries) {
  97. fileEntries.push(fileEntry)
  98. }
  99. }
  100. return { docEntries, fileEntries }
  101. }
  102. async function getDocsInMongo(projectId) {
  103. return await db.docs
  104. .find({ project_id: new ObjectId(projectId), deleted: { $ne: true } })
  105. .toArray()
  106. }
  107. function getDocIdsInFileTree(docEntries) {
  108. return docEntries.map(({ doc }) => doc._id.toString())
  109. }
  110. function findMissingDocs(docsInMongo, docIdsInFileTree) {
  111. const missingDocs = []
  112. for (const doc of docsInMongo) {
  113. const docId = doc._id.toString()
  114. if (!docIdsInFileTree.includes(docId)) {
  115. console.log(`Found doc in docstore not in project filetree:`, docId)
  116. missingDocs.push(doc)
  117. }
  118. }
  119. return missingDocs
  120. }
  121. async function createRecoveryFolder(projectId) {
  122. const recoveryFolder = `recovered-${Date.now()}`
  123. const { folder } = await ProjectEntityMongoUpdateHandler.promises.mkdirp(
  124. new ObjectId(projectId),
  125. recoveryFolder
  126. )
  127. console.log('Created recovery folder:', folder._id.toString())
  128. return folder
  129. }
  130. async function restoreMissingDocs(projectId, folder, missingDocs) {
  131. for (const doc of missingDocs) {
  132. doc.name = doc.name || `unknown-file-${doc._id.toString()}`
  133. try {
  134. await ProjectEntityMongoUpdateHandler.promises.addDoc(
  135. new ObjectId(projectId),
  136. folder._id,
  137. doc
  138. )
  139. console.log('Restored doc to filetree:', doc._id.toString())
  140. } catch (err) {
  141. console.log(`Error adding doc to filetree:`, err)
  142. }
  143. }
  144. }
  145. async function checkProject(projectId) {
  146. try {
  147. await DocumentUpdaterHandler.promises.flushProjectToMongo(projectId)
  148. } catch (err) {
  149. console.log(`Error flushing project ${projectId} to mongo: ${err}`)
  150. }
  151. const project = await ProjectGetter.promises.getProject(projectId, {
  152. rootFolder: true,
  153. rootDoc_id: true,
  154. })
  155. if (verbose) {
  156. console.log(`project: ${JSON.stringify(project)}`)
  157. }
  158. const { docEntries, fileEntries } = _getFolderEntries(project.rootFolder[0])
  159. console.log(
  160. `Found ${docEntries.length} docEntries and ${fileEntries.length} fileEntries`
  161. )
  162. const pathCounts = findPathCounts(projectId, docEntries, fileEntries)
  163. for (const [path, count] of pathCounts) {
  164. if (count > 1) {
  165. console.log(`Found duplicate path: ${path}`)
  166. }
  167. }
  168. let errors = 0
  169. for (const { doc, path } of docEntries) {
  170. try {
  171. const { lines, rev, version, ranges } =
  172. await DocstoreManager.promises.getDoc(projectId, doc._id)
  173. if (!lines) {
  174. throw new Error('no doclines')
  175. }
  176. if (pathCounts.get(path) > 1) {
  177. logDoc(
  178. projectId,
  179. path,
  180. { ...doc, lines, rev, version, ranges },
  181. 'duplicate path'
  182. )
  183. errors++
  184. } else if (verbose) {
  185. logDoc(projectId, path, { ...doc, lines, rev, version, ranges })
  186. }
  187. } catch (err) {
  188. logDoc(projectId, path, doc, err)
  189. errors++
  190. }
  191. }
  192. for (const { file, path } of fileEntries) {
  193. try {
  194. const fileSize = await FileStoreHandler.promises.getFileSize(
  195. projectId,
  196. file._id
  197. )
  198. if (pathCounts.get(path) > 1) {
  199. logFile(projectId, path, { ...file, fileSize }, 'duplicate path')
  200. errors++
  201. } else if (verbose) {
  202. logFile(projectId, path, { ...file, fileSize })
  203. }
  204. } catch (err) {
  205. logFile(projectId, path, file, err)
  206. errors++
  207. }
  208. }
  209. // now look for docs in the docstore that are not in the project filetree
  210. const docsInMongo = await getDocsInMongo(projectId)
  211. const docIdsInFileTree = getDocIdsInFileTree(docEntries)
  212. const missingDocs = findMissingDocs(docsInMongo, docIdsInFileTree)
  213. if (args.fix && missingDocs.length > 0) {
  214. console.log('Restoring missing docs to filetree...')
  215. const folder = await createRecoveryFolder(projectId)
  216. await restoreMissingDocs(projectId, folder, missingDocs)
  217. }
  218. if (errors > 0) {
  219. console.log(`Errors found in project: ${projectId}`)
  220. } else {
  221. console.log(`No errors found in project: ${projectId}`)
  222. }
  223. }
  224. async function main() {
  225. await waitForDb()
  226. for (const projectId of args._) {
  227. await checkProject(projectId)
  228. }
  229. }
  230. main()
  231. .then(() => {
  232. console.log('DONE')
  233. process.exit(0)
  234. })
  235. .catch(err => {
  236. console.error(err)
  237. process.exit(1)
  238. })