ProjectCreationHandler.mjs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import OError from '@overleaf/o-error'
  2. import metrics from '@overleaf/metrics'
  3. import Settings from '@overleaf/settings'
  4. import mongodb from 'mongodb-legacy'
  5. import Features from '../../infrastructure/Features.js'
  6. import { Project } from '../../models/Project.mjs'
  7. import { Folder } from '../../models/Folder.mjs'
  8. import ProjectEntityUpdateHandler from './ProjectEntityUpdateHandler.mjs'
  9. import ProjectDetailsHandler from './ProjectDetailsHandler.mjs'
  10. import HistoryManager from '../History/HistoryManager.mjs'
  11. import { User } from '../../models/User.mjs'
  12. import fs from 'node:fs'
  13. import path from 'node:path'
  14. import { callbackify } from 'node:util'
  15. import _ from 'lodash'
  16. import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
  17. import TpdsUpdateSender from '../ThirdPartyDataStore/TpdsUpdateSender.mjs'
  18. import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
  19. const { ObjectId } = mongodb
  20. const MONTH_NAMES = [
  21. 'January',
  22. 'February',
  23. 'March',
  24. 'April',
  25. 'May',
  26. 'June',
  27. 'July',
  28. 'August',
  29. 'September',
  30. 'October',
  31. 'November',
  32. 'December',
  33. ]
  34. const templateProjectDir = Features.hasFeature('saas')
  35. ? 'example-project'
  36. : 'example-project-sp'
  37. async function createBlankProject(
  38. ownerId,
  39. projectName,
  40. attributes = {},
  41. options
  42. ) {
  43. const isImport = attributes && attributes.overleaf
  44. const project = await _createBlankProject(
  45. ownerId,
  46. projectName,
  47. attributes,
  48. options
  49. )
  50. const segmentation = _.pick(attributes, [
  51. 'fromV1TemplateId',
  52. 'fromV1TemplateVersionId',
  53. ])
  54. Object.assign(segmentation, attributes.segmentation)
  55. segmentation.projectId = project._id
  56. if (isImport) {
  57. AnalyticsManager.recordEventForUserInBackground(
  58. ownerId,
  59. 'project-imported',
  60. segmentation
  61. )
  62. } else {
  63. AnalyticsManager.recordEventForUserInBackground(
  64. ownerId,
  65. 'project-created',
  66. segmentation
  67. )
  68. }
  69. return project
  70. }
  71. async function createProjectFromSnippet(ownerId, projectName, docLines) {
  72. const project = await _createBlankProject(ownerId, projectName)
  73. AnalyticsManager.recordEventForUserInBackground(ownerId, 'project-created', {
  74. projectId: project._id,
  75. })
  76. await _createRootDoc(project, ownerId, docLines)
  77. return project
  78. }
  79. async function createBasicProject(ownerId, projectName) {
  80. const project = await _createBlankProject(ownerId, projectName)
  81. const docLines = await _buildTemplate('mainbasic.tex', ownerId, projectName)
  82. await _createRootDoc(project, ownerId, docLines)
  83. AnalyticsManager.recordEventForUserInBackground(ownerId, 'project-created', {
  84. projectId: project._id,
  85. })
  86. return project
  87. }
  88. async function createExampleProject(ownerId, projectName) {
  89. const project = await _createBlankProject(ownerId, projectName)
  90. await _addExampleProjectFiles(ownerId, projectName, project)
  91. AnalyticsManager.recordEventForUserInBackground(ownerId, 'project-created', {
  92. projectId: project._id,
  93. })
  94. return project
  95. }
  96. async function _addExampleProjectFiles(ownerId, projectName, project) {
  97. const mainDocLines = await _buildTemplate(
  98. `${templateProjectDir}/main.tex`,
  99. ownerId,
  100. projectName
  101. )
  102. await _createRootDoc(project, ownerId, mainDocLines)
  103. const bibDocLines = await _buildTemplate(
  104. `${templateProjectDir}/sample.bib`,
  105. ownerId,
  106. projectName
  107. )
  108. await ProjectEntityUpdateHandler.promises.addDoc(
  109. project._id,
  110. project.rootFolder[0]._id,
  111. 'sample.bib',
  112. bibDocLines,
  113. ownerId,
  114. null
  115. )
  116. const frogPath = path.join(
  117. import.meta.dirname,
  118. `/../../../templates/project_files/${templateProjectDir}/frog.jpg`
  119. )
  120. await ProjectEntityUpdateHandler.promises.addFile(
  121. project._id,
  122. project.rootFolder[0]._id,
  123. 'frog.jpg',
  124. frogPath,
  125. null,
  126. ownerId,
  127. null
  128. )
  129. }
  130. async function _createBlankProject(
  131. ownerId,
  132. projectName,
  133. attributes = {},
  134. { skipCreatingInTPDS = false } = {}
  135. ) {
  136. metrics.inc('project-creation')
  137. const timer = new metrics.Timer('project-creation')
  138. await ProjectDetailsHandler.promises.validateProjectName(projectName)
  139. const rootFolder = new Folder({ name: 'rootFolder' })
  140. attributes.lastUpdatedBy = attributes.owner_ref = new ObjectId(ownerId)
  141. attributes.name = projectName
  142. const project = new Project(attributes)
  143. // Initialise the history unless the caller has overridden it in the attributes
  144. // (to allow scripted creation of projects without full project history)
  145. if (project.overleaf.history.id == null && !attributes.overleaf) {
  146. const historyId = await HistoryManager.promises.initializeProject(
  147. project._id
  148. )
  149. if (historyId != null) {
  150. project.overleaf.history.id = historyId
  151. }
  152. }
  153. // All the projects are initialised with Full Project History. This property
  154. // is still set for backwards compatibility: Server Pro requires all projects
  155. // have it set to `true` since SP 4.0
  156. project.overleaf.history.display = true
  157. if (Settings.currentImageName) {
  158. // avoid clobbering any imageName already set in attributes (e.g. importedImageName)
  159. if (!project.imageName) {
  160. project.imageName = Settings.currentImageName
  161. }
  162. }
  163. project.rootFolder[0] = rootFolder
  164. const user = await User.findById(ownerId, {
  165. 'ace.spellCheckLanguage': 1,
  166. _id: 1,
  167. })
  168. project.spellCheckLanguage = user.ace.spellCheckLanguage
  169. const historyRangesSupportAssignment =
  170. await SplitTestHandler.promises.getAssignmentForUser(
  171. user._id,
  172. 'history-ranges-support'
  173. )
  174. if (historyRangesSupportAssignment.variant === 'enabled') {
  175. project.overleaf.history.rangesSupportEnabled = true
  176. }
  177. await project.save()
  178. if (!skipCreatingInTPDS) {
  179. await TpdsUpdateSender.promises.createProject({
  180. projectId: project._id,
  181. projectName,
  182. ownerId,
  183. userId: ownerId,
  184. })
  185. }
  186. timer.done()
  187. return project
  188. }
  189. async function _createRootDoc(project, ownerId, docLines) {
  190. try {
  191. const { doc } = await ProjectEntityUpdateHandler.promises.addDoc(
  192. project._id,
  193. project.rootFolder[0]._id,
  194. 'main.tex',
  195. docLines,
  196. ownerId,
  197. null
  198. )
  199. await ProjectEntityUpdateHandler.promises.setRootDoc(project._id, doc._id)
  200. } catch (error) {
  201. throw OError.tag(error, 'error adding root doc when creating project')
  202. }
  203. }
  204. async function _buildTemplate(templateName, userId, projectName) {
  205. const user = await User.findById(userId, 'first_name last_name')
  206. const templatePath = path.join(
  207. import.meta.dirname,
  208. `/../../../templates/project_files/${templateName}`
  209. )
  210. const template = fs.readFileSync(templatePath)
  211. const data = {
  212. project_name: projectName,
  213. user,
  214. year: new Date().getUTCFullYear(),
  215. month: MONTH_NAMES[new Date().getUTCMonth()],
  216. }
  217. const output = _.template(template.toString())(data)
  218. return output.split('\n')
  219. }
  220. export default {
  221. createBlankProject: callbackify(createBlankProject),
  222. createProjectFromSnippet: callbackify(createProjectFromSnippet),
  223. createBasicProject: callbackify(createBasicProject),
  224. createExampleProject: callbackify(createExampleProject),
  225. promises: {
  226. createBlankProject,
  227. createProjectFromSnippet,
  228. createBasicProject,
  229. createExampleProject,
  230. },
  231. }