05895dc0dc
[web] Stop logging handled errors GitOrigin-RevId: 7a60a576032a0dd389ef200e22c860b2ea9e8ed8
304 lines
8.6 KiB
JavaScript
304 lines
8.6 KiB
JavaScript
import logger from '@overleaf/logger'
|
|
import metrics from '@overleaf/metrics'
|
|
import fs from 'node:fs'
|
|
import fsPromises from 'node:fs/promises'
|
|
import Path from 'node:path'
|
|
import FileSystemImportManager from './FileSystemImportManager.mjs'
|
|
import ProjectUploadManager from './ProjectUploadManager.mjs'
|
|
import SessionManager from '../Authentication/SessionManager.mjs'
|
|
import EditorController from '../Editor/EditorController.mjs'
|
|
import ProjectLocator from '../Project/ProjectLocator.mjs'
|
|
import Settings from '@overleaf/settings'
|
|
import { InvalidZipFileError } from './ArchiveErrors.mjs'
|
|
import multer from 'multer'
|
|
import lodash from 'lodash'
|
|
import { expressify } from '@overleaf/promise-utils'
|
|
import {
|
|
DuplicateNameError,
|
|
FileTooLargeError,
|
|
DocumentConversionError,
|
|
} from '../Errors/Errors.js'
|
|
import DocumentConversionManager from './DocumentConversionManager.mjs'
|
|
import ProjectOptionsHandler from '../Project/ProjectOptionsHandler.mjs'
|
|
import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
|
|
|
|
const defaultsDeep = lodash.defaultsDeep
|
|
|
|
const upload = multer(
|
|
defaultsDeep(
|
|
{
|
|
dest: Settings.path.uploadFolder,
|
|
limits: {
|
|
fileSize: Settings.maxUploadSize,
|
|
},
|
|
},
|
|
Settings.multerOptions
|
|
)
|
|
)
|
|
|
|
/**
|
|
* @param {any} req
|
|
* @param {any} res
|
|
* @param {any} next
|
|
*/
|
|
function uploadProject(req, res, next) {
|
|
const timer = new metrics.Timer('project-upload')
|
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
|
const { path } = req.file
|
|
const name = Path.basename(req.body.name, '.zip')
|
|
return ProjectUploadManager.createProjectFromZipArchive(
|
|
userId,
|
|
name,
|
|
path,
|
|
function (error, project) {
|
|
fs.unlink(path, function () {})
|
|
timer.done()
|
|
if (error != null) {
|
|
logger.error(
|
|
{ err: error, filePath: path, fileName: name },
|
|
'error uploading project'
|
|
)
|
|
if (error instanceof InvalidZipFileError) {
|
|
return res.status(422).json({
|
|
success: false,
|
|
error: req.i18n.translate(error.message),
|
|
})
|
|
} else {
|
|
return res.status(500).json({
|
|
success: false,
|
|
error: req.i18n.translate('upload_failed'),
|
|
})
|
|
}
|
|
} else {
|
|
return res.json({ success: true, project_id: project._id })
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* @param {any} req
|
|
* @param {any} res
|
|
* @param {any} next
|
|
*/
|
|
async function uploadFile(req, res, next) {
|
|
const timer = new metrics.Timer('file-upload')
|
|
const name = req.body.name
|
|
const { path } = req.file
|
|
const projectId = req.params.Project_id
|
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
|
let { folder_id: folderId } = req.query
|
|
if (name == null || name.length === 0 || name.length > 150) {
|
|
await fsPromises.unlink(path).catch(unlinkErr => {
|
|
logger.warn({ err: unlinkErr, path }, 'error unlinking uploaded file')
|
|
})
|
|
return res.status(422).json({
|
|
success: false,
|
|
error: 'invalid_filename',
|
|
})
|
|
}
|
|
|
|
try {
|
|
// preserve the directory structure from an uploaded folder
|
|
const { relativePath } = req.body
|
|
// NOTE: Uppy sends a "null" string for `relativePath` when the file is not nested in a folder
|
|
if (relativePath && relativePath !== 'null') {
|
|
const { path } = await ProjectLocator.promises.findElement({
|
|
project_id: projectId,
|
|
element_id: folderId,
|
|
type: 'folder',
|
|
})
|
|
const { lastFolder } = await EditorController.promises.mkdirp(
|
|
projectId,
|
|
Path.dirname(Path.join('/', path.fileSystem, relativePath)),
|
|
userId
|
|
)
|
|
folderId = lastFolder._id
|
|
}
|
|
} catch (error) {
|
|
await fsPromises.unlink(path).catch(unlinkErr => {
|
|
logger.warn({ err: unlinkErr, path }, 'error unlinking uploaded file')
|
|
})
|
|
throw error
|
|
}
|
|
|
|
return FileSystemImportManager.addEntity(
|
|
userId,
|
|
projectId,
|
|
folderId,
|
|
name,
|
|
path,
|
|
true,
|
|
function (error, entity) {
|
|
fs.unlink(path, function () {})
|
|
timer.done()
|
|
if (error != null) {
|
|
if (error.name === 'InvalidNameError') {
|
|
return res.status(422).json({
|
|
success: false,
|
|
error: 'invalid_filename',
|
|
})
|
|
} else if (error instanceof DuplicateNameError) {
|
|
return res.status(422).json({
|
|
success: false,
|
|
error: 'duplicate_file_name',
|
|
})
|
|
} else if (error.message === 'project_has_too_many_files') {
|
|
return res.status(422).json({
|
|
success: false,
|
|
error: 'project_has_too_many_files',
|
|
})
|
|
} else if (error.message === 'folder_not_found') {
|
|
return res.status(422).json({
|
|
success: false,
|
|
error: 'folder_not_found',
|
|
})
|
|
} else {
|
|
logger.error(
|
|
{
|
|
err: error,
|
|
projectId,
|
|
filePath: path,
|
|
fileName: name,
|
|
folderId,
|
|
},
|
|
'error uploading file'
|
|
)
|
|
return res.status(422).json({ success: false })
|
|
}
|
|
} else {
|
|
return res.json({
|
|
success: true,
|
|
entity_id: entity?._id,
|
|
entity_type: entity?.type,
|
|
hash: entity?.hash,
|
|
})
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* @param {any} req
|
|
* @param {any} res
|
|
* @param {any} next
|
|
*/
|
|
async function importDocument(req, res, next) {
|
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
|
const { path } = req.file
|
|
const conversionType = req.query.type
|
|
if (!['docx', 'markdown'].includes(conversionType)) {
|
|
return res.status(400).json({ success: false, error: 'invalid_type' })
|
|
}
|
|
const name = Path.basename(req.body.name, Path.extname(req.body.name))
|
|
logger.debug({ path, userId, conversionType }, 'importing document file')
|
|
try {
|
|
const archivePath =
|
|
await DocumentConversionManager.promises.convertDocumentToLaTeXZipArchive(
|
|
path,
|
|
userId,
|
|
conversionType
|
|
)
|
|
try {
|
|
const project =
|
|
await ProjectUploadManager.promises.createProjectFromZipArchive(
|
|
userId,
|
|
name,
|
|
archivePath
|
|
)
|
|
await ProjectOptionsHandler.promises.setCompiler(project._id, 'lualatex')
|
|
AnalyticsManager.recordEventForUserInBackground(
|
|
userId,
|
|
'convert-format',
|
|
{
|
|
sourceFormat: conversionType,
|
|
targetFormat: 'latex',
|
|
status: 'success',
|
|
operation: 'import',
|
|
}
|
|
)
|
|
res.json({ success: true, project_id: project._id })
|
|
} finally {
|
|
await fsPromises.unlink(archivePath).catch(unlinkErr => {
|
|
logger.warn(
|
|
{ err: unlinkErr, archivePath },
|
|
'error unlinking after docx conversion'
|
|
)
|
|
})
|
|
}
|
|
} catch (error) {
|
|
AnalyticsManager.recordEventForUserInBackground(userId, 'convert-format', {
|
|
sourceFormat: conversionType,
|
|
targetFormat: 'latex',
|
|
status: 'failure',
|
|
operation: 'import',
|
|
})
|
|
if (
|
|
error instanceof FileTooLargeError ||
|
|
error?.name === 'FileTooLargeError'
|
|
) {
|
|
return res.status(422).json({
|
|
success: false,
|
|
error: 'file_too_large',
|
|
})
|
|
}
|
|
if (error instanceof DocumentConversionError) {
|
|
return res.status(422).json({
|
|
success: false,
|
|
error: error.message || req.i18n.translate('upload_failed'),
|
|
})
|
|
}
|
|
logger.error({ error, userId }, 'unhandled error while importing document')
|
|
res.status(500).json({
|
|
success: false,
|
|
error: req.i18n.translate('upload_failed'),
|
|
})
|
|
} finally {
|
|
await fsPromises.unlink(path).catch(unlinkErr => {
|
|
logger.warn(
|
|
{ err: unlinkErr, path },
|
|
'error unlinking uploaded file in importDocx'
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {any} req
|
|
* @param {any} res
|
|
* @param {any} next
|
|
*/
|
|
function multerMiddleware(req, res, next) {
|
|
if (upload == null) {
|
|
return res
|
|
.status(500)
|
|
.json({ success: false, error: req.i18n.translate('upload_failed') })
|
|
}
|
|
return upload.single('qqfile')(
|
|
req,
|
|
res,
|
|
/** @param {any} err */ function (err) {
|
|
if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') {
|
|
return res
|
|
.status(422)
|
|
.json({ success: false, error: req.i18n.translate('file_too_large') })
|
|
}
|
|
if (err) return next(err)
|
|
if (!req.file?.path) {
|
|
logger.info({ req }, 'missing req.file.path on upload')
|
|
return res
|
|
.status(400)
|
|
.json({ success: false, error: 'invalid_upload_request' })
|
|
}
|
|
next()
|
|
}
|
|
)
|
|
}
|
|
|
|
export default {
|
|
uploadProject,
|
|
uploadFile: expressify(uploadFile),
|
|
multerMiddleware,
|
|
importDocument: expressify(importDocument),
|
|
}
|