check_project_files.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  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 ProjectGetter = require('../app/src/Features/Project/ProjectGetter')
  5. const ProjectEntityMongoUpdateHandler = require('../app/src/Features/Project/ProjectEntityMongoUpdateHandler')
  6. const { waitForDb, db, ObjectId } = require('../app/src/infrastructure/mongodb')
  7. const HistoryManager = require('../app/src/Features/History/HistoryManager')
  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. null // unset lastUpdatedBy
  127. )
  128. console.log('Created recovery folder:', folder._id.toString())
  129. return folder
  130. }
  131. async function restoreMissingDocs(projectId, folder, missingDocs) {
  132. for (const doc of missingDocs) {
  133. doc.name = doc.name || `unknown-file-${doc._id.toString()}`
  134. try {
  135. await ProjectEntityMongoUpdateHandler.promises.addDoc(
  136. new ObjectId(projectId),
  137. folder._id,
  138. doc,
  139. null // unset lastUpdatedBy
  140. )
  141. console.log('Restored doc to filetree:', doc._id.toString())
  142. } catch (err) {
  143. console.log(`Error adding doc to filetree:`, err)
  144. }
  145. }
  146. }
  147. async function checkProject(projectId) {
  148. try {
  149. await DocumentUpdaterHandler.promises.flushProjectToMongo(projectId)
  150. } catch (err) {
  151. console.log(`Error flushing project ${projectId} to mongo: ${err}`)
  152. }
  153. const project = await ProjectGetter.promises.getProject(projectId, {
  154. rootFolder: true,
  155. rootDoc_id: true,
  156. })
  157. if (verbose) {
  158. console.log(`project: ${JSON.stringify(project)}`)
  159. }
  160. const { docEntries, fileEntries } = _getFolderEntries(project.rootFolder[0])
  161. console.log(
  162. `Found ${docEntries.length} docEntries and ${fileEntries.length} fileEntries`
  163. )
  164. const pathCounts = findPathCounts(projectId, docEntries, fileEntries)
  165. for (const [path, count] of pathCounts) {
  166. if (count > 1) {
  167. console.log(`Found duplicate path: ${path}`)
  168. }
  169. }
  170. let errors = 0
  171. for (const { doc, path } of docEntries) {
  172. try {
  173. const { lines, rev, version, ranges } =
  174. await DocstoreManager.promises.getDoc(projectId, doc._id)
  175. if (!lines) {
  176. throw new Error('no doclines')
  177. }
  178. if (pathCounts.get(path) > 1) {
  179. logDoc(
  180. projectId,
  181. path,
  182. { ...doc, lines, rev, version, ranges },
  183. 'duplicate path'
  184. )
  185. errors++
  186. } else if (verbose) {
  187. logDoc(projectId, path, { ...doc, lines, rev, version, ranges })
  188. }
  189. } catch (err) {
  190. logDoc(projectId, path, doc, err)
  191. errors++
  192. }
  193. }
  194. for (const { file, path } of fileEntries) {
  195. try {
  196. const { contentLength: fileSize } =
  197. await HistoryManager.promises.requestBlobWithProjectId(
  198. projectId,
  199. file.hash,
  200. 'HEAD'
  201. )
  202. if (pathCounts.get(path) > 1) {
  203. logFile(projectId, path, { ...file, fileSize }, 'duplicate path')
  204. errors++
  205. } else if (verbose) {
  206. logFile(projectId, path, { ...file, fileSize })
  207. }
  208. } catch (err) {
  209. logFile(projectId, path, file, err)
  210. errors++
  211. }
  212. }
  213. // now look for docs in the docstore that are not in the project filetree
  214. const docsInMongo = await getDocsInMongo(projectId)
  215. const docIdsInFileTree = getDocIdsInFileTree(docEntries)
  216. const missingDocs = findMissingDocs(docsInMongo, docIdsInFileTree)
  217. if (args.fix && missingDocs.length > 0) {
  218. console.log('Restoring missing docs to filetree...')
  219. const folder = await createRecoveryFolder(projectId)
  220. await restoreMissingDocs(projectId, folder, missingDocs)
  221. }
  222. if (errors > 0) {
  223. console.log(`Errors found in project: ${projectId}`)
  224. } else {
  225. console.log(`No errors found in project: ${projectId}`)
  226. }
  227. }
  228. async function main() {
  229. await waitForDb()
  230. for (const projectId of args._) {
  231. await checkProject(projectId)
  232. }
  233. }
  234. main()
  235. .then(() => {
  236. console.log('DONE')
  237. process.exit(0)
  238. })
  239. .catch(err => {
  240. console.error(err)
  241. process.exit(1)
  242. })