ProjectDuplicator.js 7.3 KB

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