ConversionManager.js 4.1 KB

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