ProjectRootDocManager.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  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 doc = null
  110. return async.until(
  111. cb => cb(null, doc != null || files.length === 0),
  112. function (cb) {
  113. const file = files.shift()
  114. return fs.readFile(
  115. Path.join(directoryPath, file),
  116. 'utf8',
  117. function (error, content) {
  118. if (error != null) {
  119. return cb(error)
  120. }
  121. content = (content || '').replace(/\r/g, '')
  122. if (DocumentHelper.contentHasDocumentclass(content)) {
  123. doc = { path: file, content }
  124. }
  125. return cb(null)
  126. }
  127. )
  128. },
  129. err =>
  130. callback(
  131. err,
  132. doc != null ? doc.path : undefined,
  133. doc != null ? doc.content : undefined
  134. )
  135. )
  136. }
  137. ),
  138. err => callback(err)
  139. )
  140. // coffeescript's implicit-return mechanism returns filePathsPromise from this method, which confuses mocha
  141. return null
  142. },
  143. setRootDocFromName(projectId, rootDocName, callback) {
  144. if (callback == null) {
  145. callback = function () {}
  146. }
  147. return ProjectEntityHandler.getAllDocPathsFromProjectById(
  148. projectId,
  149. function (error, docPaths) {
  150. let docId, path
  151. if (error != null) {
  152. return callback(error)
  153. }
  154. // strip off leading and trailing quotes from rootDocName
  155. rootDocName = rootDocName.replace(/^\'|\'$/g, '')
  156. // prepend a slash for the root folder if not present
  157. if (rootDocName[0] !== '/') {
  158. rootDocName = `/${rootDocName}`
  159. }
  160. // find the root doc from the filename
  161. let rootDocId = null
  162. for (docId in docPaths) {
  163. // docpaths have a leading / so allow matching "folder/filename" and "/folder/filename"
  164. path = docPaths[docId]
  165. if (path === rootDocName) {
  166. rootDocId = docId
  167. }
  168. }
  169. // try a basename match if there was no match
  170. if (!rootDocId) {
  171. for (docId in docPaths) {
  172. path = docPaths[docId]
  173. if (Path.basename(path) === Path.basename(rootDocName)) {
  174. rootDocId = docId
  175. }
  176. }
  177. }
  178. // set the root doc id if we found a match
  179. if (rootDocId != null) {
  180. return ProjectEntityUpdateHandler.setRootDoc(
  181. projectId,
  182. rootDocId,
  183. callback
  184. )
  185. } else {
  186. return callback()
  187. }
  188. }
  189. )
  190. },
  191. ensureRootDocumentIsSet(projectId, callback) {
  192. if (callback == null) {
  193. callback = function () {}
  194. }
  195. return ProjectGetter.getProject(
  196. projectId,
  197. { rootDoc_id: 1 },
  198. function (error, project) {
  199. if (error != null) {
  200. return callback(error)
  201. }
  202. if (project == null) {
  203. return callback(new Error('project not found'))
  204. }
  205. if (project.rootDoc_id != null) {
  206. return callback()
  207. } else {
  208. return ProjectRootDocManager.setRootDocAutomatically(
  209. projectId,
  210. callback
  211. )
  212. }
  213. }
  214. )
  215. },
  216. /**
  217. * @param {ObjectId | string} project_id
  218. * @param {Function} callback
  219. */
  220. ensureRootDocumentIsValid(projectId, callback) {
  221. ProjectGetter.getProjectWithoutDocLines(
  222. projectId,
  223. function (error, project) {
  224. if (error != null) {
  225. return callback(error)
  226. }
  227. if (project == null) {
  228. return callback(new Error('project not found'))
  229. }
  230. if (project.rootDoc_id != null) {
  231. ProjectEntityHandler.getDocPathFromProjectByDocId(
  232. project,
  233. project.rootDoc_id,
  234. (err, docPath) => {
  235. if (docPath) return callback()
  236. ProjectEntityUpdateHandler.unsetRootDoc(projectId, () =>
  237. ProjectRootDocManager.setRootDocAutomatically(
  238. projectId,
  239. callback
  240. )
  241. )
  242. }
  243. )
  244. } else {
  245. return ProjectRootDocManager.setRootDocAutomatically(
  246. projectId,
  247. callback
  248. )
  249. }
  250. }
  251. )
  252. },
  253. _sortFileList(listToSort, rootDirectory, callback) {
  254. if (callback == null) {
  255. callback = function () {}
  256. }
  257. return async.mapLimit(
  258. listToSort,
  259. 5,
  260. (filePath, cb) =>
  261. fs.stat(Path.join(rootDirectory, filePath), function (err, stat) {
  262. if (err != null) {
  263. return cb(err)
  264. }
  265. return cb(null, {
  266. size: stat.size,
  267. path: filePath,
  268. elements: filePath.split(Path.sep).length,
  269. name: Path.basename(filePath),
  270. })
  271. }),
  272. function (err, files) {
  273. if (err != null) {
  274. return callback(err)
  275. }
  276. return callback(
  277. null,
  278. _.map(
  279. files.sort(ProjectRootDocManager._rootDocSort),
  280. file => file.path
  281. )
  282. )
  283. }
  284. )
  285. },
  286. _rootDocSort(a, b) {
  287. // sort first by folder depth
  288. if (a.elements !== b.elements) {
  289. return a.elements - b.elements
  290. }
  291. // ensure main.tex is at the start of each folder
  292. if (a.name === 'main.tex' && b.name !== 'main.tex') {
  293. return -1
  294. }
  295. if (a.name !== 'main.tex' && b.name === 'main.tex') {
  296. return 1
  297. }
  298. // prefer smaller files
  299. if (a.size !== b.size) {
  300. return a.size - b.size
  301. }
  302. // otherwise, use the full path name
  303. return a.path.localeCompare(b.path)
  304. },
  305. }
  306. module.exports = ProjectRootDocManager
  307. module.exports.promises = promisifyAll(module.exports, {
  308. without: ['_rootDocSort', 'setRootDocAutomaticallyInBackground'],
  309. multiResult: {
  310. findRootDocFileFromDirectory: ['path', 'content'],
  311. },
  312. })