ProjectDuplicator.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. const { callbackify } = require('util')
  2. const Path = require('path')
  3. const OError = require('@overleaf/o-error')
  4. const { promiseMapWithLimit } = require('../../util/promises')
  5. const { Doc } = require('../../models/Doc')
  6. const { File } = require('../../models/File')
  7. const DocstoreManager = require('../Docstore/DocstoreManager')
  8. const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
  9. const FileStoreHandler = require('../FileStore/FileStoreHandler')
  10. const ProjectCreationHandler = require('./ProjectCreationHandler')
  11. const ProjectDeleter = require('./ProjectDeleter')
  12. const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler')
  13. const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler')
  14. const ProjectGetter = require('./ProjectGetter')
  15. const ProjectLocator = require('./ProjectLocator')
  16. const ProjectOptionsHandler = require('./ProjectOptionsHandler')
  17. const SafePath = require('./SafePath')
  18. const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher')
  19. const _ = require('lodash')
  20. module.exports = {
  21. duplicate: callbackify(duplicate),
  22. promises: {
  23. duplicate,
  24. },
  25. }
  26. async function duplicate(owner, originalProjectId, newProjectName) {
  27. await DocumentUpdaterHandler.promises.flushProjectToMongo(originalProjectId)
  28. const originalProject = await ProjectGetter.promises.getProject(
  29. originalProjectId,
  30. {
  31. compiler: true,
  32. rootFolder: true,
  33. rootDoc_id: true,
  34. fromV1TemplateId: true,
  35. fromV1TemplateVersionId: true,
  36. }
  37. )
  38. const { path: rootDocPath } = await ProjectLocator.promises.findRootDoc({
  39. project_id: originalProjectId,
  40. })
  41. const originalEntries = _getFolderEntries(originalProject.rootFolder[0])
  42. // Pass template ID as analytics segmentation if duplicating project from a template
  43. const segmentation = _.pick(originalProject, [
  44. 'fromV1TemplateId',
  45. 'fromV1TemplateVersionId',
  46. ])
  47. segmentation.duplicatedFromProject = originalProjectId
  48. // Now create the new project, cleaning it up on failure if necessary
  49. const newProject = await ProjectCreationHandler.promises.createBlankProject(
  50. owner._id,
  51. newProjectName,
  52. { segmentation }
  53. )
  54. try {
  55. await ProjectOptionsHandler.promises.setCompiler(
  56. newProject._id,
  57. originalProject.compiler
  58. )
  59. const [docEntries, fileEntries] = await Promise.all([
  60. _copyDocs(originalEntries.docEntries, originalProject, newProject),
  61. _copyFiles(originalEntries.fileEntries, originalProject, newProject),
  62. ])
  63. const projectVersion =
  64. await ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure(
  65. newProject._id,
  66. docEntries,
  67. fileEntries
  68. )
  69. // Silently ignore the rootDoc in case it's not valid per the new limits.
  70. if (
  71. rootDocPath &&
  72. ProjectEntityUpdateHandler.isPathValidForRootDoc(rootDocPath.fileSystem)
  73. ) {
  74. await _setRootDoc(newProject._id, rootDocPath.fileSystem)
  75. }
  76. await _notifyDocumentUpdater(newProject, owner._id, {
  77. newFiles: fileEntries,
  78. newDocs: docEntries,
  79. newProject: { version: projectVersion },
  80. })
  81. await TpdsProjectFlusher.promises.flushProjectToTpds(newProject._id)
  82. } catch (err) {
  83. // Clean up broken clone on error.
  84. // Make sure we delete the new failed project, not the original one!
  85. await ProjectDeleter.promises.deleteProject(newProject._id)
  86. throw OError.tag(err, 'error cloning project, broken clone deleted', {
  87. originalProjectId,
  88. newProjectName,
  89. newProjectId: newProject._id,
  90. })
  91. }
  92. return newProject
  93. }
  94. function _getFolderEntries(folder, folderPath = '/') {
  95. const docEntries = []
  96. const fileEntries = []
  97. const docs = folder.docs || []
  98. const files = folder.fileRefs || []
  99. const subfolders = folder.folders || []
  100. for (const doc of docs) {
  101. if (doc == null || doc._id == null) {
  102. continue
  103. }
  104. const path = Path.join(folderPath, doc.name)
  105. docEntries.push({ doc, path })
  106. }
  107. for (const file of files) {
  108. if (file == null || file._id == null) {
  109. continue
  110. }
  111. const path = Path.join(folderPath, file.name)
  112. fileEntries.push({ file, path })
  113. }
  114. for (const subfolder of subfolders) {
  115. if (subfolder == null || subfolder._id == null) {
  116. continue
  117. }
  118. const subfolderPath = Path.join(folderPath, subfolder.name)
  119. const subfolderEntries = _getFolderEntries(subfolder, subfolderPath)
  120. for (const docEntry of subfolderEntries.docEntries) {
  121. docEntries.push(docEntry)
  122. }
  123. for (const fileEntry of subfolderEntries.fileEntries) {
  124. fileEntries.push(fileEntry)
  125. }
  126. }
  127. return { docEntries, fileEntries }
  128. }
  129. async function _copyDocs(sourceEntries, sourceProject, targetProject) {
  130. const docLinesById = await _getDocLinesForProject(sourceProject._id)
  131. const targetEntries = []
  132. for (const sourceEntry of sourceEntries) {
  133. const sourceDoc = sourceEntry.doc
  134. const path = sourceEntry.path
  135. const doc = new Doc({ name: sourceDoc.name })
  136. const docLines = docLinesById.get(sourceDoc._id.toString())
  137. await DocstoreManager.promises.updateDoc(
  138. targetProject._id.toString(),
  139. doc._id.toString(),
  140. docLines,
  141. 0,
  142. {}
  143. )
  144. targetEntries.push({ doc, path, docLines: docLines.join('\n') })
  145. }
  146. return targetEntries
  147. }
  148. async function _getDocLinesForProject(projectId) {
  149. const docs = await DocstoreManager.promises.getAllDocs(projectId)
  150. const docLinesById = new Map(docs.map(doc => [doc._id, doc.lines]))
  151. return docLinesById
  152. }
  153. async function _copyFiles(sourceEntries, sourceProject, targetProject) {
  154. const targetEntries = await promiseMapWithLimit(
  155. 5,
  156. sourceEntries,
  157. async sourceEntry => {
  158. const sourceFile = sourceEntry.file
  159. const path = sourceEntry.path
  160. const file = new File({ name: SafePath.clean(sourceFile.name) })
  161. if (sourceFile.linkedFileData != null) {
  162. file.linkedFileData = sourceFile.linkedFileData
  163. }
  164. if (sourceFile.hash != null) {
  165. file.hash = sourceFile.hash
  166. }
  167. const url = await FileStoreHandler.promises.copyFile(
  168. sourceProject._id,
  169. sourceFile._id,
  170. targetProject._id,
  171. file._id
  172. )
  173. return { file, path, url }
  174. }
  175. )
  176. return targetEntries
  177. }
  178. async function _setRootDoc(projectId, path) {
  179. const { element: rootDoc } = await ProjectLocator.promises.findElementByPath({
  180. project_id: projectId,
  181. path,
  182. exactCaseMatch: true,
  183. })
  184. await ProjectEntityUpdateHandler.promises.setRootDoc(projectId, rootDoc._id)
  185. }
  186. async function _notifyDocumentUpdater(project, userId, changes) {
  187. const projectHistoryId =
  188. project.overleaf && project.overleaf.history && project.overleaf.history.id
  189. await DocumentUpdaterHandler.promises.updateProjectStructure(
  190. project._id,
  191. projectHistoryId,
  192. userId,
  193. changes
  194. )
  195. }