ConversionController.js 3.2 KB

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