be5a7b56c8
* using CLSI logic for fetching the project contents and skip the .zip export * Use unique conversion directory for project-to-docx export to avoid corrupting the shared compile directory when a compile runs concurrently * Remove X-Accel-Buffering header — not needed as CLSI does not run behind nginx * moving log before sending the data * Return CLSI stream directly instead of buffering to disk on web Previously convertProjectToDocx wrote the CLSI response to a temp file on disk, then the controller read it back to stream to the client. Now the stream is returned directly and piped to the response, avoiding unnecessary disk I/O on the web server. * Use href redirect for docx export instead of fetching blob into memory * making functions and files more generic so they can be used in future for other documents exports as well * adding export-docx split test * adding unit tests * adding cypress E2E test * format:fix * renaming the route to download from convert * adding new icon for export docx button * format:fix * remove unused showExportDocumentErrorToast export and adding guard against invalid Content-Length header from CLSI * format:fix * refactor(clsi): move promisify(parse) into RequestParser * refactor: generic conversion endpoint with type as route param * refactor: use type→extension map for validated conversion types * refactor(clsi): remove --standalone flag and fix rejection test * fixing the href in cypress test * renaming function * adding type to Metrics.inc * fix: rename exportProjectDocument, add WithLock wrapper and metrics type label * format:fix * fix: hide docx export from anonymous users and add WithLock wrapper * format fix * remove redundant Content-Length validation from DocumentConversionManager * format:fix * removing trailing icon GitOrigin-RevId: e9764fefac2c4b625d23be9e942ea4a8b283c70d
129 lines
3.9 KiB
JavaScript
129 lines
3.9 KiB
JavaScript
import Metrics from '@overleaf/metrics'
|
|
import ProjectGetter from '../Project/ProjectGetter.mjs'
|
|
import ProjectZipStreamManager from './ProjectZipStreamManager.mjs'
|
|
import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.mjs'
|
|
import { prepareZipAttachment } from '../../infrastructure/Response.mjs'
|
|
import SessionManager from '../Authentication/SessionManager.mjs'
|
|
import ProjectAuditLogHandler from '../Project/ProjectAuditLogHandler.mjs'
|
|
import DocumentConversionManager from '../Uploads/DocumentConversionManager.mjs'
|
|
import { expressify } from '@overleaf/promise-utils'
|
|
import { pipeline } from 'node:stream/promises'
|
|
|
|
const SUPPORTED_CONVERSION_TYPES = new Map([['docx', 'docx']])
|
|
|
|
// Keep in sync with the logic for PDF files in CompileController
|
|
function getSafeProjectName(project) {
|
|
return project.name.replace(/[^\p{L}\p{Nd}]/gu, '_')
|
|
}
|
|
|
|
async function exportProjectConversion(req, res) {
|
|
const type = req.params.type
|
|
const extension = SUPPORTED_CONVERSION_TYPES.get(type)
|
|
if (!extension) {
|
|
return res.sendStatus(400)
|
|
}
|
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
|
const projectId = req.params.Project_id
|
|
Metrics.inc('document-exports', 1, { type })
|
|
|
|
const project = await ProjectGetter.promises.getProject(projectId, {
|
|
name: true,
|
|
})
|
|
|
|
const { stream, contentLength } =
|
|
await DocumentConversionManager.promises.convertProjectToDocument(
|
|
projectId,
|
|
userId,
|
|
type
|
|
)
|
|
|
|
const safeFileName = getSafeProjectName(project)
|
|
res.setHeader('Content-Length', contentLength)
|
|
res.attachment(`${safeFileName}.${extension}`)
|
|
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
res.setHeader('X-Accel-Buffering', 'no')
|
|
ProjectAuditLogHandler.addEntryInBackground(
|
|
projectId,
|
|
`project-exported-${type}`,
|
|
userId,
|
|
req.ip
|
|
)
|
|
await pipeline(stream, res)
|
|
}
|
|
|
|
export default {
|
|
exportProjectConversion: expressify(exportProjectConversion),
|
|
|
|
downloadProject(req, res, next) {
|
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
|
const projectId = req.params.Project_id
|
|
Metrics.inc('zip-downloads')
|
|
DocumentUpdaterHandler.flushProjectToMongo(projectId, function (error) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
ProjectGetter.getProject(
|
|
projectId,
|
|
{ name: true },
|
|
function (error, project) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
ProjectAuditLogHandler.addEntryInBackground(
|
|
projectId,
|
|
'project-downloaded',
|
|
userId,
|
|
req.ip
|
|
)
|
|
ProjectZipStreamManager.createZipStreamForProject(
|
|
projectId,
|
|
function (error, stream) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
prepareZipAttachment(res, `${getSafeProjectName(project)}.zip`)
|
|
stream.pipe(res)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
})
|
|
},
|
|
|
|
downloadMultipleProjects(req, res, next) {
|
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
|
const projectIds = req.query.project_ids.split(',')
|
|
Metrics.inc('zip-downloads-multiple')
|
|
DocumentUpdaterHandler.flushMultipleProjectsToMongo(
|
|
projectIds,
|
|
function (error) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
// Log audit entry for each project in the batch
|
|
for (const projectId of projectIds) {
|
|
ProjectAuditLogHandler.addEntryInBackground(
|
|
projectId,
|
|
'project-downloaded',
|
|
userId,
|
|
req.ip
|
|
)
|
|
}
|
|
ProjectZipStreamManager.createZipStreamForMultipleProjects(
|
|
projectIds,
|
|
function (error, stream) {
|
|
if (error) {
|
|
return next(error)
|
|
}
|
|
prepareZipAttachment(
|
|
res,
|
|
`Overleaf Projects (${projectIds.length} items).zip`
|
|
)
|
|
stream.pipe(res)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
},
|
|
}
|