ProjectRootDocManager.mjs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. /* eslint-disable
  2. n/handle-callback-err,
  3. max-len,
  4. no-unused-vars,
  5. no-useless-escape,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS207: Consider shorter variations of null checks
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. import ProjectEntityHandler from './ProjectEntityHandler.mjs'
  16. import ProjectEntityUpdateHandler from './ProjectEntityUpdateHandler.mjs'
  17. import ProjectGetter from './ProjectGetter.mjs'
  18. import DocumentHelper from '../Documents/DocumentHelper.js'
  19. import Path from 'node:path'
  20. import fs from 'node:fs'
  21. import async from 'async'
  22. import globby from 'globby'
  23. import _ from 'lodash'
  24. import { promisifyAll } from '@overleaf/promise-utils'
  25. import logger from '@overleaf/logger'
  26. import { BackgroundTaskTracker } from '../../infrastructure/GracefulShutdown.js'
  27. const rootDocResets = new BackgroundTaskTracker('root doc resets')
  28. const ProjectRootDocManager = {
  29. setRootDocAutomaticallyInBackground(projectId) {
  30. rootDocResets.add()
  31. setTimeout(async () => {
  32. try {
  33. await ProjectRootDocManager.promises.setRootDocAutomatically(projectId)
  34. } catch (err) {
  35. logger.warn(
  36. { err },
  37. 'failed to set root doc automatically in background'
  38. )
  39. } finally {
  40. rootDocResets.done()
  41. }
  42. }, 30 * 1000)
  43. },
  44. setRootDocAutomatically(projectId, callback) {
  45. if (callback == null) {
  46. callback = function () {}
  47. }
  48. return ProjectEntityHandler.getAllDocs(projectId, function (error, docs) {
  49. if (error != null) {
  50. return callback(error)
  51. }
  52. const jobs = _.map(
  53. docs,
  54. (doc, path) =>
  55. function (cb) {
  56. if (
  57. ProjectEntityUpdateHandler.isPathValidForRootDoc(path) &&
  58. DocumentHelper.contentHasDocumentclass(doc.lines)
  59. ) {
  60. async.setImmediate(function () {
  61. cb(doc._id)
  62. })
  63. } else {
  64. async.setImmediate(function () {
  65. cb(null)
  66. })
  67. }
  68. }
  69. )
  70. return async.series(jobs, function (rootDocId) {
  71. if (rootDocId != null) {
  72. return ProjectEntityUpdateHandler.setRootDoc(
  73. projectId,
  74. rootDocId,
  75. callback
  76. )
  77. } else {
  78. return callback()
  79. }
  80. })
  81. })
  82. },
  83. findRootDocFileFromDirectory(directoryPath, callback) {
  84. if (callback == null) {
  85. callback = function () {}
  86. }
  87. const filePathsPromise = globby(['**/*.{tex,Rtex,Rnw}'], {
  88. cwd: directoryPath,
  89. followSymlinkedDirectories: false,
  90. onlyFiles: true,
  91. case: false,
  92. })
  93. // the search order is such that we prefer files closer to the project root, then
  94. // we go by file size in ascending order, because people often have a main
  95. // file that just includes a bunch of other files; then we go by name, in
  96. // order to be deterministic
  97. filePathsPromise.then(
  98. unsortedFiles =>
  99. ProjectRootDocManager._sortFileList(
  100. unsortedFiles,
  101. directoryPath,
  102. function (err, files) {
  103. if (err != null) {
  104. return callback(err)
  105. }
  106. let firstFileInRootFolder
  107. let doc = null
  108. return async.until(
  109. cb => cb(null, doc != null || files.length === 0),
  110. function (cb) {
  111. const file = files.shift()
  112. return fs.readFile(
  113. Path.join(directoryPath, file),
  114. 'utf8',
  115. function (error, content) {
  116. if (error != null) {
  117. return cb(error)
  118. }
  119. content = (content || '').replace(/\r/g, '')
  120. if (DocumentHelper.contentHasDocumentclass(content)) {
  121. doc = { path: file, content }
  122. }
  123. if (!firstFileInRootFolder && !file.includes('/')) {
  124. firstFileInRootFolder = { path: file, content }
  125. }
  126. cb(null)
  127. }
  128. )
  129. },
  130. err => {
  131. if (err) {
  132. return callback(err)
  133. }
  134. // if no doc was found, use the first file in the root folder as the main doc
  135. if (!doc && firstFileInRootFolder) {
  136. doc = firstFileInRootFolder
  137. }
  138. callback(null, doc?.path, doc?.content)
  139. }
  140. )
  141. }
  142. ),
  143. err => callback(err)
  144. )
  145. // coffeescript's implicit-return mechanism returns filePathsPromise from this method, which confuses mocha
  146. return null
  147. },
  148. setRootDocFromName(projectId, rootDocName, callback) {
  149. if (callback == null) {
  150. callback = function () {}
  151. }
  152. return ProjectEntityHandler.getAllDocPathsFromProjectById(
  153. projectId,
  154. function (error, docPaths) {
  155. let docId, path
  156. if (error != null) {
  157. return callback(error)
  158. }
  159. // strip off leading and trailing quotes from rootDocName
  160. rootDocName = rootDocName.replace(/^\'|\'$/g, '')
  161. // prepend a slash for the root folder if not present
  162. if (rootDocName[0] !== '/') {
  163. rootDocName = `/${rootDocName}`
  164. }
  165. // find the root doc from the filename
  166. let rootDocId = null
  167. for (docId in docPaths) {
  168. // docpaths have a leading / so allow matching "folder/filename" and "/folder/filename"
  169. path = docPaths[docId]
  170. if (path === rootDocName) {
  171. rootDocId = docId
  172. }
  173. }
  174. // try a basename match if there was no match
  175. if (!rootDocId) {
  176. for (docId in docPaths) {
  177. path = docPaths[docId]
  178. if (Path.basename(path) === Path.basename(rootDocName)) {
  179. rootDocId = docId
  180. }
  181. }
  182. }
  183. // set the root doc id if we found a match
  184. if (rootDocId != null) {
  185. return ProjectEntityUpdateHandler.setRootDoc(
  186. projectId,
  187. rootDocId,
  188. callback
  189. )
  190. } else {
  191. return callback()
  192. }
  193. }
  194. )
  195. },
  196. ensureRootDocumentIsSet(projectId, callback) {
  197. if (callback == null) {
  198. callback = function () {}
  199. }
  200. return ProjectGetter.getProject(
  201. projectId,
  202. { rootDoc_id: 1 },
  203. function (error, project) {
  204. if (error != null) {
  205. return callback(error)
  206. }
  207. if (project == null) {
  208. return callback(new Error('project not found'))
  209. }
  210. if (project.rootDoc_id != null) {
  211. return callback()
  212. } else {
  213. return ProjectRootDocManager.setRootDocAutomatically(
  214. projectId,
  215. callback
  216. )
  217. }
  218. }
  219. )
  220. },
  221. /**
  222. * @param {ObjectId | string} project_id
  223. * @param {Function} callback
  224. */
  225. ensureRootDocumentIsValid(projectId, callback) {
  226. ProjectGetter.getProjectWithoutDocLines(
  227. projectId,
  228. function (error, project) {
  229. if (error != null) {
  230. return callback(error)
  231. }
  232. if (project == null) {
  233. return callback(new Error('project not found'))
  234. }
  235. if (project.rootDoc_id != null) {
  236. ProjectEntityHandler.getDocPathFromProjectByDocId(
  237. project,
  238. project.rootDoc_id,
  239. (err, docPath) => {
  240. if (docPath) return callback()
  241. ProjectEntityUpdateHandler.unsetRootDoc(projectId, () =>
  242. ProjectRootDocManager.setRootDocAutomatically(
  243. projectId,
  244. callback
  245. )
  246. )
  247. }
  248. )
  249. } else {
  250. return ProjectRootDocManager.setRootDocAutomatically(
  251. projectId,
  252. callback
  253. )
  254. }
  255. }
  256. )
  257. },
  258. _sortFileList(listToSort, rootDirectory, callback) {
  259. if (callback == null) {
  260. callback = function () {}
  261. }
  262. return async.mapLimit(
  263. listToSort,
  264. 5,
  265. (filePath, cb) =>
  266. fs.stat(Path.join(rootDirectory, filePath), function (err, stat) {
  267. if (err != null) {
  268. return cb(err)
  269. }
  270. return cb(null, {
  271. size: stat.size,
  272. path: filePath,
  273. elements: filePath.split(Path.sep).length,
  274. name: Path.basename(filePath),
  275. })
  276. }),
  277. function (err, files) {
  278. if (err != null) {
  279. return callback(err)
  280. }
  281. return callback(
  282. null,
  283. _.map(
  284. files.sort(ProjectRootDocManager._rootDocSort),
  285. file => file.path
  286. )
  287. )
  288. }
  289. )
  290. },
  291. _rootDocSort(a, b) {
  292. // sort first by folder depth
  293. if (a.elements !== b.elements) {
  294. return a.elements - b.elements
  295. }
  296. // ensure main.tex is at the start of each folder
  297. if (a.name === 'main.tex' && b.name !== 'main.tex') {
  298. return -1
  299. }
  300. if (a.name !== 'main.tex' && b.name === 'main.tex') {
  301. return 1
  302. }
  303. // prefer smaller files
  304. if (a.size !== b.size) {
  305. return a.size - b.size
  306. }
  307. // otherwise, use the full path name
  308. return a.path.localeCompare(b.path)
  309. },
  310. }
  311. ProjectRootDocManager.promises = promisifyAll(ProjectRootDocManager, {
  312. without: ['_rootDocSort', 'setRootDocAutomaticallyInBackground'],
  313. multiResult: {
  314. findRootDocFileFromDirectory: ['path', 'content'],
  315. },
  316. })
  317. export default ProjectRootDocManager