ProjectLocator.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. const _ = require('lodash')
  2. const logger = require('@overleaf/logger')
  3. const OError = require('@overleaf/o-error')
  4. const async = require('async')
  5. const ProjectGetter = require('./ProjectGetter')
  6. const Errors = require('../Errors/Errors')
  7. const { promisifyMultiResult } = require('@overleaf/promise-utils')
  8. const { iterablePaths } = require('./IterablePath')
  9. function findElement(options, _callback) {
  10. // The search algorithm below potentially invokes the callback multiple
  11. // times.
  12. const callback = _.once(_callback)
  13. const {
  14. project,
  15. project_id: projectId,
  16. element_id: elementId,
  17. type,
  18. } = options
  19. const elementType = sanitizeTypeOfElement(type)
  20. let count = 0
  21. const endOfBranch = function () {
  22. if (--count === 0) {
  23. logger.warn(
  24. `element ${elementId} could not be found for project ${
  25. projectId || project._id
  26. }`
  27. )
  28. callback(new Errors.NotFoundError('entity not found'))
  29. }
  30. }
  31. function search(searchFolder, path) {
  32. count++
  33. const element = _.find(
  34. searchFolder[elementType],
  35. el => (el != null ? el._id : undefined) + '' === elementId + ''
  36. ) // need to ToString both id's for robustness
  37. if (
  38. element == null &&
  39. searchFolder.folders != null &&
  40. searchFolder.folders.length !== 0
  41. ) {
  42. _.forEach(searchFolder.folders, (folder, index) => {
  43. if (folder == null) {
  44. return
  45. }
  46. const newPath = {}
  47. for (const key of Object.keys(path)) {
  48. const value = path[key]
  49. newPath[key] = value
  50. } // make a value copy of the string
  51. newPath.fileSystem += `/${folder.name}`
  52. newPath.mongo += `.folders.${index}`
  53. search(folder, newPath)
  54. })
  55. endOfBranch()
  56. } else if (element != null) {
  57. const elementPlaceInArray = getIndexOf(
  58. searchFolder[elementType],
  59. elementId
  60. )
  61. path.fileSystem += `/${element.name}`
  62. path.mongo += `.${elementType}.${elementPlaceInArray}`
  63. callback(null, element, path, searchFolder)
  64. } else if (element == null) {
  65. endOfBranch()
  66. }
  67. }
  68. const path = { fileSystem: '', mongo: 'rootFolder.0' }
  69. const startSearch = project => {
  70. if (elementId + '' === project.rootFolder[0]._id + '') {
  71. callback(null, project.rootFolder[0], path, null)
  72. } else {
  73. search(project.rootFolder[0], path)
  74. }
  75. }
  76. if (project != null) {
  77. startSearch(project)
  78. } else {
  79. ProjectGetter.getProject(
  80. projectId,
  81. { rootFolder: true, rootDoc_id: true },
  82. (err, project) => {
  83. if (err != null) {
  84. return callback(err)
  85. }
  86. if (project == null) {
  87. return callback(new Errors.NotFoundError('project not found'))
  88. }
  89. startSearch(project)
  90. }
  91. )
  92. }
  93. }
  94. function findRootDoc(opts, callback) {
  95. const getRootDoc = project => {
  96. if (project.rootDoc_id != null) {
  97. findElement(
  98. { project, element_id: project.rootDoc_id, type: 'docs' },
  99. (error, ...args) => {
  100. if (error != null) {
  101. if (error instanceof Errors.NotFoundError) {
  102. return callback(null, null)
  103. } else {
  104. return callback(error)
  105. }
  106. }
  107. callback(null, ...args)
  108. }
  109. )
  110. } else {
  111. callback(null, null)
  112. }
  113. }
  114. const { project, project_id: projectId } = opts
  115. if (project != null) {
  116. getRootDoc(project)
  117. } else {
  118. ProjectGetter.getProject(
  119. projectId,
  120. { rootFolder: true, rootDoc_id: true },
  121. (err, project) => {
  122. if (err != null) {
  123. logger.warn({ err }, 'error getting project')
  124. callback(err)
  125. } else {
  126. getRootDoc(project)
  127. }
  128. }
  129. )
  130. }
  131. }
  132. function findElementByPath(options, callback) {
  133. const { project, project_id: projectId, path, exactCaseMatch } = options
  134. if (path == null) {
  135. return new Error('no path provided for findElementByPath')
  136. }
  137. if (project != null) {
  138. _findElementByPathWithProject(project, path, exactCaseMatch, callback)
  139. } else {
  140. ProjectGetter.getProject(
  141. projectId,
  142. { rootFolder: true, rootDoc_id: true },
  143. (err, project) => {
  144. if (err != null) {
  145. return callback(err)
  146. }
  147. _findElementByPathWithProject(project, path, exactCaseMatch, callback)
  148. }
  149. )
  150. }
  151. }
  152. function _findElementByPathWithProject(
  153. project,
  154. needlePath,
  155. exactCaseMatch,
  156. callback
  157. ) {
  158. let matchFn
  159. if (exactCaseMatch) {
  160. matchFn = (a, b) => a === b
  161. } else {
  162. matchFn = (a, b) =>
  163. (a != null ? a.toLowerCase() : undefined) ===
  164. (b != null ? b.toLowerCase() : undefined)
  165. }
  166. function getParentFolder(haystackFolder, foldersList, level, cb) {
  167. if (foldersList.length === 0) {
  168. return cb(null, haystackFolder)
  169. }
  170. const needleFolderName = foldersList[level]
  171. let found = false
  172. for (const folder of haystackFolder.folders) {
  173. if (matchFn(folder.name, needleFolderName)) {
  174. found = true
  175. if (level === foldersList.length - 1) {
  176. return cb(null, folder)
  177. } else {
  178. return getParentFolder(folder, foldersList, level + 1, cb)
  179. }
  180. }
  181. }
  182. if (!found) {
  183. cb(
  184. new Error(
  185. `not found project: ${project._id} search path: ${needlePath}, folder ${foldersList[level]} could not be found`
  186. )
  187. )
  188. }
  189. }
  190. function getEntity(folder, entityName, cb) {
  191. let result, type
  192. if (entityName == null) {
  193. return cb(null, folder, 'folder', null)
  194. }
  195. for (const file of iterablePaths(folder, 'fileRefs')) {
  196. if (matchFn(file != null ? file.name : undefined, entityName)) {
  197. result = file
  198. type = 'file'
  199. }
  200. }
  201. for (const doc of iterablePaths(folder, 'docs')) {
  202. if (matchFn(doc != null ? doc.name : undefined, entityName)) {
  203. result = doc
  204. type = 'doc'
  205. }
  206. }
  207. for (const childFolder of iterablePaths(folder, 'folders')) {
  208. if (
  209. matchFn(childFolder != null ? childFolder.name : undefined, entityName)
  210. ) {
  211. result = childFolder
  212. type = 'folder'
  213. }
  214. }
  215. if (result != null) {
  216. cb(null, result, type, folder)
  217. } else {
  218. cb(
  219. new Error(
  220. `not found project: ${project._id} search path: ${needlePath}, entity ${entityName} could not be found`
  221. )
  222. )
  223. }
  224. }
  225. if (project == null) {
  226. return callback(new Error('Tried to find an element for a null project'))
  227. }
  228. if (needlePath === '' || needlePath === '/') {
  229. return callback(null, project.rootFolder[0], 'folder', null)
  230. }
  231. if (needlePath.indexOf('/') === 0) {
  232. needlePath = needlePath.substring(1)
  233. }
  234. const foldersList = needlePath.split('/')
  235. const needleName = foldersList.pop()
  236. const rootFolder = project.rootFolder[0]
  237. const jobs = []
  238. jobs.push(cb => getParentFolder(rootFolder, foldersList, 0, cb))
  239. jobs.push((folder, cb) => getEntity(folder, needleName, cb))
  240. async.waterfall(jobs, callback)
  241. }
  242. function sanitizeTypeOfElement(elementType) {
  243. const lastChar = elementType.slice(-1)
  244. if (lastChar !== 's') {
  245. elementType += 's'
  246. }
  247. if (elementType === 'files') {
  248. elementType = 'fileRefs'
  249. }
  250. return elementType
  251. }
  252. function getIndexOf(searchEntity, id) {
  253. const { length } = searchEntity
  254. let count = 0
  255. while (count < length) {
  256. if (
  257. (searchEntity[count] != null ? searchEntity[count]._id : undefined) +
  258. '' ===
  259. id + ''
  260. ) {
  261. return count
  262. }
  263. count++
  264. }
  265. }
  266. /**
  267. * Follow the given Mongo path (as returned by findElement) and return the
  268. * entity at the end of it.
  269. */
  270. function findElementByMongoPath(project, mongoPath) {
  271. const components = mongoPath.split('.')
  272. let node = project
  273. for (const component of components) {
  274. const key = Array.isArray(node) ? parseInt(component, 10) : component
  275. node = node[key]
  276. if (node == null) {
  277. throw new OError('entity not found', {
  278. projectId: project._id,
  279. mongoPath,
  280. })
  281. }
  282. }
  283. return node
  284. }
  285. module.exports = {
  286. findElement,
  287. findElementByPath,
  288. findRootDoc,
  289. findElementByMongoPath,
  290. promises: {
  291. findElement: promisifyMultiResult(findElement, [
  292. 'element',
  293. 'path',
  294. 'folder',
  295. ]),
  296. findElementByPath: promisifyMultiResult(findElementByPath, [
  297. 'element',
  298. 'type',
  299. 'folder',
  300. ]),
  301. findRootDoc: promisifyMultiResult(findRootDoc, [
  302. 'element',
  303. 'path',
  304. 'folder',
  305. ]),
  306. },
  307. }