mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-04 22:59:01 +02:00
cb0266035d
11 years ago, the db.projects collection was storing doc lines in the
file-tree/rootFolder. Any operations on the project that did not need
those lines were benefitting from excluding all those entries from the
file-tree. These days, the verbose exclusions are not useful anymore and
merely add load on mongo.
REF: 9805c6a9ff
GitOrigin-RevId: 89f544688934c1ed1ca98877ffbe8baefe66c126
229 lines
6.8 KiB
JavaScript
229 lines
6.8 KiB
JavaScript
// 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.mjs'
|
|
import Path from 'node:path'
|
|
import fs from 'node:fs'
|
|
import pLimit from 'p-limit'
|
|
import globby from 'globby'
|
|
import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils'
|
|
import logger from '@overleaf/logger'
|
|
import { BackgroundTaskTracker } from '../../infrastructure/GracefulShutdown.mjs'
|
|
|
|
const rootDocResets = new BackgroundTaskTracker('root doc resets')
|
|
|
|
function 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)
|
|
}
|
|
|
|
async function setRootDocAutomatically(projectId) {
|
|
const docs = await ProjectEntityHandler.promises.getAllDocs(projectId)
|
|
|
|
for (const [path, doc] of Object.entries(docs)) {
|
|
if (
|
|
ProjectEntityUpdateHandler.isPathValidForRootDoc(path) &&
|
|
DocumentHelper.contentHasDocumentclass(doc.lines) &&
|
|
doc._id
|
|
) {
|
|
return await ProjectEntityUpdateHandler.promises.setRootDoc(
|
|
projectId,
|
|
doc._id
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
async function findRootDocFileFromDirectory(directoryPath) {
|
|
const unsortedFiles = await 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
|
|
|
|
const files = await _sortFileList(unsortedFiles, directoryPath)
|
|
let firstFileInRootFolder
|
|
let doc = null
|
|
|
|
while (files.length > 0 && doc == null) {
|
|
const file = files.shift()
|
|
const content = await fs.promises.readFile(
|
|
Path.join(directoryPath, file),
|
|
'utf8'
|
|
)
|
|
const normalizedContent = (content || '').replace(/\r/g, '')
|
|
if (DocumentHelper.contentHasDocumentclass(normalizedContent)) {
|
|
doc = { path: file, content: normalizedContent }
|
|
}
|
|
|
|
if (!firstFileInRootFolder && !file.includes('/')) {
|
|
firstFileInRootFolder = { path: file, content: normalizedContent }
|
|
}
|
|
}
|
|
|
|
// if no doc was found, use the first file in the root folder as the main doc
|
|
if (!doc && firstFileInRootFolder) {
|
|
doc = firstFileInRootFolder
|
|
}
|
|
|
|
return { path: doc?.path, content: doc?.content }
|
|
}
|
|
|
|
async function setRootDocFromName(projectId, rootDocName) {
|
|
const docPaths =
|
|
await ProjectEntityHandler.promises.getAllDocPathsFromProjectById(projectId)
|
|
|
|
let docId, path
|
|
// 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 await ProjectEntityUpdateHandler.promises.setRootDoc(
|
|
projectId,
|
|
rootDocId
|
|
)
|
|
}
|
|
}
|
|
|
|
async function ensureRootDocumentIsSet(projectId) {
|
|
const project = await ProjectGetter.promises.getProject(projectId, {
|
|
rootDoc_id: 1,
|
|
})
|
|
if (!project) {
|
|
throw new Error('project not found')
|
|
}
|
|
if (project.rootDoc_id != null) {
|
|
return
|
|
}
|
|
return await ProjectRootDocManager.promises.setRootDocAutomatically(projectId)
|
|
}
|
|
|
|
/**
|
|
* @param {ObjectId | string} projectId
|
|
*/
|
|
async function ensureRootDocumentIsValid(projectId) {
|
|
const project = await ProjectGetter.promises.getProject(projectId, {
|
|
rootFolder: 1,
|
|
rootDoc_id: 1,
|
|
})
|
|
if (!project) {
|
|
throw new Error('project not found')
|
|
}
|
|
if (project.rootDoc_id != null) {
|
|
const docPath =
|
|
await ProjectEntityHandler.promises.getDocPathFromProjectByDocId(
|
|
project,
|
|
project.rootDoc_id
|
|
)
|
|
if (docPath) {
|
|
return
|
|
}
|
|
await ProjectEntityUpdateHandler.promises.unsetRootDoc(projectId)
|
|
}
|
|
return await ProjectRootDocManager.promises.setRootDocAutomatically(projectId)
|
|
}
|
|
|
|
async function _sortFileList(listToSort, rootDirectory) {
|
|
const limit = pLimit(5)
|
|
const files = await Promise.all(
|
|
listToSort.map(filePath =>
|
|
limit(async () => {
|
|
const fullPath = Path.join(rootDirectory, filePath)
|
|
const stat = await fs.promises.stat(fullPath)
|
|
return {
|
|
size: stat.size,
|
|
path: filePath,
|
|
elements: filePath.split(Path.sep).length,
|
|
name: Path.basename(filePath),
|
|
}
|
|
})
|
|
)
|
|
)
|
|
return files.sort(_rootDocSort).map(file => file.path)
|
|
}
|
|
|
|
function _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)
|
|
}
|
|
|
|
const ProjectRootDocManager = {
|
|
setRootDocAutomaticallyInBackground,
|
|
setRootDocAutomatically: callbackify(setRootDocAutomatically),
|
|
findRootDocFileFromDirectory: callbackifyMultiResult(
|
|
findRootDocFileFromDirectory,
|
|
['path', 'content']
|
|
),
|
|
setRootDocFromName: callbackify(setRootDocFromName),
|
|
ensureRootDocumentIsSet: callbackify(ensureRootDocumentIsSet),
|
|
ensureRootDocumentIsValid: callbackify(ensureRootDocumentIsValid),
|
|
promises: {
|
|
setRootDocAutomatically,
|
|
findRootDocFileFromDirectory,
|
|
setRootDocFromName,
|
|
ensureRootDocumentIsSet,
|
|
ensureRootDocumentIsValid,
|
|
},
|
|
}
|
|
|
|
export default ProjectRootDocManager
|