| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- /* eslint-disable
- n/handle-callback-err,
- max-len,
- no-unused-vars,
- no-useless-escape,
- */
- // TODO: This file was created by bulk-decaffeinate.
- // Fix any style issues and re-enable lint.
- /*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
- import ProjectEntityHandler from './ProjectEntityHandler.mjs'
- import ProjectEntityUpdateHandler from './ProjectEntityUpdateHandler.mjs'
- import ProjectGetter from './ProjectGetter.mjs'
- import DocumentHelper from '../Documents/DocumentHelper.js'
- import Path from 'node:path'
- import fs from 'node:fs'
- import async from 'async'
- import globby from 'globby'
- import _ from 'lodash'
- import { promisifyAll } from '@overleaf/promise-utils'
- import logger from '@overleaf/logger'
- import { BackgroundTaskTracker } from '../../infrastructure/GracefulShutdown.js'
- const rootDocResets = new BackgroundTaskTracker('root doc resets')
- const ProjectRootDocManager = {
- setRootDocAutomaticallyInBackground(projectId) {
- rootDocResets.add()
- setTimeout(async () => {
- try {
- await ProjectRootDocManager.promises.setRootDocAutomatically(projectId)
- } catch (err) {
- logger.warn(
- { err },
- 'failed to set root doc automatically in background'
- )
- } finally {
- rootDocResets.done()
- }
- }, 30 * 1000)
- },
- setRootDocAutomatically(projectId, callback) {
- if (callback == null) {
- callback = function () {}
- }
- return ProjectEntityHandler.getAllDocs(projectId, function (error, docs) {
- if (error != null) {
- return callback(error)
- }
- const jobs = _.map(
- docs,
- (doc, path) =>
- function (cb) {
- if (
- ProjectEntityUpdateHandler.isPathValidForRootDoc(path) &&
- DocumentHelper.contentHasDocumentclass(doc.lines)
- ) {
- async.setImmediate(function () {
- cb(doc._id)
- })
- } else {
- async.setImmediate(function () {
- cb(null)
- })
- }
- }
- )
- return async.series(jobs, function (rootDocId) {
- if (rootDocId != null) {
- return ProjectEntityUpdateHandler.setRootDoc(
- projectId,
- rootDocId,
- callback
- )
- } else {
- return callback()
- }
- })
- })
- },
- findRootDocFileFromDirectory(directoryPath, callback) {
- if (callback == null) {
- callback = function () {}
- }
- const filePathsPromise = globby(['**/*.{tex,Rtex,Rnw}'], {
- cwd: directoryPath,
- followSymlinkedDirectories: false,
- onlyFiles: true,
- case: false,
- })
- // the search order is such that we prefer files closer to the project root, then
- // we go by file size in ascending order, because people often have a main
- // file that just includes a bunch of other files; then we go by name, in
- // order to be deterministic
- filePathsPromise.then(
- unsortedFiles =>
- ProjectRootDocManager._sortFileList(
- unsortedFiles,
- directoryPath,
- function (err, files) {
- if (err != null) {
- return callback(err)
- }
- let firstFileInRootFolder
- let doc = null
- return async.until(
- cb => cb(null, doc != null || files.length === 0),
- function (cb) {
- const file = files.shift()
- return fs.readFile(
- Path.join(directoryPath, file),
- 'utf8',
- function (error, content) {
- if (error != null) {
- return cb(error)
- }
- content = (content || '').replace(/\r/g, '')
- if (DocumentHelper.contentHasDocumentclass(content)) {
- doc = { path: file, content }
- }
- if (!firstFileInRootFolder && !file.includes('/')) {
- firstFileInRootFolder = { path: file, content }
- }
- cb(null)
- }
- )
- },
- err => {
- if (err) {
- return callback(err)
- }
- // if no doc was found, use the first file in the root folder as the main doc
- if (!doc && firstFileInRootFolder) {
- doc = firstFileInRootFolder
- }
- callback(null, doc?.path, doc?.content)
- }
- )
- }
- ),
- err => callback(err)
- )
- // coffeescript's implicit-return mechanism returns filePathsPromise from this method, which confuses mocha
- return null
- },
- setRootDocFromName(projectId, rootDocName, callback) {
- if (callback == null) {
- callback = function () {}
- }
- return ProjectEntityHandler.getAllDocPathsFromProjectById(
- projectId,
- function (error, docPaths) {
- let docId, path
- if (error != null) {
- return callback(error)
- }
- // strip off leading and trailing quotes from rootDocName
- rootDocName = rootDocName.replace(/^\'|\'$/g, '')
- // prepend a slash for the root folder if not present
- if (rootDocName[0] !== '/') {
- rootDocName = `/${rootDocName}`
- }
- // find the root doc from the filename
- let rootDocId = null
- for (docId in docPaths) {
- // docpaths have a leading / so allow matching "folder/filename" and "/folder/filename"
- path = docPaths[docId]
- if (path === rootDocName) {
- rootDocId = docId
- }
- }
- // try a basename match if there was no match
- if (!rootDocId) {
- for (docId in docPaths) {
- path = docPaths[docId]
- if (Path.basename(path) === Path.basename(rootDocName)) {
- rootDocId = docId
- }
- }
- }
- // set the root doc id if we found a match
- if (rootDocId != null) {
- return ProjectEntityUpdateHandler.setRootDoc(
- projectId,
- rootDocId,
- callback
- )
- } else {
- return callback()
- }
- }
- )
- },
- ensureRootDocumentIsSet(projectId, callback) {
- if (callback == null) {
- callback = function () {}
- }
- return ProjectGetter.getProject(
- projectId,
- { rootDoc_id: 1 },
- function (error, project) {
- if (error != null) {
- return callback(error)
- }
- if (project == null) {
- return callback(new Error('project not found'))
- }
- if (project.rootDoc_id != null) {
- return callback()
- } else {
- return ProjectRootDocManager.setRootDocAutomatically(
- projectId,
- callback
- )
- }
- }
- )
- },
- /**
- * @param {ObjectId | string} project_id
- * @param {Function} callback
- */
- ensureRootDocumentIsValid(projectId, callback) {
- ProjectGetter.getProjectWithoutDocLines(
- projectId,
- function (error, project) {
- if (error != null) {
- return callback(error)
- }
- if (project == null) {
- return callback(new Error('project not found'))
- }
- if (project.rootDoc_id != null) {
- ProjectEntityHandler.getDocPathFromProjectByDocId(
- project,
- project.rootDoc_id,
- (err, docPath) => {
- if (docPath) return callback()
- ProjectEntityUpdateHandler.unsetRootDoc(projectId, () =>
- ProjectRootDocManager.setRootDocAutomatically(
- projectId,
- callback
- )
- )
- }
- )
- } else {
- return ProjectRootDocManager.setRootDocAutomatically(
- projectId,
- callback
- )
- }
- }
- )
- },
- _sortFileList(listToSort, rootDirectory, callback) {
- if (callback == null) {
- callback = function () {}
- }
- return async.mapLimit(
- listToSort,
- 5,
- (filePath, cb) =>
- fs.stat(Path.join(rootDirectory, filePath), function (err, stat) {
- if (err != null) {
- return cb(err)
- }
- return cb(null, {
- size: stat.size,
- path: filePath,
- elements: filePath.split(Path.sep).length,
- name: Path.basename(filePath),
- })
- }),
- function (err, files) {
- if (err != null) {
- return callback(err)
- }
- return callback(
- null,
- _.map(
- files.sort(ProjectRootDocManager._rootDocSort),
- file => file.path
- )
- )
- }
- )
- },
- _rootDocSort(a, b) {
- // sort first by folder depth
- if (a.elements !== b.elements) {
- return a.elements - b.elements
- }
- // ensure main.tex is at the start of each folder
- if (a.name === 'main.tex' && b.name !== 'main.tex') {
- return -1
- }
- if (a.name !== 'main.tex' && b.name === 'main.tex') {
- return 1
- }
- // prefer smaller files
- if (a.size !== b.size) {
- return a.size - b.size
- }
- // otherwise, use the full path name
- return a.path.localeCompare(b.path)
- },
- }
- ProjectRootDocManager.promises = promisifyAll(ProjectRootDocManager, {
- without: ['_rootDocSort', 'setRootDocAutomaticallyInBackground'],
- multiResult: {
- findRootDocFileFromDirectory: ['path', 'content'],
- },
- })
- export default ProjectRootDocManager
|