Files
overleaf-cep/services/web/app/src/Features/ThirdPartyDataStore/TpdsController.js
Jakob Ackermann 37c69ec830 Merge pull request #9493 from overleaf/jpa-dropbox-create-project-action
[misc] create new project (folder) when creating project in dropbox/web

GitOrigin-RevId: 4235b6ed66d0957bf45cb6f6009201ee02e188ca
2022-10-05 13:25:22 +00:00

209 lines
6.2 KiB
JavaScript

const { expressify } = require('../../util/promises')
const TpdsUpdateHandler = require('./TpdsUpdateHandler')
const UpdateMerger = require('./UpdateMerger')
const Errors = require('../Errors/Errors')
const logger = require('@overleaf/logger')
const Path = require('path')
const metrics = require('@overleaf/metrics')
const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
const SessionManager = require('../Authentication/SessionManager')
const ProjectCreationHandler = require('../Project/ProjectCreationHandler')
const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
const HttpErrorHandler = require('../Errors/HttpErrorHandler')
const TpdsQueueManager = require('./TpdsQueueManager')
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-sl-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)
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-sl-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-sl-update-source'] || 'unknown'
try {
await UpdateMerger.promises.mergeUpdate(null, projectId, path, req, source)
} catch (error) {
if (error.constructor === Errors.InvalidNameError) {
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-sl-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 }
}
module.exports = {
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,
}