| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- import { expressify } from '@overleaf/promise-utils'
- import TpdsUpdateHandler from './TpdsUpdateHandler.mjs'
- import UpdateMerger from './UpdateMerger.js'
- import Errors from '../Errors/Errors.js'
- import logger from '@overleaf/logger'
- import Path from 'path'
- import metrics from '@overleaf/metrics'
- import NotificationsBuilder from '../Notifications/NotificationsBuilder.js'
- import SessionManager from '../Authentication/SessionManager.js'
- import ProjectCreationHandler from '../Project/ProjectCreationHandler.js'
- import ProjectDetailsHandler from '../Project/ProjectDetailsHandler.js'
- import HttpErrorHandler from '../Errors/HttpErrorHandler.js'
- import TpdsQueueManager from './TpdsQueueManager.mjs'
- async function createProject(req, res) {
- const { user_id: userId } = req.params
- let { projectName } = req.body
- projectName = await ProjectDetailsHandler.promises.generateUniqueName(
- userId,
- projectName
- )
- const project = await ProjectCreationHandler.promises.createBlankProject(
- userId,
- projectName,
- {},
- { skipCreatingInTPDS: true }
- )
- res.json({
- projectId: project._id.toString(),
- })
- }
- // mergeUpdate and deleteUpdate are used by Dropbox, where the project is only
- // passed as the name, as the first part of the file path. They have to check
- // the project exists, find it, and create it if not. They also ignore 'noisy'
- // files like .DS_Store, .gitignore, etc.
- async function mergeUpdate(req, res) {
- metrics.inc('tpds.merge-update')
- const { filePath, userId, projectId, projectName } = parseParams(req)
- const source = req.headers['x-update-source'] || 'unknown'
- let metadata
- try {
- metadata = await TpdsUpdateHandler.promises.newUpdate(
- userId,
- projectId,
- projectName,
- filePath,
- req,
- source
- )
- } catch (err) {
- if (err.name === 'TooManyRequestsError') {
- logger.warn(
- { err, userId, filePath },
- 'tpds update failed to be processed, too many requests'
- )
- return res.sendStatus(429)
- } else if (err.message === 'project_has_too_many_files') {
- logger.warn(
- { err, userId, filePath },
- 'tpds trying to append to project over file limit'
- )
- NotificationsBuilder.tpdsFileLimit(userId).create(projectName, projectId)
- return res.sendStatus(400)
- } else {
- throw err
- }
- }
- if (metadata == null) {
- return res.json({ status: 'rejected' })
- }
- const payload = {
- status: 'applied',
- projectId: metadata.projectId.toString(),
- entityId: metadata.entityId.toString(),
- entityType: metadata.entityType,
- folderId: metadata.folderId.toString(),
- }
- // When the update is a doc edit, the update is merged in docupdater and
- // doesn't generate a new rev.
- if (metadata.rev != null) {
- payload.rev = metadata.rev.toString()
- }
- res.json(payload)
- }
- async function deleteUpdate(req, res) {
- metrics.inc('tpds.delete-update')
- const { filePath, userId, projectId, projectName } = parseParams(req)
- const source = req.headers['x-update-source'] || 'unknown'
- await TpdsUpdateHandler.promises.deleteUpdate(
- userId,
- projectId,
- projectName,
- filePath,
- source
- )
- res.sendStatus(200)
- }
- /**
- * Update endpoint that accepts update details as JSON
- */
- async function updateFolder(req, res) {
- const userId = req.body.userId
- const projectId = req.body.projectId
- const { projectName, filePath } = splitPath(projectId, req.body.path)
- const metadata = await TpdsUpdateHandler.promises.createFolder(
- userId,
- projectId,
- projectName,
- filePath
- )
- if (metadata == null) {
- return HttpErrorHandler.conflict(req, res, 'Could not create folder', {
- userId,
- projectName,
- filePath,
- })
- }
- res.json({
- entityId: metadata.folderId.toString(),
- projectId: metadata.projectId.toString(),
- path: metadata.path,
- folderId: metadata.parentFolderId?.toString() || null,
- })
- }
- // updateProjectContents and deleteProjectContents are used by GitHub. The
- // project_id is known so we can skip right ahead to creating/updating/deleting
- // the file. These methods will not ignore noisy files like .DS_Store,
- // .gitignore, etc because people are generally more explicit with the files
- // they want in git.
- async function updateProjectContents(req, res, next) {
- const projectId = req.params.project_id
- const path = `/${req.params[0]}` // UpdateMerger expects leading slash
- const source = req.headers['x-update-source'] || 'unknown'
- try {
- await UpdateMerger.promises.mergeUpdate(null, projectId, path, req, source)
- } catch (error) {
- if (
- error instanceof Errors.InvalidNameError ||
- error instanceof Errors.DuplicateNameError
- ) {
- return res.sendStatus(422)
- } else {
- throw error
- }
- }
- res.sendStatus(200)
- }
- async function deleteProjectContents(req, res, next) {
- const projectId = req.params.project_id
- const path = `/${req.params[0]}` // UpdateMerger expects leading slash
- const source = req.headers['x-update-source'] || 'unknown'
- await UpdateMerger.promises.deleteUpdate(null, projectId, path, source)
- res.sendStatus(200)
- }
- async function getQueues(req, res, next) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- res.json(await TpdsQueueManager.promises.getQueues(userId))
- }
- function parseParams(req) {
- const userId = req.params.user_id
- const projectId = req.params.project_id
- const { projectName, filePath } = splitPath(projectId, req.params[0])
- return { filePath, userId, projectName, projectId }
- }
- function splitPath(projectId, path) {
- let filePath, projectName
- path = Path.join('/', path)
- if (projectId) {
- filePath = path
- projectName = ''
- } else if (path.substring(1).indexOf('/') === -1) {
- filePath = '/'
- projectName = path.substring(1)
- } else {
- filePath = path.substring(path.indexOf('/', 1))
- projectName = path.substring(0, path.indexOf('/', 1))
- projectName = projectName.replace('/', '')
- }
- return { filePath, projectName }
- }
- export default {
- createProject: expressify(createProject),
- mergeUpdate: expressify(mergeUpdate),
- deleteUpdate: expressify(deleteUpdate),
- updateFolder: expressify(updateFolder),
- updateProjectContents: expressify(updateProjectContents),
- deleteProjectContents: expressify(deleteProjectContents),
- getQueues: expressify(getQueues),
- // for tests only
- parseParams,
- }
|