ProjectRootDocManager.js 9.6 KB

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