ProjectDownloadsController.mjs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import Metrics from '@overleaf/metrics'
  2. import ProjectGetter from '../Project/ProjectGetter.mjs'
  3. import ProjectZipStreamManager from './ProjectZipStreamManager.mjs'
  4. import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.mjs'
  5. import { prepareZipAttachment } from '../../infrastructure/Response.mjs'
  6. import SessionManager from '../Authentication/SessionManager.mjs'
  7. import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.mjs'
  8. import DocumentConversionManager from '../Uploads/DocumentConversionManager.mjs'
  9. import { expressify } from '@overleaf/promise-utils'
  10. import { pipeline } from 'node:stream/promises'
  11. const SUPPORTED_CONVERSION_TYPES = new Map([['docx', 'docx']])
  12. // Keep in sync with the logic for PDF files in CompileController
  13. function getSafeProjectName(project) {
  14. return project.name.replace(/[^\p{L}\p{Nd}]/gu, '_')
  15. }
  16. async function exportProjectConversion(req, res) {
  17. const type = req.params.type
  18. const extension = SUPPORTED_CONVERSION_TYPES.get(type)
  19. if (!extension) {
  20. return res.sendStatus(400)
  21. }
  22. const userId = SessionManager.getLoggedInUserId(req.session)
  23. const projectId = req.params.Project_id
  24. Metrics.inc('document-exports', 1, { type })
  25. const project = await ProjectGetter.promises.getProject(projectId, {
  26. name: true,
  27. })
  28. const { stream, contentLength } =
  29. await DocumentConversionManager.promises.convertProjectToDocument(
  30. projectId,
  31. userId,
  32. type
  33. )
  34. const safeFileName = getSafeProjectName(project)
  35. res.setHeader('Content-Length', contentLength)
  36. res.attachment(`${safeFileName}.${extension}`)
  37. res.setHeader('X-Content-Type-Options', 'nosniff')
  38. res.setHeader('X-Accel-Buffering', 'no')
  39. ProjectAuditLogHandler.addEntryInBackground(
  40. projectId,
  41. `project-exported-${type}`,
  42. userId,
  43. req.ip
  44. )
  45. await pipeline(stream, res)
  46. }
  47. export default {
  48. exportProjectConversion: expressify(exportProjectConversion),
  49. downloadProject(req, res, next) {
  50. const userId = SessionManager.getLoggedInUserId(req.session)
  51. const projectId = req.params.Project_id
  52. Metrics.inc('zip-downloads')
  53. DocumentUpdaterHandler.flushProjectToMongo(projectId, function (error) {
  54. if (error) {
  55. return next(error)
  56. }
  57. ProjectGetter.getProject(
  58. projectId,
  59. { name: true },
  60. function (error, project) {
  61. if (error) {
  62. return next(error)
  63. }
  64. ProjectAuditLogHandler.addEntryInBackground(
  65. projectId,
  66. 'project-downloaded',
  67. userId,
  68. req.ip
  69. )
  70. ProjectZipStreamManager.createZipStreamForProject(
  71. projectId,
  72. function (error, stream) {
  73. if (error) {
  74. return next(error)
  75. }
  76. prepareZipAttachment(res, `${getSafeProjectName(project)}.zip`)
  77. stream.pipe(res)
  78. }
  79. )
  80. }
  81. )
  82. })
  83. },
  84. downloadMultipleProjects(req, res, next) {
  85. const userId = SessionManager.getLoggedInUserId(req.session)
  86. const projectIds = req.query.project_ids.split(',')
  87. Metrics.inc('zip-downloads-multiple')
  88. DocumentUpdaterHandler.flushMultipleProjectsToMongo(
  89. projectIds,
  90. function (error) {
  91. if (error) {
  92. return next(error)
  93. }
  94. // Log audit entry for each project in the batch
  95. for (const projectId of projectIds) {
  96. ProjectAuditLogHandler.addEntryInBackground(
  97. projectId,
  98. 'project-downloaded',
  99. userId,
  100. req.ip
  101. )
  102. }
  103. ProjectZipStreamManager.createZipStreamForMultipleProjects(
  104. projectIds,
  105. function (error, stream) {
  106. if (error) {
  107. return next(error)
  108. }
  109. prepareZipAttachment(
  110. res,
  111. `Overleaf Projects (${projectIds.length} items).zip`
  112. )
  113. stream.pipe(res)
  114. }
  115. )
  116. }
  117. )
  118. },
  119. }