소스 검색

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
Jakob Ackermann 3 년 전
부모
커밋
37c69ec830

+ 11 - 0
services/web/app/src/Features/Collaborators/CollaboratorsHandler.js

@@ -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(

+ 27 - 3
services/web/app/src/Features/Project/ProjectCreationHandler.js

@@ -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
 }

+ 21 - 0
services/web/app/src/Features/ThirdPartyDataStore/TpdsController.js

@@ -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),

+ 29 - 0
services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js

@@ -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,

+ 5 - 0
services/web/app/src/router.js

@@ -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(),

+ 19 - 0
services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js

@@ -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

+ 43 - 2
services/web/test/unit/src/ThirdPartyDataStore/TpdsControllerTests.js

@@ -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'