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
This commit is contained in:
Jakob Ackermann 2022-10-05 10:27:19 +01:00 committed by Copybot
parent ac91f40c08
commit 37c69ec830
7 changed files with 155 additions and 5 deletions

View file

@ -9,6 +9,7 @@ const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher')
const CollaboratorsGetter = require('./CollaboratorsGetter')
const Errors = require('../Errors/Errors')
const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
module.exports = {
userIsTokenMember: callbackify(userIsTokenMember),
@ -98,6 +99,8 @@ async function addUserIdToProject(
privilegeLevel
) {
const project = await ProjectGetter.promises.getProject(projectId, {
owner_ref: 1,
name: 1,
collaberator_refs: 1,
readOnly_refs: 1,
})
@ -127,6 +130,14 @@ async function addUserIdToProject(
await Project.updateOne({ _id: projectId }, { $addToSet: level }).exec()
// Ensure there is a dedicated folder for this "new" project.
await TpdsUpdateSender.promises.createProject({
projectId,
projectName: project.name,
ownerId: project.owner_ref,
userId,
})
// Flush to TPDS in background to add files to collaborator's Dropbox
TpdsProjectFlusher.promises.flushProjectToTpds(projectId).catch(err => {
logger.error(

View file

@ -15,6 +15,7 @@ const path = require('path')
const { callbackify } = require('util')
const _ = require('underscore')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
const MONTH_NAMES = [
'January',
@ -35,9 +36,19 @@ const templateProjectDir = Features.hasFeature('saas')
? 'example-project'
: 'example-project-sp'
async function createBlankProject(ownerId, projectName, attributes = {}) {
async function createBlankProject(
ownerId,
projectName,
attributes = {},
options
) {
const isImport = attributes && attributes.overleaf
const project = await _createBlankProject(ownerId, projectName, attributes)
const project = await _createBlankProject(
ownerId,
projectName,
attributes,
options
)
const segmentation = _.pick(attributes, [
'fromV1TemplateId',
'fromV1TemplateVersionId',
@ -131,7 +142,12 @@ async function _addExampleProjectFiles(ownerId, projectName, project) {
)
}
async function _createBlankProject(ownerId, projectName, attributes = {}) {
async function _createBlankProject(
ownerId,
projectName,
attributes = {},
{ skipCreatingInTPDS = false } = {}
) {
metrics.inc('project-creation')
const timer = new metrics.Timer('project-creation')
await ProjectDetailsHandler.promises.validateProjectName(projectName)
@ -171,6 +187,14 @@ async function _createBlankProject(ownerId, projectName, attributes = {}) {
const user = await User.findById(ownerId, 'ace.spellCheckLanguage')
project.spellCheckLanguage = user.ace.spellCheckLanguage
await project.save()
if (!skipCreatingInTPDS) {
await TpdsUpdateSender.promises.createProject({
projectId: project._id,
projectName,
ownerId,
userId: ownerId,
})
}
timer.done()
return project
}

View file

@ -7,9 +7,29 @@ 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'
@ -175,6 +195,7 @@ function splitPath(projectId, path) {
}
module.exports = {
createProject: expressify(createProject),
mergeUpdate: expressify(mergeUpdate),
deleteUpdate: expressify(deleteUpdate),
updateFolder: expressify(updateFolder),

View file

@ -142,6 +142,33 @@ async function deleteEntity(params) {
}
}
async function createProject(params) {
if (!tpdsUrl) return // Server CE/Pro
const { projectId, projectName, ownerId, userId } = params
const job = {
method: 'post',
headers: {
sl_project_id: projectId.toString(),
sl_all_user_ids: JSON.stringify([userId.toString()]),
sl_project_owner_user_id: ownerId.toString(),
},
uri: Path.join(
tpdsUrl,
'user',
userId.toString(),
'project',
'new',
encodeURIComponent(projectName)
),
title: 'createProject',
sl_all_user_ids: JSON.stringify([userId]),
}
await enqueue(userId, 'standardHttpRequest', job)
}
async function deleteProject(params) {
const { projectId } = params
// deletion only applies to project archiver
@ -274,6 +301,7 @@ const TpdsUpdateSender = {
addEntity: callbackify(addEntity),
addFile: callbackify(addFile),
deleteEntity: callbackify(deleteEntity),
createProject: callbackify(createProject),
deleteProject: callbackify(deleteProject),
enqueue: callbackify(enqueue),
moveEntity: callbackify(moveEntity),
@ -283,6 +311,7 @@ const TpdsUpdateSender = {
addEntity,
addFile,
deleteEntity,
createProject,
deleteProject,
enqueue,
moveEntity,

View file

@ -908,6 +908,11 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
DocumentController.setDocument
)
privateApiRouter.post(
'/user/:user_id/project/new',
AuthenticationController.requirePrivateApiAuth(),
TpdsController.createProject
)
privateApiRouter.post(
'/tpds/folder-update',
AuthenticationController.requirePrivateApiAuth(),

View file

@ -20,6 +20,8 @@ describe('CollaboratorsHandler', function () {
this.addingUserId = ObjectId()
this.project = {
_id: ObjectId(),
owner_ref: this.addingUserId,
name: 'Foo',
}
this.archivedProject = {
@ -46,6 +48,11 @@ describe('CollaboratorsHandler', function () {
flushProjectToTpds: sinon.stub().resolves(),
},
}
this.TpdsUpdateSender = {
promises: {
createProject: sinon.stub().resolves(),
},
}
this.ProjectGetter = {
promises: {
getProject: sinon.stub().resolves(this.project),
@ -66,6 +73,7 @@ describe('CollaboratorsHandler', function () {
'../Contacts/ContactManager': this.ContactManager,
'../../models/Project': { Project },
'../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
'../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender,
'../Project/ProjectGetter': this.ProjectGetter,
'../Project/ProjectHelper': this.ProjectHelper,
'./CollaboratorsGetter': this.CollaboratorsGetter,
@ -212,6 +220,17 @@ describe('CollaboratorsHandler', function () {
)
})
it('should create the project folder in dropbox', function () {
expect(
this.TpdsUpdateSender.promises.createProject
).to.have.been.calledWith({
projectId: this.project._id,
projectName: this.project.name,
ownerId: this.addingUserId,
userId: this.userId,
})
})
it('should flush the project to the TPDS', function () {
expect(
this.TpdsProjectFlusher.promises.flushProjectToTpds

View file

@ -1,8 +1,10 @@
const { ObjectId } = require('mongodb')
const { expect } = require('chai')
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const { ObjectId } = require('mongodb')
const Errors = require('../../../../app/src/Features/Errors/Errors')
const MockResponse = require('../helpers/MockResponse')
const MockRequest = require('../helpers/MockRequest')
const MODULE_PATH =
'../../../../app/src/Features/ThirdPartyDataStore/TpdsController.js'
@ -44,6 +46,15 @@ describe('TpdsController', function () {
conflict: sinon.stub(),
}
this.newProject = { _id: ObjectId() }
this.ProjectCreationHandler = {
promises: { createBlankProject: sinon.stub().resolves(this.newProject) },
}
this.ProjectDetailsHandler = {
promises: {
generateUniqueName: sinon.stub().resolves('unique'),
},
}
this.TpdsController = SandboxedModule.require(MODULE_PATH, {
requires: {
'./TpdsUpdateHandler': this.TpdsUpdateHandler,
@ -52,12 +63,42 @@ describe('TpdsController', function () {
'../Authentication/SessionManager': this.SessionManager,
'../Errors/HttpErrorHandler': this.HttpErrorHandler,
'./TpdsQueueManager': this.TpdsQueueManager,
'../Project/ProjectCreationHandler': this.ProjectCreationHandler,
'../Project/ProjectDetailsHandler': this.ProjectDetailsHandler,
},
})
this.user_id = 'dsad29jlkjas'
})
describe('creating a project', function () {
it('should yield the new projects id', function (done) {
const res = new MockResponse()
const req = new MockRequest()
req.params.user_id = this.user_id
req.body = { projectName: 'foo' }
res.callback = err => {
if (err) done(err)
expect(res.body).to.equal(
JSON.stringify({ projectId: this.newProject._id.toString() })
)
expect(
this.ProjectDetailsHandler.promises.generateUniqueName
).to.have.been.calledWith(this.user_id, 'foo')
expect(
this.ProjectCreationHandler.promises.createBlankProject
).to.have.been.calledWith(
this.user_id,
'unique',
{},
{ skipCreatingInTPDS: true }
)
done()
}
this.TpdsController.createProject(req, res)
})
})
describe('getting an update', function () {
beforeEach(function () {
this.projectName = 'projectName'