DocumentConversionManager.mjs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import Settings from '@overleaf/settings'
  2. import CompileManager from '../Compile/CompileManager.mjs'
  3. import ClsiManager from '../Compile/ClsiManager.mjs'
  4. import fs from 'node:fs'
  5. import fsPromises from 'node:fs/promises'
  6. import logger from '@overleaf/logger'
  7. import Path from 'node:path'
  8. import { fetchStreamWithResponse } from '@overleaf/fetch-utils'
  9. import { pipeline } from 'node:stream/promises'
  10. import OError from '@overleaf/o-error'
  11. import FormData from 'form-data'
  12. import { FileTooLargeError } from '../Errors/Errors.js'
  13. async function convertDocumentToLaTeXZipArchive(path, userId, conversionType) {
  14. const clsiUrl = new URL(Settings.apis.clsi.url)
  15. const limits = await CompileManager.promises._getUserCompileLimits(userId)
  16. // Uncomment this and remove the line below when the deploy is done.
  17. // clsiUrl.pathname = '/convert/document-to-latex'
  18. clsiUrl.pathname =
  19. conversionType === 'docx'
  20. ? '/convert/docx-to-latex'
  21. : '/convert/document-to-latex'
  22. clsiUrl.searchParams.set('compileBackendClass', limits.compileBackendClass)
  23. clsiUrl.searchParams.set('compileGroup', limits.compileGroup)
  24. clsiUrl.searchParams.set('type', conversionType)
  25. const formData = new FormData()
  26. formData.append('qqfile', fs.createReadStream(path))
  27. logger.debug(
  28. { clsiUrl: clsiUrl.toString(), conversionType },
  29. 'sending document to CLSI for conversion'
  30. )
  31. const outputFileName = crypto.randomUUID() + '_document-conversion' + '.zip'
  32. const outputPath = Path.join(Settings.path.dumpFolder, outputFileName)
  33. let outputStream
  34. const abortController = new AbortController()
  35. try {
  36. const { stream, response } = await fetchStreamWithResponse(clsiUrl, {
  37. method: 'POST',
  38. body: formData,
  39. signal: abortController.signal,
  40. })
  41. const contentLength = parseInt(response.headers.get('Content-Length'), 10)
  42. if (contentLength > Settings.maxUploadSize) {
  43. abortController.abort()
  44. stream.destroy()
  45. throw new FileTooLargeError({
  46. message: 'converted document archive too large',
  47. info: {
  48. size: contentLength,
  49. },
  50. })
  51. }
  52. outputStream = fs.createWriteStream(outputPath)
  53. await pipeline(stream, outputStream)
  54. logger.debug({ outputPath }, 'received converted file from CLSI')
  55. } catch (error) {
  56. logger.error({ err: error }, 'error during document conversion')
  57. outputStream?.destroy()
  58. // Make sure to clean up the output file if conversion didn't work
  59. await fsPromises.unlink(outputPath).catch(() => {})
  60. if (error instanceof FileTooLargeError) {
  61. throw error
  62. }
  63. throw new OError('document conversion failed').withCause(error)
  64. }
  65. return outputPath
  66. }
  67. async function convertProjectToDocument(projectId, userId, type) {
  68. const limits = await CompileManager.promises._getUserCompileLimits(userId)
  69. const clsiRequest =
  70. await ClsiManager.promises.buildDocumentConversionRequest(projectId)
  71. const clsiUrl = new URL(Settings.apis.clsi.url)
  72. clsiUrl.pathname = `/project/${projectId}/user/${userId}/download/project-to-document`
  73. clsiUrl.searchParams.set('type', type)
  74. clsiUrl.searchParams.set('compileBackendClass', limits.compileBackendClass)
  75. clsiUrl.searchParams.set('compileGroup', limits.compileGroup)
  76. logger.debug(
  77. { clsiUrl: clsiUrl.toString(), projectId, userId, type },
  78. 'sending project to CLSI for document conversion'
  79. )
  80. const { stream, response } = await fetchStreamWithResponse(clsiUrl, {
  81. method: 'POST',
  82. json: clsiRequest,
  83. })
  84. const contentLength = parseInt(response.headers.get('Content-Length'), 10)
  85. return { stream, contentLength }
  86. }
  87. export default {
  88. promises: {
  89. convertDocumentToLaTeXZipArchive,
  90. convertProjectToDocument,
  91. },
  92. }