ConversionManager.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import logger from '@overleaf/logger'
  2. import Settings from '@overleaf/settings'
  3. import fs from 'node:fs/promises'
  4. import Path from 'node:path'
  5. import CommandRunner from './CommandRunner.js'
  6. import LockManager from './LockManager.js'
  7. import OError from '@overleaf/o-error'
  8. const CONVERSION_CONFIGS = {
  9. docx: {
  10. inputFilename: 'input.docx',
  11. pandocArgs: ['--extract-media=.', '--from', 'docx+citations', '--citeproc'],
  12. },
  13. markdown: {
  14. inputFilename: 'input.md',
  15. pandocArgs: ['--from', 'markdown'],
  16. },
  17. }
  18. async function convertToLaTeXWithLock(conversionId, inputPath, conversionType) {
  19. const conversionDir = Path.join(Settings.path.compilesDir, conversionId)
  20. const lock = LockManager.acquire(conversionDir)
  21. try {
  22. return await convertToLaTeX(
  23. conversionId,
  24. conversionDir,
  25. inputPath,
  26. conversionType
  27. )
  28. } finally {
  29. lock.release()
  30. }
  31. }
  32. async function convertToLaTeX(
  33. conversionId,
  34. conversionDir,
  35. inputPath,
  36. conversionType
  37. ) {
  38. const config = CONVERSION_CONFIGS[conversionType]
  39. if (!config) {
  40. throw new OError('unsupported conversion type', { conversionType })
  41. }
  42. await fs.mkdir(conversionDir, { recursive: true })
  43. const newSourcePath = Path.join(conversionDir, config.inputFilename)
  44. await fs.copyFile(inputPath, newSourcePath)
  45. const outputName = crypto.randomUUID() + '.zip'
  46. try {
  47. const {
  48. stdout: stdoutPandoc,
  49. stderr: stderrPandoc,
  50. exitCode: exitCodePandoc,
  51. } = await CommandRunner.promises.run(
  52. conversionId,
  53. [
  54. 'pandoc',
  55. config.inputFilename,
  56. '--output',
  57. 'main.tex',
  58. '--to',
  59. 'latex',
  60. '--standalone',
  61. ...config.pandocArgs,
  62. ],
  63. conversionDir,
  64. Settings.pandocImage,
  65. Settings.conversionTimeoutSeconds * 1000,
  66. {},
  67. 'conversions'
  68. )
  69. if (exitCodePandoc !== 0) {
  70. throw new OError('Non-zero exit code from pandoc', {
  71. exitCode: exitCodePandoc,
  72. stderr: stderrPandoc,
  73. })
  74. }
  75. logger.debug(
  76. { stdout: stdoutPandoc, stderr: stderrPandoc, exitCode: exitCodePandoc },
  77. 'conversion command completed'
  78. )
  79. // Clean up the source document to leave only the conversion result
  80. await fs.unlink(newSourcePath).catch(() => {})
  81. const {
  82. stdout: stdoutZip,
  83. stderr: stderrZip,
  84. exitCode: exitCodeZip,
  85. } = await CommandRunner.promises.run(
  86. conversionId,
  87. ['zip', '-r', outputName, '.'],
  88. conversionDir,
  89. Settings.pandocImage,
  90. Settings.conversionTimeoutSeconds * 1000,
  91. {},
  92. 'conversions'
  93. )
  94. if (exitCodeZip !== 0) {
  95. throw new OError('Non-zero exit code from pandoc', {
  96. exitCode: exitCodeZip,
  97. stderr: stderrZip,
  98. })
  99. }
  100. logger.debug(
  101. { stdout: stdoutZip, stderr: stderrZip, exitCode: exitCodeZip },
  102. 'conversion output compressed'
  103. )
  104. } catch (error) {
  105. // Clean up the conversion directory on error to avoid leaving failed conversions around
  106. await fs.rm(conversionDir, { force: true, recursive: true }).catch(() => {})
  107. throw new OError('pandoc conversion failed').withCause(error)
  108. }
  109. return Path.join(conversionDir, outputName)
  110. }
  111. async function convertLaTeXToDocumentInDirWithLock(
  112. conversionId,
  113. compileDir,
  114. rootDocPath,
  115. type,
  116. extension
  117. ) {
  118. const lock = LockManager.acquire(compileDir)
  119. try {
  120. return await convertLaTeXToDocumentInDir(
  121. conversionId,
  122. compileDir,
  123. rootDocPath,
  124. type,
  125. extension
  126. )
  127. } finally {
  128. lock.release()
  129. }
  130. }
  131. async function convertLaTeXToDocumentInDir(
  132. conversionId,
  133. compileDir,
  134. rootDocPath = 'main.tex',
  135. type,
  136. extension
  137. ) {
  138. const outputName = crypto.randomUUID() + '.' + extension
  139. const timeoutMs = Settings.conversionTimeoutSeconds * 1000
  140. logger.debug(
  141. { compileDir, rootDocPath, type },
  142. 'running pandoc latex-to-document in compile dir'
  143. )
  144. const { exitCode, stdout, stderr } = await CommandRunner.promises.run(
  145. conversionId,
  146. [
  147. 'pandoc',
  148. rootDocPath,
  149. '--output',
  150. outputName,
  151. '--from',
  152. 'latex',
  153. '--to',
  154. type,
  155. '--resource-path=.',
  156. ],
  157. compileDir,
  158. Settings.pandocImage,
  159. timeoutMs,
  160. {},
  161. 'conversions'
  162. )
  163. if (exitCode !== 0) {
  164. throw new OError('pandoc latex-to-document conversion failed', {
  165. type,
  166. exitCode,
  167. stdout,
  168. stderr,
  169. })
  170. }
  171. return Path.join(compileDir, outputName)
  172. }
  173. export default {
  174. promises: {
  175. convertToLaTeXWithLock,
  176. convertLaTeXToDocumentInDirWithLock,
  177. },
  178. }