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

330 lines
8.8 KiB
JavaScript
Raw Normal View History

const ProjectGetter = require('./ProjectGetter')
const ProjectHelper = require('./ProjectHelper')
const Errors = require('../Errors/Errors')
const _ = require('underscore')
const logger = require('logger-sharelatex')
const async = require('async')
const { promisifyAll } = require('../../util/promises')
const ProjectLocator = {
findElement(options, _callback) {
// The search algorithm below potentially invokes the callback multiple
// times.
const callback = _.once(_callback)
const {
project,
project_id: projectId,
element_id: elementId,
type
} = options
const elementType = sanitizeTypeOfElement(type)
let count = 0
const endOfBranch = function() {
if (--count === 0) {
logger.warn(
`element ${elementId} could not be found for project ${projectId ||
project._id}`
)
callback(new Errors.NotFoundError('entity not found'))
}
}
function search(searchFolder, path) {
count++
const element = _.find(
searchFolder[elementType],
el => (el != null ? el._id : undefined) + '' === elementId + ''
) // need to ToString both id's for robustness
if (
element == null &&
searchFolder.folders != null &&
searchFolder.folders.length !== 0
) {
_.each(searchFolder.folders, (folder, index) => {
if (folder == null) {
return
}
const newPath = {}
for (let key of Object.keys(path)) {
const value = path[key]
newPath[key] = value
} // make a value copy of the string
newPath.fileSystem += `/${folder.name}`
newPath.mongo += `.folders.${index}`
search(folder, newPath)
})
endOfBranch()
} else if (element != null) {
const elementPlaceInArray = getIndexOf(
searchFolder[elementType],
elementId
)
path.fileSystem += `/${element.name}`
path.mongo += `.${elementType}.${elementPlaceInArray}`
callback(null, element, path, searchFolder)
} else if (element == null) {
endOfBranch()
}
}
const path = { fileSystem: '', mongo: 'rootFolder.0' }
const startSearch = project => {
if (elementId + '' === project.rootFolder[0]._id + '') {
callback(null, project.rootFolder[0], path, null)
} else {
search(project.rootFolder[0], path)
}
}
if (project != null) {
startSearch(project)
} else {
ProjectGetter.getProject(
projectId,
{ rootFolder: true, rootDoc_id: true },
(err, project) => {
if (err != null) {
return callback(err)
}
if (project == null) {
return callback(new Errors.NotFoundError('project not found'))
}
startSearch(project)
}
)
}
},
findRootDoc(opts, callback) {
const getRootDoc = project => {
if (project.rootDoc_id != null) {
ProjectLocator.findElement(
{ project, element_id: project.rootDoc_id, type: 'docs' },
(error, ...args) => {
if (error != null) {
if (error instanceof Errors.NotFoundError) {
return callback(null, null)
} else {
return callback(error)
}
}
callback(null, ...args)
}
)
} else {
callback(null, null)
}
}
const { project, project_id: projectId } = opts
if (project != null) {
getRootDoc(project)
} else {
ProjectGetter.getProject(
projectId,
{ rootFolder: true, rootDoc_id: true },
(err, project) => {
if (err != null) {
logger.warn({ err }, 'error getting project')
callback(err)
} else {
getRootDoc(project)
}
}
)
}
},
findElementByPath(options, callback) {
const { project, project_id: projectId, path, exactCaseMatch } = options
if (path == null) {
return new Error('no path provided for findElementByPath')
}
if (project != null) {
ProjectLocator._findElementByPathWithProject(
project,
path,
exactCaseMatch,
callback
)
} else {
ProjectGetter.getProject(
projectId,
{ rootFolder: true, rootDoc_id: true },
(err, project) => {
if (err != null) {
return callback(err)
}
ProjectLocator._findElementByPathWithProject(
project,
path,
exactCaseMatch,
callback
)
}
)
}
},
_findElementByPathWithProject(project, needlePath, exactCaseMatch, callback) {
let matchFn
if (exactCaseMatch) {
matchFn = (a, b) => a === b
} else {
matchFn = (a, b) =>
(a != null ? a.toLowerCase() : undefined) ===
(b != null ? b.toLowerCase() : undefined)
}
function getParentFolder(haystackFolder, foldersList, level, cb) {
if (foldersList.length === 0) {
return cb(null, haystackFolder)
}
const needleFolderName = foldersList[level]
let found = false
for (let folder of haystackFolder.folders) {
if (matchFn(folder.name, needleFolderName)) {
found = true
if (level === foldersList.length - 1) {
return cb(null, folder)
} else {
return getParentFolder(folder, foldersList, level + 1, cb)
}
}
}
if (!found) {
cb(
new Error(
`not found project: ${
project._id
} search path: ${needlePath}, folder ${
foldersList[level]
} could not be found`
)
)
}
}
function getEntity(folder, entityName, cb) {
let result, type
if (entityName == null) {
return cb(null, folder, 'folder')
}
for (let file of folder.fileRefs || []) {
if (matchFn(file != null ? file.name : undefined, entityName)) {
result = file
type = 'file'
}
}
for (let doc of folder.docs || []) {
if (matchFn(doc != null ? doc.name : undefined, entityName)) {
result = doc
type = 'doc'
}
}
for (let childFolder of folder.folders || []) {
if (
matchFn(
childFolder != null ? childFolder.name : undefined,
entityName
)
) {
result = childFolder
type = 'folder'
}
}
if (result != null) {
cb(null, result, type)
} else {
cb(
new Error(
`not found project: ${
project._id
} search path: ${needlePath}, entity ${entityName} could not be found`
)
)
}
}
if (project == null) {
return callback(new Error('Tried to find an element for a null project'))
}
if (needlePath === '' || needlePath === '/') {
return callback(null, project.rootFolder[0], 'folder')
}
if (needlePath.indexOf('/') === 0) {
needlePath = needlePath.substring(1)
}
const foldersList = needlePath.split('/')
const needleName = foldersList.pop()
const rootFolder = project.rootFolder[0]
const jobs = []
jobs.push(cb => getParentFolder(rootFolder, foldersList, 0, cb))
jobs.push((folder, cb) => getEntity(folder, needleName, cb))
async.waterfall(jobs, callback)
},
findUsersProjectByName(userId, projectName, callback) {
ProjectGetter.findAllUsersProjects(
userId,
'name archived trashed',
(err, allProjects) => {
if (err != null) {
return callback(err)
}
const { owned, readAndWrite } = allProjects
const projects = owned.concat(readAndWrite)
projectName = projectName.toLowerCase()
const project = _.find(
projects,
project =>
project.name.toLowerCase() === projectName &&
!ProjectHelper.isArchivedOrTrashed(project, userId)
)
callback(null, project)
}
)
}
}
function sanitizeTypeOfElement(elementType) {
const lastChar = elementType.slice(-1)
if (lastChar !== 's') {
elementType += 's'
}
if (elementType === 'files') {
elementType = 'fileRefs'
}
return elementType
}
function getIndexOf(searchEntity, id) {
const { length } = searchEntity
let count = 0
while (count < length) {
if (
(searchEntity[count] != null ? searchEntity[count]._id : undefined) +
'' ===
id + ''
) {
return count
}
count++
}
}
module.exports = ProjectLocator
module.exports.promises = promisifyAll(ProjectLocator, {
multiResult: {
findElement: ['element', 'path', 'folder'],
findElementByPath: ['element', 'type'],
findRootDoc: ['element', 'path', 'folder']
}
})