ProjectUploadManager.mjs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. const Path = require('path')
  2. const fs = require('fs')
  3. const { callbackify } = require('util')
  4. const ArchiveManager = require('./ArchiveManager')
  5. const { Doc } = require('../../models/Doc')
  6. const DocstoreManager = require('../Docstore/DocstoreManager')
  7. const DocumentHelper = require('../Documents/DocumentHelper')
  8. const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
  9. const FileStoreHandler = require('../FileStore/FileStoreHandler')
  10. const FileSystemImportManager = require('./FileSystemImportManager')
  11. const ProjectCreationHandler = require('../Project/ProjectCreationHandler')
  12. const ProjectEntityMongoUpdateHandler = require('../Project/ProjectEntityMongoUpdateHandler')
  13. const ProjectRootDocManager = require('../Project/ProjectRootDocManager')
  14. const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
  15. const ProjectDeleter = require('../Project/ProjectDeleter')
  16. const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher')
  17. const logger = require('@overleaf/logger')
  18. const OError = require('@overleaf/o-error')
  19. module.exports = {
  20. createProjectFromZipArchive: callbackify(createProjectFromZipArchive),
  21. createProjectFromZipArchiveWithName: callbackify(
  22. createProjectFromZipArchiveWithName
  23. ),
  24. promises: {
  25. createProjectFromZipArchive,
  26. createProjectFromZipArchiveWithName,
  27. },
  28. }
  29. async function createProjectFromZipArchive(ownerId, defaultName, zipPath) {
  30. const contentsPath = await _extractZip(zipPath)
  31. const { path, content } =
  32. await ProjectRootDocManager.promises.findRootDocFileFromDirectory(
  33. contentsPath
  34. )
  35. const projectName =
  36. DocumentHelper.getTitleFromTexContent(content || '') || defaultName
  37. const uniqueName = await _generateUniqueName(ownerId, projectName)
  38. const project = await ProjectCreationHandler.promises.createBlankProject(
  39. ownerId,
  40. uniqueName
  41. )
  42. try {
  43. await _initializeProjectWithZipContents(ownerId, project, contentsPath)
  44. if (path) {
  45. await ProjectRootDocManager.promises.setRootDocFromName(project._id, path)
  46. }
  47. } catch (err) {
  48. // no need to wait for the cleanup here
  49. ProjectDeleter.promises
  50. .deleteProject(project._id)
  51. .catch(err =>
  52. logger.error(
  53. { err, projectId: project._id },
  54. 'there was an error cleaning up project after importing a zip failed'
  55. )
  56. )
  57. throw err
  58. }
  59. await fs.promises.rm(contentsPath, { recursive: true, force: true })
  60. return project
  61. }
  62. async function createProjectFromZipArchiveWithName(
  63. ownerId,
  64. proposedName,
  65. zipPath,
  66. attributes = {}
  67. ) {
  68. const contentsPath = await _extractZip(zipPath)
  69. const uniqueName = await _generateUniqueName(ownerId, proposedName)
  70. const project = await ProjectCreationHandler.promises.createBlankProject(
  71. ownerId,
  72. uniqueName,
  73. attributes
  74. )
  75. try {
  76. await _initializeProjectWithZipContents(ownerId, project, contentsPath)
  77. await ProjectRootDocManager.promises.setRootDocAutomatically(project._id)
  78. } catch (err) {
  79. // no need to wait for the cleanup here
  80. ProjectDeleter.promises
  81. .deleteProject(project._id)
  82. .catch(err =>
  83. logger.error(
  84. { err, projectId: project._id },
  85. 'there was an error cleaning up project after importing a zip failed'
  86. )
  87. )
  88. throw err
  89. }
  90. await fs.promises.rm(contentsPath, { recursive: true, force: true })
  91. return project
  92. }
  93. async function _extractZip(zipPath) {
  94. const destination = Path.join(
  95. Path.dirname(zipPath),
  96. `${Path.basename(zipPath, '.zip')}-${Date.now()}`
  97. )
  98. await ArchiveManager.promises.extractZipArchive(zipPath, destination)
  99. return destination
  100. }
  101. async function _generateUniqueName(ownerId, originalName) {
  102. const fixedName = ProjectDetailsHandler.fixProjectName(originalName)
  103. const uniqueName = await ProjectDetailsHandler.promises.generateUniqueName(
  104. ownerId,
  105. fixedName
  106. )
  107. return uniqueName
  108. }
  109. async function _initializeProjectWithZipContents(
  110. ownerId,
  111. project,
  112. contentsPath
  113. ) {
  114. const topLevelDir =
  115. await ArchiveManager.promises.findTopLevelDirectory(contentsPath)
  116. const importEntries =
  117. await FileSystemImportManager.promises.importDir(topLevelDir)
  118. const { fileEntries, docEntries } = await _createEntriesFromImports(
  119. project,
  120. importEntries
  121. )
  122. const projectVersion =
  123. await ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure(
  124. project._id,
  125. docEntries,
  126. fileEntries
  127. )
  128. await _notifyDocumentUpdater(project, ownerId, {
  129. newFiles: fileEntries,
  130. newDocs: docEntries,
  131. newProject: { version: projectVersion },
  132. })
  133. await TpdsProjectFlusher.promises.flushProjectToTpds(project._id)
  134. }
  135. async function _createEntriesFromImports(project, importEntries) {
  136. const fileEntries = []
  137. const docEntries = []
  138. for (const importEntry of importEntries) {
  139. switch (importEntry.type) {
  140. case 'doc': {
  141. const docEntry = await _createDoc(
  142. project,
  143. importEntry.projectPath,
  144. importEntry.lines
  145. )
  146. docEntries.push(docEntry)
  147. break
  148. }
  149. case 'file': {
  150. const fileEntry = await _createFile(
  151. project,
  152. importEntry.projectPath,
  153. importEntry.fsPath
  154. )
  155. fileEntries.push(fileEntry)
  156. break
  157. }
  158. default: {
  159. throw new Error(`Invalid import type: ${importEntry.type}`)
  160. }
  161. }
  162. }
  163. return { fileEntries, docEntries }
  164. }
  165. async function _createDoc(project, projectPath, docLines) {
  166. const projectId = project._id
  167. const docName = Path.basename(projectPath)
  168. const doc = new Doc({ name: docName })
  169. await DocstoreManager.promises.updateDoc(
  170. projectId.toString(),
  171. doc._id.toString(),
  172. docLines,
  173. 0,
  174. {}
  175. )
  176. return { doc, path: projectPath, docLines: docLines.join('\n') }
  177. }
  178. async function _createFile(project, projectPath, fsPath) {
  179. const projectId = project._id
  180. const historyId = project.overleaf?.history?.id
  181. if (!historyId) {
  182. throw new OError('missing history id')
  183. }
  184. const fileName = Path.basename(projectPath)
  185. const { createdBlob, fileRef } =
  186. await FileStoreHandler.promises.uploadFileFromDiskWithHistoryId(
  187. projectId,
  188. historyId,
  189. { name: fileName },
  190. fsPath
  191. )
  192. return { createdBlob, file: fileRef, path: projectPath }
  193. }
  194. async function _notifyDocumentUpdater(project, userId, changes) {
  195. const projectHistoryId =
  196. project.overleaf && project.overleaf.history && project.overleaf.history.id
  197. await DocumentUpdaterHandler.promises.updateProjectStructure(
  198. project._id,
  199. projectHistoryId,
  200. userId,
  201. changes,
  202. null
  203. )
  204. }