overleaf/services/web/app/src/Features/Project/ProjectRootDocManager.js

311 lines
8.7 KiB
JavaScript
Raw Normal View History

/* eslint-disable
camelcase,
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
*/
let ProjectRootDocManager
const ProjectEntityHandler = require('./ProjectEntityHandler')
const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler')
const ProjectGetter = require('./ProjectGetter')
const DocumentHelper = require('../Documents/DocumentHelper')
const Path = require('path')
const fs = require('fs')
const { promisify } = require('util')
const async = require('async')
const globby = require('globby')
const _ = require('underscore')
const { promisifyAll } = require('../../util/promises')
module.exports = ProjectRootDocManager = {
setRootDocAutomatically(project_id, callback) {
if (callback == null) {
callback = function () {}
}
return ProjectEntityHandler.getAllDocs(project_id, 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 (root_doc_id) {
if (root_doc_id != null) {
return ProjectEntityUpdateHandler.setRootDoc(
project_id,
root_doc_id,
callback
)
} else {
return callback()
}
})
})
},
findRootDocFileFromDirectory(directoryPath, callback) {
if (callback == null) {
callback = function () {}
}
const filePathsPromise = globby(['**/*.{tex,Rtex}'], {
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 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 }
}
return cb(null)
}
)
},
err =>
callback(
err,
doc != null ? doc.path : undefined,
doc != null ? doc.content : undefined
)
)
}
),
err => callback(err)
)
// coffeescript's implicit-return mechanism returns filePathsPromise from this method, which confuses mocha
return null
},
setRootDocFromName(project_id, rootDocName, callback) {
if (callback == null) {
callback = function () {}
}
return ProjectEntityHandler.getAllDocPathsFromProjectById(
project_id,
function (error, docPaths) {
let doc_id, 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 root_doc_id = null
for (doc_id in docPaths) {
// docpaths have a leading / so allow matching "folder/filename" and "/folder/filename"
path = docPaths[doc_id]
if (path === rootDocName) {
root_doc_id = doc_id
}
}
// try a basename match if there was no match
if (!root_doc_id) {
for (doc_id in docPaths) {
path = docPaths[doc_id]
if (Path.basename(path) === Path.basename(rootDocName)) {
root_doc_id = doc_id
}
}
}
// set the root doc id if we found a match
if (root_doc_id != null) {
return ProjectEntityUpdateHandler.setRootDoc(
project_id,
root_doc_id,
callback
)
} else {
return callback()
}
}
)
},
ensureRootDocumentIsSet(project_id, callback) {
if (callback == null) {
callback = function () {}
}
return ProjectGetter.getProject(
project_id,
{ 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(
project_id,
callback
)
}
}
)
},
/**
* @param {ObjectId | string} project_id
* @param {Function} callback
*/
ensureRootDocumentIsValid(project_id, callback) {
ProjectGetter.getProjectWithoutDocLines(
project_id,
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(project_id, () =>
ProjectRootDocManager.setRootDocAutomatically(
project_id,
callback
)
)
}
)
} else {
return ProjectRootDocManager.setRootDocAutomatically(
project_id,
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)
},
}
module.exports = ProjectRootDocManager
module.exports.promises = promisifyAll(module.exports, {
without: ['_rootDocSort'],
multiResult: {
findRootDocFileFromDirectory: ['path', 'content'],
},
})