ProjectUploadManager.mjs 6.9 KB

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