ProjectLocator.mjs 8.9 KB

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