ProjectEntityMongoUpdateHandler.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. const { callbackify } = require('util')
  2. const { callbackifyMultiResult } = require('../../util/promises')
  3. const _ = require('underscore')
  4. const logger = require('logger-sharelatex')
  5. const path = require('path')
  6. const { ObjectId } = require('mongodb')
  7. const Settings = require('settings-sharelatex')
  8. const OError = require('@overleaf/o-error')
  9. const CooldownManager = require('../Cooldown/CooldownManager')
  10. const Errors = require('../Errors/Errors')
  11. const { Folder } = require('../../models/Folder')
  12. const LockManager = require('../../infrastructure/LockManager')
  13. const { Project } = require('../../models/Project')
  14. const ProjectEntityHandler = require('./ProjectEntityHandler')
  15. const ProjectGetter = require('./ProjectGetter')
  16. const ProjectLocator = require('./ProjectLocator')
  17. const FolderStructureBuilder = require('./FolderStructureBuilder')
  18. const SafePath = require('./SafePath')
  19. const LOCK_NAMESPACE = 'mongoTransaction'
  20. const ENTITY_TYPE_TO_MONGO_PATH_SEGMENT = {
  21. doc: 'docs',
  22. docs: 'docs',
  23. file: 'fileRefs',
  24. files: 'fileRefs',
  25. fileRefs: 'fileRefs',
  26. folder: 'folders',
  27. folders: 'folders'
  28. }
  29. module.exports = {
  30. LOCK_NAMESPACE,
  31. addDoc: callbackifyMultiResult(wrapWithLock(addDoc), ['result', 'project']),
  32. addFile: callbackifyMultiResult(wrapWithLock(addFile), ['result', 'project']),
  33. addFolder: callbackifyMultiResult(wrapWithLock(addFolder), [
  34. 'folder',
  35. 'parentFolderId'
  36. ]),
  37. replaceFileWithNew: callbackifyMultiResult(wrapWithLock(replaceFileWithNew), [
  38. 'oldFileRef',
  39. 'project',
  40. 'path',
  41. 'newProject'
  42. ]),
  43. replaceDocWithFile: callbackify(replaceDocWithFile),
  44. replaceFileWithDoc: callbackify(replaceFileWithDoc),
  45. mkdirp: callbackifyMultiResult(wrapWithLock(mkdirp), [
  46. 'newFolders',
  47. 'folder'
  48. ]),
  49. moveEntity: callbackifyMultiResult(wrapWithLock(moveEntity), [
  50. 'project',
  51. 'startPath',
  52. 'endPath',
  53. 'rev',
  54. 'changes'
  55. ]),
  56. deleteEntity: callbackifyMultiResult(wrapWithLock(deleteEntity), [
  57. 'entity',
  58. 'path',
  59. 'projectBeforeDeletion',
  60. 'newProject'
  61. ]),
  62. renameEntity: callbackifyMultiResult(wrapWithLock(renameEntity), [
  63. 'project',
  64. 'startPath',
  65. 'endPath',
  66. 'rev',
  67. 'changes'
  68. ]),
  69. createNewFolderStructure: callbackify(wrapWithLock(createNewFolderStructure)),
  70. _insertDeletedDocReference: callbackify(_insertDeletedDocReference),
  71. _insertDeletedFileReference: callbackify(_insertDeletedFileReference),
  72. _putElement: callbackifyMultiResult(_putElement, ['result', 'project']),
  73. _confirmFolder,
  74. promises: {
  75. addDoc: wrapWithLock(addDoc),
  76. addFile: wrapWithLock(addFile),
  77. addFolder: wrapWithLock(addFolder),
  78. replaceFileWithNew: wrapWithLock(replaceFileWithNew),
  79. replaceDocWithFile: wrapWithLock(replaceDocWithFile),
  80. replaceFileWithDoc: wrapWithLock(replaceFileWithDoc),
  81. mkdirp: wrapWithLock(mkdirp),
  82. moveEntity: wrapWithLock(moveEntity),
  83. deleteEntity: wrapWithLock(deleteEntity),
  84. renameEntity: wrapWithLock(renameEntity),
  85. createNewFolderStructure: wrapWithLock(createNewFolderStructure),
  86. _insertDeletedDocReference,
  87. _insertDeletedFileReference,
  88. _putElement
  89. }
  90. }
  91. function wrapWithLock(methodWithoutLock) {
  92. // This lock is used whenever we read or write to an existing project's
  93. // structure. Some operations to project structure cannot be done atomically
  94. // in mongo, this lock is used to prevent reading the structure between two
  95. // parts of a staged update.
  96. async function methodWithLock(projectId, ...rest) {
  97. return LockManager.promises.runWithLock(LOCK_NAMESPACE, projectId, () =>
  98. methodWithoutLock(projectId, ...rest)
  99. )
  100. }
  101. return methodWithLock
  102. }
  103. async function addDoc(projectId, folderId, doc) {
  104. const project = await ProjectGetter.promises.getProjectWithoutLock(
  105. projectId,
  106. {
  107. rootFolder: true,
  108. name: true,
  109. overleaf: true
  110. }
  111. )
  112. folderId = _confirmFolder(project, folderId)
  113. const { result, project: newProject } = await _putElement(
  114. project,
  115. folderId,
  116. doc,
  117. 'doc'
  118. )
  119. return { result, project: newProject }
  120. }
  121. async function addFile(projectId, folderId, fileRef) {
  122. const project = await ProjectGetter.promises.getProjectWithoutLock(
  123. projectId,
  124. { rootFolder: true, name: true, overleaf: true }
  125. )
  126. folderId = _confirmFolder(project, folderId)
  127. const { result, project: newProject } = await _putElement(
  128. project,
  129. folderId,
  130. fileRef,
  131. 'file'
  132. )
  133. return { result, project: newProject }
  134. }
  135. async function addFolder(projectId, parentFolderId, folderName) {
  136. const project = await ProjectGetter.promises.getProjectWithoutLock(
  137. projectId,
  138. { rootFolder: true, name: true, overleaf: true }
  139. )
  140. parentFolderId = _confirmFolder(project, parentFolderId)
  141. const folder = new Folder({ name: folderName })
  142. await _putElement(project, parentFolderId, folder, 'folder')
  143. return { folder, parentFolderId }
  144. }
  145. async function replaceFileWithNew(projectId, fileId, newFileRef) {
  146. const project = await ProjectGetter.promises.getProjectWithoutLock(
  147. projectId,
  148. { rootFolder: true, name: true, overleaf: true }
  149. )
  150. const { element: fileRef, path } = await ProjectLocator.promises.findElement({
  151. project,
  152. element_id: fileId,
  153. type: 'file'
  154. })
  155. await _insertDeletedFileReference(projectId, fileRef)
  156. const newProject = await Project.findOneAndUpdate(
  157. { _id: project._id },
  158. {
  159. $set: {
  160. [`${path.mongo}._id`]: newFileRef._id,
  161. [`${path.mongo}.created`]: new Date(),
  162. [`${path.mongo}.linkedFileData`]: newFileRef.linkedFileData,
  163. [`${path.mongo}.hash`]: newFileRef.hash
  164. },
  165. $inc: {
  166. version: 1,
  167. [`${path.mongo}.rev`]: 1
  168. }
  169. },
  170. { new: true }
  171. ).exec()
  172. // Note: Mongoose uses new:true to return the modified document
  173. // https://mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate
  174. // but Mongo uses returnNewDocument:true instead
  175. // https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndUpdate/
  176. // We are using Mongoose here, but if we ever switch to a direct mongo call
  177. // the next line will need to be updated.
  178. return { oldFileRef: fileRef, project, path, newProject }
  179. }
  180. async function replaceDocWithFile(projectId, docId, fileRef) {
  181. const project = await ProjectGetter.promises.getProjectWithoutLock(
  182. projectId,
  183. { rootFolder: true, name: true, overleaf: true }
  184. )
  185. const { path } = await ProjectLocator.promises.findElement({
  186. project,
  187. element_id: docId,
  188. type: 'doc'
  189. })
  190. const folderMongoPath = _getParentMongoPath(path.mongo)
  191. const newProject = await Project.findOneAndUpdate(
  192. { _id: project._id },
  193. {
  194. $pull: {
  195. [`${folderMongoPath}.docs`]: { _id: docId }
  196. },
  197. $push: {
  198. [`${folderMongoPath}.fileRefs`]: fileRef
  199. },
  200. $inc: { version: 1 }
  201. },
  202. { new: true }
  203. ).exec()
  204. return newProject
  205. }
  206. async function replaceFileWithDoc(projectId, fileId, newDoc) {
  207. const project = await ProjectGetter.promises.getProjectWithoutLock(
  208. projectId,
  209. { rootFolder: true, name: true, overleaf: true }
  210. )
  211. const { path } = await ProjectLocator.promises.findElement({
  212. project,
  213. element_id: fileId,
  214. type: 'file'
  215. })
  216. const folderMongoPath = _getParentMongoPath(path.mongo)
  217. const newProject = await Project.findOneAndUpdate(
  218. { _id: project._id },
  219. {
  220. $pull: {
  221. [`${folderMongoPath}.fileRefs`]: { _id: fileId }
  222. },
  223. $push: {
  224. [`${folderMongoPath}.docs`]: newDoc
  225. },
  226. $inc: { version: 1 }
  227. },
  228. { new: true }
  229. ).exec()
  230. return newProject
  231. }
  232. async function mkdirp(projectId, path, options = {}) {
  233. // defaults to case insensitive paths, use options {exactCaseMatch:true}
  234. // to make matching case-sensitive
  235. let folders = path.split('/')
  236. folders = _.select(folders, folder => folder.length !== 0)
  237. const project = await ProjectGetter.promises.getProjectWithOnlyFolders(
  238. projectId
  239. )
  240. if (path === '/') {
  241. return { newFolders: [], folder: project.rootFolder[0] }
  242. }
  243. const newFolders = []
  244. let builtUpPath = ''
  245. let lastFolder = null
  246. for (const folderName of folders) {
  247. builtUpPath += `/${folderName}`
  248. try {
  249. const {
  250. element: foundFolder
  251. } = await ProjectLocator.promises.findElementByPath({
  252. project,
  253. path: builtUpPath,
  254. exactCaseMatch: options.exactCaseMatch
  255. })
  256. lastFolder = foundFolder
  257. } catch (err) {
  258. // Folder couldn't be found. Create it.
  259. const parentFolderId = lastFolder && lastFolder._id
  260. const {
  261. folder: newFolder,
  262. parentFolderId: newParentFolderId
  263. } = await addFolder(projectId, parentFolderId, folderName)
  264. newFolder.parentFolder_id = newParentFolderId
  265. lastFolder = newFolder
  266. newFolders.push(newFolder)
  267. }
  268. }
  269. return { folder: lastFolder, newFolders }
  270. }
  271. async function moveEntity(projectId, entityId, destFolderId, entityType) {
  272. const project = await ProjectGetter.promises.getProjectWithoutLock(
  273. projectId,
  274. { rootFolder: true, name: true, overleaf: true }
  275. )
  276. const {
  277. element: entity,
  278. path: entityPath
  279. } = await ProjectLocator.promises.findElement({
  280. project,
  281. element_id: entityId,
  282. type: entityType
  283. })
  284. // Prevent top-level docs/files with reserved names (to match v1 behaviour)
  285. if (_blockedFilename(entityPath, entityType)) {
  286. throw new Errors.InvalidNameError('blocked element name')
  287. }
  288. await _checkValidMove(project, entityType, entity, entityPath, destFolderId)
  289. const {
  290. docs: oldDocs,
  291. files: oldFiles
  292. } = await ProjectEntityHandler.promises.getAllEntitiesFromProject(project)
  293. // For safety, insert the entity in the destination
  294. // location first, and then remove the original. If
  295. // there is an error the entity may appear twice. This
  296. // will cause some breakage but is better than being
  297. // lost, which is what happens if this is done in the
  298. // opposite order.
  299. const { result } = await _putElement(
  300. project,
  301. destFolderId,
  302. entity,
  303. entityType
  304. )
  305. // Note: putElement always pushes onto the end of an
  306. // array so it will never change an existing mongo
  307. // path. Therefore it is safe to remove an element
  308. // from the project with an existing path after
  309. // calling putElement. But we must be sure that we
  310. // have not moved a folder subfolder of itself (which
  311. // is done by _checkValidMove above) because that
  312. // would lead to it being deleted.
  313. const newProject = await _removeElementFromMongoArray(
  314. Project,
  315. projectId,
  316. entityPath.mongo,
  317. entityId
  318. )
  319. const {
  320. docs: newDocs,
  321. files: newFiles
  322. } = await ProjectEntityHandler.promises.getAllEntitiesFromProject(newProject)
  323. const startPath = entityPath.fileSystem
  324. const endPath = result.path.fileSystem
  325. const changes = {
  326. oldDocs,
  327. newDocs,
  328. oldFiles,
  329. newFiles,
  330. newProject
  331. }
  332. // check that no files have been lost (or duplicated)
  333. if (
  334. oldFiles.length !== newFiles.length ||
  335. oldDocs.length !== newDocs.length
  336. ) {
  337. logger.warn(
  338. {
  339. projectId,
  340. oldDocs: oldDocs.length,
  341. newDocs: newDocs.length,
  342. oldFiles: oldFiles.length,
  343. newFiles: newFiles.length,
  344. origProject: project,
  345. newProject
  346. },
  347. "project corrupted moving files - shouldn't happen"
  348. )
  349. throw new Error('unexpected change in project structure')
  350. }
  351. return { project, startPath, endPath, rev: entity.rev, changes }
  352. }
  353. async function deleteEntity(projectId, entityId, entityType, callback) {
  354. const project = await ProjectGetter.promises.getProjectWithoutLock(
  355. projectId,
  356. { name: true, rootFolder: true, overleaf: true, rootDoc_id: true }
  357. )
  358. const deleteRootDoc =
  359. project.rootDoc_id &&
  360. entityId &&
  361. project.rootDoc_id.toString() === entityId.toString()
  362. const { element: entity, path } = await ProjectLocator.promises.findElement({
  363. project,
  364. element_id: entityId,
  365. type: entityType
  366. })
  367. const newProject = await _removeElementFromMongoArray(
  368. Project,
  369. projectId,
  370. path.mongo,
  371. entityId,
  372. deleteRootDoc
  373. )
  374. return { entity, path, projectBeforeDeletion: project, newProject }
  375. }
  376. async function renameEntity(
  377. projectId,
  378. entityId,
  379. entityType,
  380. newName,
  381. callback
  382. ) {
  383. const project = await ProjectGetter.promises.getProjectWithoutLock(
  384. projectId,
  385. { rootFolder: true, name: true, overleaf: true }
  386. )
  387. const {
  388. element: entity,
  389. path: entPath,
  390. folder: parentFolder
  391. } = await ProjectLocator.promises.findElement({
  392. project,
  393. element_id: entityId,
  394. type: entityType
  395. })
  396. const startPath = entPath.fileSystem
  397. const endPath = path.join(path.dirname(entPath.fileSystem), newName)
  398. // Prevent top-level docs/files with reserved names (to match v1 behaviour)
  399. if (_blockedFilename({ fileSystem: endPath }, entityType)) {
  400. throw new Errors.InvalidNameError('blocked element name')
  401. }
  402. // check if the new name already exists in the current folder
  403. _checkValidElementName(parentFolder, newName)
  404. const {
  405. docs: oldDocs,
  406. files: oldFiles
  407. } = await ProjectEntityHandler.promises.getAllEntitiesFromProject(project)
  408. // we need to increment the project version number for any structure change
  409. const newProject = await Project.findOneAndUpdate(
  410. { _id: projectId },
  411. { $set: { [`${entPath.mongo}.name`]: newName }, $inc: { version: 1 } },
  412. { new: true }
  413. ).exec()
  414. const {
  415. docs: newDocs,
  416. files: newFiles
  417. } = await ProjectEntityHandler.promises.getAllEntitiesFromProject(newProject)
  418. return {
  419. project,
  420. startPath,
  421. endPath,
  422. rev: entity.rev,
  423. changes: { oldDocs, newDocs, oldFiles, newFiles, newProject }
  424. }
  425. }
  426. async function _insertDeletedDocReference(projectId, doc) {
  427. await Project.updateOne(
  428. { _id: projectId },
  429. {
  430. $push: {
  431. deletedDocs: { _id: doc._id, name: doc.name, deletedAt: new Date() }
  432. }
  433. }
  434. ).exec()
  435. }
  436. async function _insertDeletedFileReference(projectId, fileRef) {
  437. await Project.updateOne(
  438. { _id: projectId },
  439. {
  440. $push: {
  441. deletedFiles: {
  442. _id: fileRef._id,
  443. name: fileRef.name,
  444. linkedFileData: fileRef.linkedFileData,
  445. hash: fileRef.hash,
  446. deletedAt: new Date()
  447. }
  448. }
  449. }
  450. ).exec()
  451. }
  452. async function _removeElementFromMongoArray(
  453. model,
  454. modelId,
  455. path,
  456. elementId,
  457. deleteRootDoc = false
  458. ) {
  459. const nonArrayPath = path.slice(0, path.lastIndexOf('.'))
  460. const options = { new: true }
  461. const query = { _id: modelId }
  462. const update = {
  463. $pull: { [nonArrayPath]: { _id: elementId } },
  464. $inc: { version: 1 }
  465. }
  466. if (deleteRootDoc) {
  467. update.$unset = { rootDoc_id: 1 }
  468. }
  469. return model.findOneAndUpdate(query, update, options).exec()
  470. }
  471. function _countElements(project) {
  472. function countFolder(folder) {
  473. if (folder == null) {
  474. return 0
  475. }
  476. let total = 0
  477. if (folder.folders) {
  478. total += folder.folders.length
  479. for (const subfolder of folder.folders) {
  480. total += countFolder(subfolder)
  481. }
  482. }
  483. if (folder.docs) {
  484. total += folder.docs.length
  485. }
  486. if (folder.fileRefs) {
  487. total += folder.fileRefs.length
  488. }
  489. return total
  490. }
  491. return countFolder(project.rootFolder[0])
  492. }
  493. async function _putElement(project, folderId, element, type) {
  494. if (element == null || element._id == null) {
  495. logger.warn(
  496. { projectId: project._id, folderId, element, type },
  497. 'failed trying to insert element as it was null'
  498. )
  499. throw new Error('no element passed to be inserted')
  500. }
  501. const pathSegment = _getMongoPathSegmentFromType(type)
  502. // original check path.resolve("/", element.name) isnt "/#{element.name}" or element.name.match("/")
  503. // check if name is allowed
  504. if (!SafePath.isCleanFilename(element.name)) {
  505. logger.warn(
  506. { projectId: project._id, folderId, element, type },
  507. 'failed trying to insert element as name was invalid'
  508. )
  509. throw new Errors.InvalidNameError('invalid element name')
  510. }
  511. if (folderId == null) {
  512. folderId = project.rootFolder[0]._id
  513. }
  514. if (_countElements(project) > Settings.maxEntitiesPerProject) {
  515. logger.warn(
  516. { projectId: project._id },
  517. 'project too big, stopping insertions'
  518. )
  519. CooldownManager.putProjectOnCooldown(project._id)
  520. throw new Error('project_has_too_many_files')
  521. }
  522. const { element: folder, path } = await ProjectLocator.promises.findElement({
  523. project,
  524. element_id: folderId,
  525. type: 'folder'
  526. })
  527. const newPath = {
  528. fileSystem: `${path.fileSystem}/${element.name}`,
  529. mongo: path.mongo
  530. }
  531. // check if the path would be too long
  532. if (!SafePath.isAllowedLength(newPath.fileSystem)) {
  533. throw new Errors.InvalidNameError('path too long')
  534. }
  535. // Prevent top-level docs/files with reserved names (to match v1 behaviour)
  536. if (_blockedFilename(newPath, type)) {
  537. throw new Errors.InvalidNameError('blocked element name')
  538. }
  539. _checkValidElementName(folder, element.name)
  540. element._id = ObjectId(element._id.toString())
  541. const mongoPath = `${path.mongo}.${pathSegment}`
  542. const newProject = await Project.findOneAndUpdate(
  543. { _id: project._id },
  544. { $push: { [mongoPath]: element }, $inc: { version: 1 } },
  545. { new: true }
  546. ).exec()
  547. return { result: { path: newPath }, project: newProject }
  548. }
  549. function _blockedFilename(entityPath, entityType) {
  550. // check if name would be blocked in v1
  551. // javascript reserved names are forbidden for docs and files
  552. // at the top-level (but folders with reserved names are allowed).
  553. const isFolder = entityType === 'folder'
  554. const dir = path.dirname(entityPath.fileSystem)
  555. const file = path.basename(entityPath.fileSystem)
  556. const isTopLevel = dir === '/'
  557. if (isTopLevel && !isFolder && SafePath.isBlockedFilename(file)) {
  558. return true
  559. } else {
  560. return false
  561. }
  562. }
  563. function _getMongoPathSegmentFromType(type) {
  564. const pathSegment = ENTITY_TYPE_TO_MONGO_PATH_SEGMENT[type]
  565. if (pathSegment == null) {
  566. throw new Error(`Unknown entity type: ${type}`)
  567. }
  568. return pathSegment
  569. }
  570. /**
  571. * Check if the name is already taken by a doc, file or folder. If so, return an
  572. * error "file already exists".
  573. */
  574. function _checkValidElementName(folder, name) {
  575. if (folder == null) {
  576. return
  577. }
  578. const elements = []
  579. .concat(folder.docs || [])
  580. .concat(folder.fileRefs || [])
  581. .concat(folder.folders || [])
  582. for (const element of elements) {
  583. if (element.name === name) {
  584. throw new Errors.InvalidNameError('file already exists')
  585. }
  586. }
  587. }
  588. function _confirmFolder(project, folderId) {
  589. if (folderId == null) {
  590. return project.rootFolder[0]._id
  591. } else {
  592. return folderId
  593. }
  594. }
  595. async function _checkValidMove(
  596. project,
  597. entityType,
  598. entity,
  599. entityPath,
  600. destFolderId
  601. ) {
  602. const {
  603. element: destEntity,
  604. path: destFolderPath
  605. } = await ProjectLocator.promises.findElement({
  606. project,
  607. element_id: destFolderId,
  608. type: 'folder'
  609. })
  610. // check if there is already a doc/file/folder with the same name
  611. // in the destination folder
  612. _checkValidElementName(destEntity, entity.name)
  613. if (/folder/.test(entityType)) {
  614. const isNestedFolder =
  615. destFolderPath.fileSystem.slice(0, entityPath.fileSystem.length) ===
  616. entityPath.fileSystem
  617. if (isNestedFolder) {
  618. throw new Errors.InvalidNameError(
  619. 'destination folder is a child folder of me'
  620. )
  621. }
  622. }
  623. }
  624. /**
  625. * Create an initial file tree out of a list of doc and file entries
  626. *
  627. * Each entry specifies a path to the doc or file. Folders are automatically
  628. * created.
  629. *
  630. * @param {ObjectId} projectId - id of the project
  631. * @param {DocEntry[]} docEntries - list of docs to add
  632. * @param {FileEntry[]} fileEntries - list of files to add
  633. * @return {Promise<string>} the project version after the operation
  634. */
  635. async function createNewFolderStructure(projectId, docEntries, fileEntries) {
  636. try {
  637. const rootFolder = FolderStructureBuilder.buildFolderStructure(
  638. docEntries,
  639. fileEntries
  640. )
  641. const project = await Project.findOneAndUpdate(
  642. {
  643. _id: projectId,
  644. 'rootFolder.0.folders.0': { $exists: false },
  645. 'rootFolder.0.docs.0': { $exists: false },
  646. 'rootFolder.0.files.0': { $exists: false }
  647. },
  648. {
  649. $set: { rootFolder: [rootFolder] },
  650. $inc: { version: 1 }
  651. },
  652. {
  653. new: true,
  654. lean: true,
  655. fields: { version: 1 }
  656. }
  657. ).exec()
  658. if (project == null) {
  659. throw new OError('project not found or folder structure already exists', {
  660. projectId
  661. })
  662. }
  663. return project.version
  664. } catch (err) {
  665. throw OError.tag(err, 'failed to create folder structure', { projectId })
  666. }
  667. }
  668. /**
  669. * Given a Mongo path to an entity, return the Mongo path to the parent folder
  670. */
  671. function _getParentMongoPath(mongoPath) {
  672. const segments = mongoPath.split('.')
  673. if (segments.length <= 2) {
  674. throw new Error('Root folder has no parents')
  675. }
  676. return segments.slice(0, -2).join('.')
  677. }