ProjectLocator.mjs 8.0 KB

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