ConversionController.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import crypto from 'node:crypto'
  2. import logger from '@overleaf/logger'
  3. import { expressify } from '@overleaf/promise-utils'
  4. import fs from 'node:fs/promises'
  5. import fsSync from 'node:fs'
  6. import ConversionManager from './ConversionManager.js'
  7. import ConversionOutputCleaner from './ConversionOutputCleaner.js'
  8. import OutputCacheManager from './OutputCacheManager.js'
  9. import ResourceWriter from './ResourceWriter.js'
  10. import RequestParser from './RequestParser.js'
  11. import { pipeline } from 'node:stream/promises'
  12. import Settings from '@overleaf/settings'
  13. import Path from 'node:path'
  14. const CONVERSION_CONFIGS = {
  15. docx: { extension: 'docx' },
  16. markdown: { extension: 'zip' },
  17. }
  18. async function convertDocumentToLaTeX(req, res) {
  19. const { path } = req.file
  20. const conversionType = req.query.type
  21. if (!Settings.enablePandocConversions) {
  22. await fs.unlink(path).catch(() => {})
  23. return res.sendStatus(404)
  24. }
  25. if (!conversionType || !['docx', 'markdown'].includes(conversionType)) {
  26. await fs.unlink(path).catch(() => {})
  27. return res.sendStatus(400)
  28. }
  29. logger.debug({ path, conversionType }, 'received file for conversion')
  30. const conversionId = crypto.randomUUID()
  31. let zipPath
  32. try {
  33. zipPath = await ConversionManager.promises.convertToLaTeXWithLock(
  34. conversionId,
  35. path,
  36. conversionType
  37. )
  38. } finally {
  39. await fs.unlink(path).catch(() => {})
  40. }
  41. try {
  42. const zipStat = await fs.stat(zipPath)
  43. res.setHeader('Content-Length', zipStat.size)
  44. res.attachment('conversion.zip')
  45. res.setHeader('X-Content-Type-Options', 'nosniff')
  46. const readStream = fsSync.createReadStream(zipPath)
  47. await pipeline(readStream, res)
  48. } finally {
  49. await fs
  50. .rm(Path.dirname(zipPath), { recursive: true, force: true })
  51. .catch(() => {})
  52. }
  53. }
  54. async function convertProjectToDocument(req, res) {
  55. if (!Settings.enablePandocConversions) {
  56. return res.sendStatus(404)
  57. }
  58. const type = req.query.type
  59. if (!Object.hasOwn(CONVERSION_CONFIGS, type)) {
  60. return res.sendStatus(400)
  61. }
  62. const config = CONVERSION_CONFIGS[type]
  63. const request = await RequestParser.promises.parse(req.body)
  64. request.project_id = req.params.project_id
  65. request.user_id = req.params.user_id
  66. request.metricsOpts = {}
  67. const responseFormat = req.query.responseFormat === 'json' ? 'json' : 'stream'
  68. const conversionId = crypto.randomUUID()
  69. const conversionDir = Path.join(Settings.path.compilesDir, conversionId)
  70. logger.debug(
  71. {
  72. projectId: request.project_id,
  73. userId: request.user_id,
  74. rootResourcePath: request.rootResourcePath,
  75. type,
  76. },
  77. 'syncing resources for project-to-document conversion'
  78. )
  79. try {
  80. await ResourceWriter.promises.syncResourcesToDisk(request, conversionDir)
  81. const documentPath =
  82. await ConversionManager.promises.convertLaTeXToDocumentInDirWithLock(
  83. conversionId,
  84. conversionDir,
  85. request.rootResourcePath,
  86. type
  87. )
  88. const outputName = `output.${config.extension}`
  89. if (responseFormat === 'json') {
  90. // TODO: drop the streaming branch once web is migrated to the two-step flow
  91. const buildId = await OutputCacheManager.promises.generateBuildId()
  92. const buildDir = Path.join(
  93. Settings.path.outputDir,
  94. conversionId,
  95. OutputCacheManager.CACHE_SUBDIR,
  96. buildId
  97. )
  98. try {
  99. await fs.mkdir(buildDir, { recursive: true })
  100. await fs.copyFile(documentPath, Path.join(buildDir, outputName))
  101. res.json({ conversionId, buildId, file: outputName })
  102. } finally {
  103. ConversionOutputCleaner.scheduleCleanup(conversionId)
  104. }
  105. } else {
  106. const documentStat = await fs.stat(documentPath)
  107. res.setHeader('Content-Length', documentStat.size)
  108. res.attachment(outputName)
  109. res.setHeader('X-Content-Type-Options', 'nosniff')
  110. const readStream = fsSync.createReadStream(documentPath)
  111. await pipeline(readStream, res)
  112. }
  113. } finally {
  114. await fs.rm(conversionDir, { recursive: true, force: true }).catch(() => {})
  115. }
  116. }
  117. export default {
  118. convertDocumentToLaTeX: expressify(convertDocumentToLaTeX),
  119. convertProjectToDocument: expressify(convertProjectToDocument),
  120. }