TemplatesManager.mjs 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import { Project } from '../../models/Project.js'
  2. import ProjectDetailsHandler from '../Project/ProjectDetailsHandler.mjs'
  3. import ProjectOptionsHandlerModule from '../Project/ProjectOptionsHandler.mjs'
  4. import ProjectRootDocManagerModule from '../Project/ProjectRootDocManager.mjs'
  5. import ProjectUploadManager from '../Uploads/ProjectUploadManager.mjs'
  6. import fs from 'node:fs'
  7. import util from 'node:util'
  8. import logger from '@overleaf/logger'
  9. import {
  10. fetchJson,
  11. fetchStreamWithResponse,
  12. RequestFailedError,
  13. } from '@overleaf/fetch-utils'
  14. import settings from '@overleaf/settings'
  15. import crypto from 'node:crypto'
  16. import Errors from '../Errors/Errors.js'
  17. import { pipeline } from 'node:stream/promises'
  18. import ClsiCacheManager from '../Compile/ClsiCacheManager.mjs'
  19. const { promises: ProjectRootDocManager } = ProjectRootDocManagerModule
  20. const { promises: ProjectOptionsHandler } = ProjectOptionsHandlerModule
  21. const TemplatesManager = {
  22. async createProjectFromV1Template(
  23. brandVariationId,
  24. compiler,
  25. mainFile,
  26. templateId,
  27. templateName,
  28. templateVersionId,
  29. userId,
  30. imageName
  31. ) {
  32. const zipUrl = `${settings.apis.v1.url}/api/v1/overleaf/templates/${templateVersionId}`
  33. const zipReq = await fetchStreamWithResponse(zipUrl, {
  34. basicAuth: {
  35. user: settings.apis.v1.user,
  36. password: settings.apis.v1.pass,
  37. },
  38. signal: AbortSignal.timeout(settings.apis.v1.timeout),
  39. })
  40. const projectName = ProjectDetailsHandler.fixProjectName(templateName)
  41. const dumpPath = `${settings.path.dumpFolder}/${crypto.randomUUID()}`
  42. const writeStream = fs.createWriteStream(dumpPath)
  43. try {
  44. const attributes = {
  45. fromV1TemplateId: templateId,
  46. fromV1TemplateVersionId: templateVersionId,
  47. }
  48. await pipeline(zipReq.stream, writeStream)
  49. if (zipReq.response.status !== 200) {
  50. logger.warn(
  51. { uri: zipUrl, statusCode: zipReq.response.status },
  52. 'non-success code getting zip from template API'
  53. )
  54. throw new Error(`get zip failed: ${zipReq.response.status}`)
  55. }
  56. const project =
  57. await ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
  58. userId,
  59. projectName,
  60. dumpPath,
  61. attributes
  62. )
  63. const prepareClsiCacheInBackground = ClsiCacheManager.prepareClsiCache(
  64. project._id,
  65. userId,
  66. { templateId, templateVersionId }
  67. ).catch(err => {
  68. logger.warn(
  69. { err, templateId, templateVersionId, projectId: project._id },
  70. 'failed to prepare clsi-cache from template'
  71. )
  72. })
  73. await TemplatesManager._setCompiler(project._id, compiler)
  74. await TemplatesManager._setImage(project._id, imageName)
  75. await TemplatesManager._setMainFile(project._id, mainFile)
  76. await TemplatesManager._setBrandVariationId(project._id, brandVariationId)
  77. const update = {
  78. fromV1TemplateId: templateId,
  79. fromV1TemplateVersionId: templateVersionId,
  80. }
  81. await Project.updateOne({ _id: project._id }, update, {})
  82. await prepareClsiCacheInBackground
  83. return project
  84. } finally {
  85. await fs.promises.unlink(dumpPath)
  86. }
  87. },
  88. async _setCompiler(projectId, compiler) {
  89. if (compiler == null) {
  90. return
  91. }
  92. await ProjectOptionsHandler.setCompiler(projectId, compiler)
  93. },
  94. async _setImage(projectId, imageName) {
  95. if (!imageName) {
  96. imageName = 'wl_texlive:2018.1'
  97. }
  98. await ProjectOptionsHandler.setImageName(projectId, imageName)
  99. },
  100. async _setMainFile(projectId, mainFile) {
  101. if (mainFile == null) {
  102. return
  103. }
  104. await ProjectRootDocManager.setRootDocFromName(projectId, mainFile)
  105. },
  106. async _setBrandVariationId(projectId, brandVariationId) {
  107. if (brandVariationId == null) {
  108. return
  109. }
  110. await ProjectOptionsHandler.setBrandVariationId(projectId, brandVariationId)
  111. },
  112. async fetchFromV1(templateId) {
  113. const url = new URL(`/api/v2/templates/${templateId}`, settings.apis.v1.url)
  114. try {
  115. return await fetchJson(url, {
  116. basicAuth: {
  117. user: settings.apis.v1.user,
  118. password: settings.apis.v1.pass,
  119. },
  120. signal: AbortSignal.timeout(settings.apis.v1.timeout),
  121. })
  122. } catch (err) {
  123. if (err instanceof RequestFailedError && err.response.status === 404) {
  124. throw new Errors.NotFoundError()
  125. } else {
  126. throw err
  127. }
  128. }
  129. },
  130. }
  131. export default {
  132. promises: TemplatesManager,
  133. createProjectFromV1Template: util.callbackify(
  134. TemplatesManager.createProjectFromV1Template
  135. ),
  136. fetchFromV1: util.callbackify(TemplatesManager.fetchFromV1),
  137. }