Files
overleaf-cep/services/web/app/src/Features/LinkedFiles/LinkedFilesController.mjs
Andrew Rumble 07c827e9fd Merge pull request #29928 from overleaf/ar-last-infrastructure-conversions
[web] last infrastructure conversions

GitOrigin-RevId: ad1aff9b7df0610ed0303157d9e2c8032f32c02b
2025-11-28 09:05:56 +00:00

278 lines
8.2 KiB
JavaScript

/* eslint-disable
max-len,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import SessionManager from '../Authentication/SessionManager.mjs'
import Settings from '@overleaf/settings'
import _ from 'lodash'
import AnalyticsManager from '../../../../app/src/Features/Analytics/AnalyticsManager.mjs'
import LinkedFilesHandler from './LinkedFilesHandler.mjs'
import LinkedFilesErrors from './LinkedFilesErrors.mjs'
import {
OutputFileFetchFailedError,
FileTooLargeError,
} from '../Errors/Errors.js'
import Modules from '../../infrastructure/Modules.mjs'
import { plainTextResponse } from '../../infrastructure/Response.mjs'
import { z, zz, validateReq } from '../../infrastructure/Validation.mjs'
import EditorRealTimeController from '../Editor/EditorRealTimeController.mjs'
import { expressify } from '@overleaf/promise-utils'
import ProjectOutputFileAgent from './ProjectOutputFileAgent.mjs'
import ProjectFileAgent from './ProjectFileAgent.mjs'
import UrlAgent from './UrlAgent.mjs'
const {
CompileFailedError,
UrlFetchFailedError,
InvalidUrlError,
AccessDeniedError,
BadEntityTypeError,
BadDataError,
ProjectNotFoundError,
V1ProjectNotFoundError,
SourceFileNotFoundError,
NotOriginalImporterError,
FeatureNotAvailableError,
RemoteServiceError,
FileCannotRefreshError,
} = LinkedFilesErrors
let LinkedFilesController
const createLinkedFileSchema = z.object({
params: z.object({
project_id: zz.objectId(),
}),
body: z.object({
name: z.string(),
provider: z.string(),
data: z.object({}).passthrough(),
parent_folder_id: zz.objectId(),
}),
})
async function createLinkedFile(req, res, next) {
const { params, body } = validateReq(req, createLinkedFileSchema)
const { project_id: projectId } = params
const { name, provider, data, parent_folder_id: parentFolderId } = body
const userId = SessionManager.getLoggedInUserId(req.session)
const Agent = await LinkedFilesController._getAgent(provider)
if (Agent == null) {
return res.sendStatus(400)
}
data.provider = provider
data.importedAt = new Date().toISOString()
try {
const newFileId = await Agent.promises.createLinkedFile(
projectId,
data,
name,
parentFolderId,
userId
)
if (name.endsWith('.bib')) {
AnalyticsManager.recordEventForUserInBackground(
userId,
'linked-bib-file',
{
integration: provider,
}
)
}
return res.json({ new_file_id: newFileId })
} catch (err) {
return LinkedFilesController.handleError(err, req, res, next)
}
}
async function refreshLinkedFile(req, res, next) {
const { project_id: projectId, file_id: fileId } = req.params
const { clientId } = req.body
const userId = SessionManager.getLoggedInUserId(req.session)
const { file, parentFolder } = await LinkedFilesHandler.promises.getFileById(
projectId,
fileId
)
if (file == null) {
return res.sendStatus(404)
}
const { name } = file
const { linkedFileData } = file
if (
linkedFileData == null ||
(linkedFileData != null ? linkedFileData.provider : undefined) == null
) {
return res.sendStatus(409)
}
const { provider } = linkedFileData
const parentFolderId = parentFolder._id
const Agent = await LinkedFilesController._getAgent(provider)
if (Agent == null) {
return res.sendStatus(400)
}
linkedFileData.importedAt = new Date().toISOString()
let newFileId
try {
newFileId = await Agent.promises.refreshLinkedFile(
projectId,
linkedFileData,
name,
parentFolderId,
userId
)
} catch (err) {
return LinkedFilesController.handleError(err, req, res, next)
}
if (req.body.shouldReindexReferences) {
// Signal to clients that they should re-index references
EditorRealTimeController.emitToRoom(
projectId,
'references:keys:updated',
[],
true,
clientId
)
}
res.json({ new_file_id: newFileId })
}
export default LinkedFilesController = {
Agents: null,
async _cacheAgents() {
if (!LinkedFilesController.Agents) {
LinkedFilesController.Agents = _.extend(
{
url: UrlAgent,
project_file: ProjectFileAgent,
project_output_file: ProjectOutputFileAgent,
},
await Modules.linkedFileAgentsIncludes()
)
}
},
async _getAgent(provider) {
await LinkedFilesController._cacheAgents()
if (
!Object.prototype.hasOwnProperty.call(
LinkedFilesController.Agents,
provider
)
) {
return null
}
if (!Array.from(Settings.enabledLinkedFileTypes).includes(provider)) {
return null
}
return LinkedFilesController.Agents[provider]
},
createLinkedFile: expressify(createLinkedFile),
refreshLinkedFile: expressify(refreshLinkedFile),
handleError(error, req, res, next) {
if (error instanceof AccessDeniedError) {
res.status(403)
plainTextResponse(
res,
res.locals.translate(
'the_project_that_contains_this_file_is_not_shared_with_you'
)
)
} else if (error instanceof BadDataError) {
res.status(400)
plainTextResponse(res, 'The submitted data is not valid')
} else if (error instanceof BadEntityTypeError) {
res.status(400)
plainTextResponse(res, 'The file is the wrong type')
} else if (error instanceof SourceFileNotFoundError) {
res.status(404)
plainTextResponse(res, 'Source file not found')
} else if (error instanceof ProjectNotFoundError) {
res.status(404)
plainTextResponse(res, 'Project not found')
} else if (error instanceof V1ProjectNotFoundError) {
res.status(409)
plainTextResponse(
res,
'Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file'
)
} else if (error instanceof CompileFailedError) {
res.status(422)
plainTextResponse(
res,
res.locals.translate('generic_linked_file_compile_error')
)
} else if (error instanceof OutputFileFetchFailedError) {
res.status(404)
plainTextResponse(res, 'Could not get output file')
} else if (error instanceof UrlFetchFailedError) {
res.status(422)
if (error.cause instanceof FileTooLargeError) {
plainTextResponse(res, 'File too large')
} else {
plainTextResponse(
res,
`Your URL could not be reached (${
error.info?.status || error.cause?.info?.status
} status code). Please check it and try again.`
)
}
} else if (error instanceof InvalidUrlError) {
res.status(422)
plainTextResponse(
res,
'Your URL is not valid. Please check it and try again.'
)
} else if (error instanceof NotOriginalImporterError) {
res.status(400)
plainTextResponse(
res,
'You are not the user who originally imported this file'
)
} else if (error instanceof FeatureNotAvailableError) {
res.status(400)
plainTextResponse(res, 'This feature is not enabled on your account')
} else if (error instanceof RemoteServiceError) {
if (error.info?.statusCode === 403) {
res.status(400).json({ relink: true })
} else {
res.status(502)
plainTextResponse(res, 'The remote service produced an error')
}
} else if (error instanceof FileCannotRefreshError) {
res.status(400)
plainTextResponse(res, 'This file cannot be refreshed')
} else if (error.message === 'project_has_too_many_files') {
res.status(400)
plainTextResponse(res, 'too many files')
} else if (/\bECONNREFUSED\b/.test(error.message)) {
res.status(500)
plainTextResponse(res, 'Importing references is not currently available')
} else if (error instanceof FileTooLargeError) {
res.status(422)
plainTextResponse(res, 'File too large')
} else {
next(error)
}
},
}