diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile index b93982876f..6d32c863cf 100644 --- a/services/web/Jenkinsfile +++ b/services/web/Jenkinsfile @@ -49,7 +49,7 @@ pipeline { } } steps { - sh 'make compile_full' + sh 'make clean compile_full' // replace the build number placeholder for sentry sh 'node_modules/.bin/grunt version' } diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee index ab6e362221..c5a74fea28 100644 --- a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee +++ b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee @@ -6,7 +6,6 @@ async = require 'async' logger = require('logger-sharelatex') metrics = require('metrics-sharelatex') Project = require("../../models/Project").Project -ProjectLocator = require('../../Features/Project/ProjectLocator') module.exports = DocumentUpdaterHandler = flushProjectToMongo: (project_id, callback = (error) ->)-> diff --git a/services/web/app/coffee/Features/Documents/DocumentController.coffee b/services/web/app/coffee/Features/Documents/DocumentController.coffee index 5a68a4777c..3cd6f7c8b4 100644 --- a/services/web/app/coffee/Features/Documents/DocumentController.coffee +++ b/services/web/app/coffee/Features/Documents/DocumentController.coffee @@ -1,4 +1,5 @@ ProjectEntityHandler = require "../Project/ProjectEntityHandler" +ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler" logger = require("logger-sharelatex") module.exports = @@ -28,7 +29,7 @@ module.exports = doc_id = req.params.doc_id {lines, version, ranges} = req.body logger.log doc_id:doc_id, project_id:project_id, "receiving set document request from api (docupdater)" - ProjectEntityHandler.updateDocLines project_id, doc_id, lines, version, ranges, (error) -> + ProjectEntityUpdateHandler.updateDocLines project_id, doc_id, lines, version, ranges, (error) -> if error? logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument" return next(error) diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index fa94c8d546..b222dc297a 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -1,128 +1,109 @@ logger = require('logger-sharelatex') Metrics = require('metrics-sharelatex') sanitize = require('sanitizer') -ProjectEntityHandler = require('../Project/ProjectEntityHandler') +ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler') ProjectOptionsHandler = require('../Project/ProjectOptionsHandler') ProjectDetailsHandler = require('../Project/ProjectDetailsHandler') ProjectDeleter = require("../Project/ProjectDeleter") DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') EditorRealTimeController = require("./EditorRealTimeController") async = require('async') -LockManager = require("../../infrastructure/LockManager") PublicAccessLevels = require("../Authorization/PublicAccessLevels") _ = require('underscore') module.exports = EditorController = - setDoc: (project_id, doc_id, user_id, docLines, source, callback = (err)->)-> - DocumentUpdaterHandler.setDocument project_id, doc_id, user_id, docLines, source, (err)=> - logger.log project_id:project_id, doc_id:doc_id, "notifying users that the document has been updated" - DocumentUpdaterHandler.flushDocToMongo project_id, doc_id, callback - - addDoc: (project_id, folder_id, docName, docLines, source, user_id, callback = (error, doc)->)-> - LockManager.runWithLock project_id, - (cb) -> EditorController.addDocWithoutLock project_id, folder_id, docName, docLines, source, user_id, cb - (err, doc) -> - if err? - logger.err err:err, project_id:project_id, source:source, "could add doc" - return callback err - callback null, doc - - addDocWithoutLock: (project_id, folder_id, docName, docLines, source, user_id, callback = (error, doc)->)-> docName = docName.trim() logger.log {project_id, folder_id, docName, source}, "sending new doc to project" Metrics.inc "editor.add-doc" - ProjectEntityHandler.addDoc project_id, folder_id, docName, docLines, user_id, (err, doc, folder_id)=> + ProjectEntityUpdateHandler.addDoc project_id, folder_id, docName, docLines, user_id, (err, doc, folder_id)=> if err? logger.err err:err, project_id:project_id, docName:docName, "error adding doc without lock" return callback(err) EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc, source) callback(err, doc) - addFile: (project_id, folder_id, fileName, path, source, user_id, callback = (error, file)->)-> - LockManager.runWithLock project_id, - (cb) -> EditorController.addFileWithoutLock project_id, folder_id, fileName, path, source, user_id, cb - (err, file) -> - if err? - logger.err err:err, project_id:project_id, source:source, "could add file" - return callback(err) - callback null, file - - addFileWithoutLock: (project_id, folder_id, fileName, path, source, user_id, callback = (error, file)->)-> + addFile: (project_id, folder_id, fileName, fsPath, source, user_id, callback = (error, file)->)-> fileName = fileName.trim() - logger.log {project_id, folder_id, fileName, path}, "sending new file to project" + logger.log {project_id, folder_id, fileName, fsPath}, "sending new file to project" Metrics.inc "editor.add-file" - ProjectEntityHandler.addFile project_id, folder_id, fileName, path, user_id, (err, fileRef, folder_id)=> + ProjectEntityUpdateHandler.addFile project_id, folder_id, fileName, fsPath, user_id, (err, fileRef, folder_id)=> if err? logger.err err:err, project_id:project_id, folder_id:folder_id, fileName:fileName, "error adding file without lock" return callback(err) EditorRealTimeController.emitToRoom(project_id, 'reciveNewFile', folder_id, fileRef, source) callback(err, fileRef) - replaceFileWithoutLock: (project_id, file_id, fsPath, source, user_id, callback = (error) ->)-> - ProjectEntityHandler.replaceFile project_id, file_id, fsPath, user_id, callback + upsertDoc: (project_id, folder_id, docName, docLines, source, user_id, callback = (err)->)-> + ProjectEntityUpdateHandler.upsertDoc project_id, folder_id, docName, docLines, source, user_id, (err, doc, didAddNewDoc) -> + if didAddNewDoc + EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc, source) + callback err, doc + + upsertFile: (project_id, folder_id, fileName, fsPath, source, user_id, callback = (err, file) ->) -> + ProjectEntityUpdateHandler.upsertFile project_id, folder_id, fileName, fsPath, user_id, (err, file, didAddFile) -> + return callback(err) if err? + if didAddFile + EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', folder_id, file, source + callback null, file + + upsertDocWithPath: (project_id, elementPath, docLines, source, user_id, callback) -> + ProjectEntityUpdateHandler.upsertDocWithPath project_id, elementPath, docLines, source, user_id, (err, doc, didAddNewDoc, newFolders, lastFolder) -> + return callback(err) if err? + EditorController._notifyProjectUsersOfNewFolders project_id, newFolders, (err) -> + return callback(err) if err? + if didAddNewDoc + EditorRealTimeController.emitToRoom project_id, 'reciveNewDoc', lastFolder._id, doc, source + callback() + + upsertFileWithPath: (project_id, elementPath, fsPath, source, user_id, callback) -> + ProjectEntityUpdateHandler.upsertFileWithPath project_id, elementPath, fsPath, user_id, (err, file, didAddFile, newFolders, lastFolder) -> + return callback(err) if err? + EditorController._notifyProjectUsersOfNewFolders project_id, newFolders, (err) -> + return callback(err) if err? + if didAddFile + EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', lastFolder._id, file, source + callback() addFolder : (project_id, folder_id, folderName, source, callback = (error, folder)->)-> - LockManager.runWithLock project_id, - (cb) -> EditorController.addFolderWithoutLock project_id, folder_id, folderName, source, cb - (err, folder)-> - if err? - logger.err err:err, project_id:project_id, source:source, "could not add folder" - return callback(err) - callback null, folder - - addFolderWithoutLock: (project_id, folder_id, folderName, source, callback = (error, folder)->)-> folderName = folderName.trim() logger.log {project_id, folder_id, folderName, source}, "sending new folder to project" Metrics.inc "editor.add-folder" - ProjectEntityHandler.addFolder project_id, folder_id, folderName, (err, folder, folder_id)=> + ProjectEntityUpdateHandler.addFolder project_id, folder_id, folderName, (err, folder, folder_id)=> if err? - logger.err err:err, project_id:project_id, folder_id:folder_id, folderName:folderName, "error adding folder without lock" + logger.err {err, project_id, folder_id, folderName, source}, "could not add folder" return callback(err) - @p.notifyProjectUsersOfNewFolder project_id, folder_id, folder, (error) -> - callback error, folder + EditorController._notifyProjectUsersOfNewFolder project_id, folder_id, folder, (err) -> + return callback(err) if err? + callback null, folder mkdirp : (project_id, path, callback = (error, newFolders, lastFolder)->)-> - LockManager.runWithLock project_id, - (cb) -> EditorController.mkdirpWithoutLock project_id, path, cb - (err, newFolders, lastFolder) -> - if err? - logger.err err:err, project_id:project_id, "could not mkdirp" - return callback(err) - callback err, newFolders, lastFolder - - mkdirpWithoutLock: (project_id, path, callback = (error, newFolders, lastFolder)->)-> logger.log project_id:project_id, path:path, "making directories if they don't exist" - ProjectEntityHandler.mkdirp project_id, path, (err, newFolders, lastFolder)=> + ProjectEntityUpdateHandler.mkdirp project_id, path, (err, newFolders, lastFolder)=> if err? - logger.err err:err, project_id:project_id, path:path, "error mkdirp without lock" + logger.err err:err, project_id:project_id, path:path, "could not mkdirp" return callback(err) - self = @ - jobs = _.map newFolders, (folder, index)-> - return (cb)-> - self.p.notifyProjectUsersOfNewFolder project_id, folder.parentFolder_id, folder, cb - async.series jobs, (err)-> - callback err, newFolders, lastFolder + + EditorController._notifyProjectUsersOfNewFolders project_id, newFolders, (err) -> + return callback(err) if err? + callback null, newFolders, lastFolder deleteEntity : (project_id, entity_id, entityType, source, userId, callback = (error)->)-> - LockManager.runWithLock project_id, - (cb) -> EditorController.deleteEntityWithoutLock project_id, entity_id, entityType, source, userId, cb - (err)-> - if err? - logger.err err:err, project_id:project_id, "could not delete entity" - callback(err) - - deleteEntityWithoutLock: (project_id, entity_id, entityType, source, userId, callback)-> logger.log {project_id, entity_id, entityType, source}, "start delete process of entity" Metrics.inc "editor.delete-entity" - ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, userId, (err)-> + ProjectEntityUpdateHandler.deleteEntity project_id, entity_id, entityType, userId, (err)-> if err? - logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, "error deleting entity" + logger.err {err, project_id, entity_id, entityType}, "could not delete entity" return callback(err) - logger.log project_id:project_id, entity_id:entity_id, entityType:entityType, "telling users entity has been deleted" + logger.log {project_id, entity_id, entityType}, "telling users entity has been deleted" EditorRealTimeController.emitToRoom(project_id, 'removeEntity', entity_id, source) - if callback? - callback() + callback() + + deleteEntityWithPath: (project_id, path, source, user_id, callback) -> + ProjectEntityUpdateHandler.deleteEntityWithPath project_id, path, user_id, (err, entity_id) -> + return callback(err) if err? + EditorRealTimeController.emitToRoom(project_id, 'removeEntity', entity_id, source) + callback null, entity_id notifyUsersProjectHasBeenDeletedOrRenamed: (project_id, callback)-> EditorRealTimeController.emitToRoom(project_id, 'projectRenamedOrDeletedByExternalSource') @@ -146,26 +127,22 @@ module.exports = EditorController = newName = sanitize.escape(newName) Metrics.inc "editor.rename-entity" logger.log entity_id:entity_id, entity_id:entity_id, entity_id:entity_id, "reciving new name for entity for project" - LockManager.runWithLock project_id, - (cb) -> ProjectEntityHandler.renameEntity project_id, entity_id, entityType, newName, userId, cb - (err) -> - if err? - logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, newName:newName, "error renaming entity" - return callback(err) - if newName.length > 0 - EditorRealTimeController.emitToRoom project_id, 'reciveEntityRename', entity_id, newName - callback() + ProjectEntityUpdateHandler.renameEntity project_id, entity_id, entityType, newName, userId, (err) -> + if err? + logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, newName:newName, "error renaming entity" + return callback(err) + if newName.length > 0 + EditorRealTimeController.emitToRoom project_id, 'reciveEntityRename', entity_id, newName + callback() moveEntity: (project_id, entity_id, folder_id, entityType, userId, callback = (error) ->)-> Metrics.inc "editor.move-entity" - LockManager.runWithLock project_id, - (cb) -> ProjectEntityHandler.moveEntity project_id, entity_id, folder_id, entityType, userId, cb - (err) -> - if err? - logger.err err:err, project_id:project_id, entity_id:entity_id, folder_id:folder_id, "error moving entity" - return callback(err) - EditorRealTimeController.emitToRoom project_id, 'reciveEntityMove', entity_id, folder_id - callback() + ProjectEntityUpdateHandler.moveEntity project_id, entity_id, folder_id, entityType, userId, (err) -> + if err? + logger.err err:err, project_id:project_id, entity_id:entity_id, folder_id:folder_id, "error moving entity" + return callback(err) + EditorRealTimeController.emitToRoom project_id, 'reciveEntityMove', entity_id, folder_id + callback() renameProject: (project_id, newName, callback = (err) ->) -> ProjectDetailsHandler.renameProject project_id, newName, (err) -> @@ -210,13 +187,17 @@ module.exports = EditorController = callback() setRootDoc: (project_id, newRootDocID, callback = (err) ->) -> - ProjectEntityHandler.setRootDoc project_id, newRootDocID, (err) -> + ProjectEntityUpdateHandler.setRootDoc project_id, newRootDocID, (err) -> return callback(err) if err? EditorRealTimeController.emitToRoom project_id, 'rootDocUpdated', newRootDocID callback() - p: - notifyProjectUsersOfNewFolder: (project_id, folder_id, folder, callback = (error)->)-> - logger.log project_id:project_id, folder:folder, parentFolder_id:folder_id, "sending newly created folder out to users" - EditorRealTimeController.emitToRoom(project_id, "reciveNewFolder", folder_id, folder) - callback() + _notifyProjectUsersOfNewFolders: (project_id, folders, callback = (error)->)-> + async.eachSeries folders, + (folder, cb) -> EditorController._notifyProjectUsersOfNewFolder project_id, folder.parentFolder_id, folder, cb + callback + + _notifyProjectUsersOfNewFolder: (project_id, folder_id, folder, callback = (error)->)-> + logger.log project_id:project_id, folder:folder, parentFolder_id:folder_id, "sending newly created folder out to users" + EditorRealTimeController.emitToRoom(project_id, "reciveNewFolder", folder_id, folder) + callback() diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 614781211c..40a4c353fb 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -1,4 +1,4 @@ -ProjectEntityHandler = require "../Project/ProjectEntityHandler" +ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler" ProjectDeleter = require "../Project/ProjectDeleter" logger = require "logger-sharelatex" EditorRealTimeController = require "./EditorRealTimeController" @@ -66,7 +66,7 @@ module.exports = EditorHttpController = return res.sendStatus 400 # Malformed request logger.log project_id: project_id, doc_id: doc_id, "restoring doc" - ProjectEntityHandler.restoreDoc project_id, doc_id, name, (err, doc, folder_id) => + ProjectEntityUpdateHandler.restoreDoc project_id, doc_id, name, (err, doc, folder_id) => return next(error) if error? EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc) res.json { diff --git a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee index 451f90865a..fa6307ae1b 100644 --- a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee @@ -5,7 +5,7 @@ Settings = require('settings-sharelatex') ObjectId = require('mongoose').Types.ObjectId Project = require('../../models/Project').Project Folder = require('../../models/Folder').Folder -ProjectEntityHandler = require('./ProjectEntityHandler') +ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') ProjectDetailsHandler = require('./ProjectDetailsHandler') HistoryManager = require('../History/HistoryManager') User = require('../../models/User').User @@ -54,11 +54,11 @@ module.exports = ProjectCreationHandler = return callback(error) if error? self._buildTemplate "mainbasic.tex", owner_id, projectName, (error, docLines)-> return callback(error) if error? - ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, owner_id, (error, doc)-> + ProjectEntityUpdateHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, owner_id, (error, doc)-> if error? logger.err err:error, "error adding doc when creating basic project" return callback(error) - ProjectEntityHandler.setRootDoc project._id, doc._id, (error) -> + ProjectEntityUpdateHandler.setRootDoc project._id, doc._id, (error) -> callback(error, project) createExampleProject: (owner_id, projectName, callback = (error, project) ->)-> @@ -69,17 +69,17 @@ module.exports = ProjectCreationHandler = (callback) -> self._buildTemplate "main.tex", owner_id, projectName, (error, docLines)-> return callback(error) if error? - ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, owner_id, (error, doc)-> + ProjectEntityUpdateHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, owner_id, (error, doc)-> return callback(error) if error? - ProjectEntityHandler.setRootDoc project._id, doc._id, callback + ProjectEntityUpdateHandler.setRootDoc project._id, doc._id, callback (callback) -> self._buildTemplate "references.bib", owner_id, projectName, (error, docLines)-> return callback(error) if error? - ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "references.bib", docLines, owner_id, (error, doc)-> + ProjectEntityUpdateHandler.addDoc project._id, project.rootFolder[0]._id, "references.bib", docLines, owner_id, (error, doc)-> callback(error) (callback) -> universePath = Path.resolve(__dirname + "/../../../templates/project_files/universe.jpg") - ProjectEntityHandler.addFile project._id, project.rootFolder[0]._id, "universe.jpg", universePath, owner_id, callback + ProjectEntityUpdateHandler.addFile project._id, project.rootFolder[0]._id, "universe.jpg", universePath, owner_id, callback ], (error) -> callback(error, project) diff --git a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee index d4640db966..0d7498c22c 100644 --- a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee @@ -1,5 +1,5 @@ projectCreationHandler = require('./ProjectCreationHandler') -projectEntityHandler = require('./ProjectEntityHandler') +ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') projectLocator = require('./ProjectLocator') projectOptionsHandler = require('./ProjectOptionsHandler') DocumentUpdaterHandler = require("../DocumentUpdater/DocumentUpdaterHandler") @@ -14,14 +14,14 @@ module.exports = ProjectDuplicator = _copyDocs: (owner_id, newProject, originalRootDoc, originalFolder, desFolder, docContents, callback)-> setRootDoc = _.once (doc_id)-> - projectEntityHandler.setRootDoc newProject._id, doc_id + ProjectEntityUpdateHandler.setRootDoc newProject._id, doc_id docs = originalFolder.docs or [] jobs = docs.map (doc)-> return (cb)-> if !doc?._id? return callback() content = docContents[doc._id.toString()] - projectEntityHandler.addDoc newProject, desFolder._id, doc.name, content.lines, owner_id, (err, newDoc)-> + ProjectEntityUpdateHandler.addDoc newProject._id, desFolder._id, doc.name, content.lines, owner_id, (err, newDoc)-> if err? logger.err err:err, "error copying doc" return callback(err) @@ -35,7 +35,7 @@ module.exports = ProjectDuplicator = fileRefs = originalFolder.fileRefs or [] jobs = fileRefs.map (file)-> return (cb)-> - projectEntityHandler.copyFileFromExistingProjectWithProject newProject, desFolder._id, originalProject_id, file, owner_id, cb + ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject newProject, desFolder._id, originalProject_id, file, owner_id, cb async.parallelLimit jobs, 5, callback @@ -51,7 +51,7 @@ module.exports = ProjectDuplicator = return (cb)-> if !childFolder?._id? return cb() - projectEntityHandler.addFolderWithProject newProject, desFolder?._id, childFolder.name, (err, newFolder)-> + ProjectEntityUpdateHandler.addFolder newProject._id, desFolder?._id, childFolder.name, (err, newFolder)-> return cb(err) if err? ProjectDuplicator._copyFolderRecursivly owner_id, newProject_id, originalProject_id, originalRootDoc, childFolder, newFolder, docContents, cb diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index 9a75092bd0..e2645f1618 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -1,32 +1,16 @@ -Project = require('../../models/Project').Project -settings = require "settings-sharelatex" -Doc = require('../../models/Doc').Doc -Folder = require('../../models/Folder').Folder -File = require('../../models/File').File -FileStoreHandler = require("../FileStore/FileStoreHandler") -Errors = require "../Errors/Errors" -tpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') -projectLocator = require('./ProjectLocator') -path = require "path" -async = require "async" _ = require('underscore') +async = require "async" +path = require "path" logger = require('logger-sharelatex') -docComparitor = require('./DocLinesComparitor') -projectUpdateHandler = require('./ProjectUpdateHandler') DocstoreManager = require "../Docstore/DocstoreManager" -ProjectGetter = require "./ProjectGetter" -CooldownManager = require '../Cooldown/CooldownManager' DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') -SafePath = require './SafePath' - -module.exports = ProjectEntityHandler = - getAllFolders: (project_id, callback) -> - logger.log project_id:project_id, "getting all folders for project" - ProjectGetter.getProjectWithoutDocLines project_id, (err, project) -> - return callback(err) if err? - return callback("no project") if !project? - ProjectEntityHandler.getAllFoldersFromProject project, callback +Errors = require '../Errors/Errors' +Project = require('../../models/Project').Project +ProjectLocator = require('./ProjectLocator') +ProjectGetter = require "./ProjectGetter" +TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') +module.exports = ProjectEntityHandler = self = getAllDocs: (project_id, callback) -> logger.log project_id:project_id, "getting all docs for project" @@ -40,7 +24,7 @@ module.exports = ProjectEntityHandler = for docContent in docContentsArray docContents[docContent._id] = docContent - ProjectEntityHandler.getAllFolders project_id, (error, folders = {}) -> + self._getAllFolders project_id, (error, folders = {}) -> return callback(error) if error? docs = {} for folderPath, folder of folders @@ -58,7 +42,7 @@ module.exports = ProjectEntityHandler = getAllFiles: (project_id, callback) -> logger.log project_id:project_id, "getting all files for project" - @getAllFolders project_id, (err, folders = {}) -> + self._getAllFolders project_id, (err, folders = {}) -> return callback(err) if err? files = {} for folderPath, folder of folders @@ -67,20 +51,9 @@ module.exports = ProjectEntityHandler = files[path.join(folderPath, file.name)] = file callback null, files - getAllFoldersFromProject: (project, callback) -> - folders = {} - processFolder = (basePath, folder) -> - folders[basePath] = folder - for childFolder in (folder.folders or []) - if childFolder.name? - processFolder path.join(basePath, childFolder.name), childFolder - - processFolder "/", project.rootFolder[0] - callback null, folders - getAllEntitiesFromProject: (project, callback) -> logger.log project:project, "getting all files for project" - @getAllFoldersFromProject project, (err, folders = {}) -> + self._getAllFoldersFromProject project, (err, folders = {}) -> return callback(err) if err? docs = [] files = [] @@ -95,7 +68,7 @@ module.exports = ProjectEntityHandler = getAllDocPathsFromProject: (project, callback) -> logger.log project:project, "getting all docs for project" - @getAllFoldersFromProject project, (err, folders = {}) -> + self._getAllFoldersFromProject project, (err, folders = {}) -> return callback(err) if err? docPath = {} for folderPath, folder of folders @@ -105,7 +78,6 @@ module.exports = ProjectEntityHandler = callback null, docPath flushProjectToThirdPartyDataStore: (project_id, callback) -> - self = @ logger.log project_id:project_id, "flushing project to tpds" DocumentUpdaterHandler.flushProjectToMongo project_id, (error) -> return callback(error) if error? @@ -117,25 +89,17 @@ module.exports = ProjectEntityHandler = for docPath, doc of docs do (docPath, doc) -> requests.push (cb) -> - tpdsUpdateSender.addDoc {project_id:project_id, doc_id:doc._id, path:docPath, project_name:project.name, rev:doc.rev||0}, cb + TpdsUpdateSender.addDoc {project_id:project_id, doc_id:doc._id, path:docPath, project_name:project.name, rev:doc.rev||0}, cb self.getAllFiles project_id, (error, files) -> return callback(error) if error? for filePath, file of files do (filePath, file) -> requests.push (cb) -> - tpdsUpdateSender.addFile {project_id:project_id, file_id:file._id, path:filePath, project_name:project.name, rev:file.rev}, cb + TpdsUpdateSender.addFile {project_id:project_id, file_id:file._id, path:filePath, project_name:project.name, rev:file.rev}, cb async.series requests, (err) -> logger.log project_id:project_id, "finished flushing project to tpds" callback(err) - setRootDoc: (project_id, newRootDocID, callback = (error) ->)-> - logger.log project_id: project_id, rootDocId: newRootDocID, "setting root doc" - Project.update {_id:project_id}, {rootDoc_id:newRootDocID}, {}, callback - - unsetRootDoc: (project_id, callback = (error) ->) -> - logger.log project_id: project_id, "removing root doc" - Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback - getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev) ->) -> if typeof(options) == "function" callback = options @@ -143,530 +107,27 @@ module.exports = ProjectEntityHandler = if options["pathname"] delete options["pathname"] - projectLocator.findElement {project_id: project_id, element_id: doc_id, type: 'doc'}, (error, doc, path) => + ProjectLocator.findElement {project_id: project_id, element_id: doc_id, type: 'doc'}, (error, doc, path) => return callback(error) if error? DocstoreManager.getDoc project_id, doc_id, options, (error, lines, rev, version, ranges) => callback(error, lines, rev, version, ranges, path.fileSystem) else DocstoreManager.getDoc project_id, doc_id, options, callback - addDoc: (project_or_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=> - ProjectEntityHandler.addDocWithoutUpdatingHistory project_or_id, folder_id, docName, docLines, userId, (error, doc, folder_id, path) -> - return callback(error) if error? - newDocs = [ - doc: doc - path: path - docLines: docLines.join('\n') - ] - project_id = project_or_id._id or project_or_id - DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newDocs}, (error) -> - return callback(error) if error? - callback null, doc, folder_id - - addDocWithoutUpdatingHistory: (project_or_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=> - # This method should never be called directly, except when importing a project - # from Overleaf. It skips sending updates to the project history, which will break - # the history unless you are making sure it is updated in some other way. - getProject = (cb) -> - if project_or_id._id? # project - return cb(null, project_or_id) - else # id - # need to retrieve full project structure to check for duplicates - return ProjectGetter.getProject project_or_id, {rootFolder:true, name:true}, cb - getProject (error, project) -> - if err? - logger.err project_id:project_id, err:err, "error getting project for add doc" - return callback(err) - ProjectEntityHandler._addDocWithProject project, folder_id, docName, docLines, userId, callback - - _addDocWithProject: (project, folder_id, docName, docLines, userId, callback = (error, doc, folder_id, path) ->)=> - # check if docname is allowed - if not SafePath.isCleanFilename docName - return callback new Errors.InvalidNameError("invalid element name") - project_id = project._id - logger.log project_id: project_id, folder_id: folder_id, doc_name: docName, "adding doc to project with project" - confirmFolder project, folder_id, (folder_id)=> - doc = new Doc name: docName - # Put doc in docstore first, so that if it errors, we don't have a doc_id in the project - # which hasn't been created in docstore. - DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, 0, {}, (err, modified, rev) -> - return callback(err) if err? - - ProjectEntityHandler._putElement project, folder_id, doc, "doc", (err, result)=> - return callback(err) if err? - tpdsUpdateSender.addDoc { - project_id: project_id, - doc_id: doc?._id - path: result?.path?.fileSystem, - project_name: project.name, - rev: 0 - }, (err) -> - return callback(err) if err? - callback(null, doc, folder_id, result?.path?.fileSystem) - - restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) -> - # check if docname is allowed (passed in from client so we check it) - if not SafePath.isCleanFilename name - return callback new Errors.InvalidNameError("invalid element name") - # getDoc will return the deleted doc's lines, but we don't actually remove - # the deleted doc, just create a new one from its lines. - ProjectEntityHandler.getDoc project_id, doc_id, include_deleted: true, (error, lines) -> - return callback(error) if error? - ProjectEntityHandler.addDoc project_id, null, name, lines, callback - - addFileWithoutUpdatingHistory: (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)-> - # check if file name is allowed - if not SafePath.isCleanFilename fileName - return callback new Errors.InvalidNameError("invalid element name") - ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project) -> - if err? - logger.err project_id:project_id, err:err, "error getting project for add file" - return callback(err) - logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file" + _getAllFolders: (project_id, callback) -> + logger.log project_id:project_id, "getting all folders for project" + ProjectGetter.getProjectWithoutDocLines project_id, (err, project) -> return callback(err) if err? - confirmFolder project, folder_id, (folder_id)-> - fileRef = new File name : fileName - FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err, fileStoreUrl)-> - if err? - logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3" - return callback(err) - ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=> - if err? - logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project" - return callback(err) - tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) -> - return callback(err) if err? - callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl) + return callback(Errors.NotFoundError("no project")) if !project? + self._getAllFoldersFromProject project, callback - addFile: (project_id, folder_id, fileName, fsPath, userId, callback = (error, fileRef, folder_id) ->)-> - ProjectEntityHandler.addFileWithoutUpdatingHistory project_id, folder_id, fileName, fsPath, userId, (error, fileRef, folder_id, path, fileStoreUrl) -> - return callback(error) if error? - newFiles = [ - file: fileRef - path: path - url: fileStoreUrl - ] - DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) -> - return callback(error) if error? - callback null, fileRef, folder_id + _getAllFoldersFromProject: (project, callback) -> + folders = {} + processFolder = (basePath, folder) -> + folders[basePath] = folder + for childFolder in (folder.folders or []) + if childFolder.name? + processFolder path.join(basePath, childFolder.name), childFolder - replaceFile: (project_id, file_id, fsPath, userId, callback)-> - self = ProjectEntityHandler - FileStoreHandler.uploadFileFromDisk project_id, file_id, fsPath, (err, fileStoreUrl)-> - return callback(err) if err? - ProjectGetter.getProject project_id, {rootFolder: true, name:true}, (err, project) -> - return callback(err) if err? - # Note there is a potential race condition here (and elsewhere) - # If the file tree changes between findElement and the Project.update - # then the path to the file element will be out of date. In practice - # this is not a problem so long as we do not do anything longer running - # between them (like waiting for the file to upload.) - projectLocator.findElement {project:project, element_id: file_id, type: 'file'}, (err, fileRef, path)=> - return callback(err) if err? - tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:path.fileSystem, rev:fileRef.rev+1, project_name:project.name}, (err) -> - return callback(err) if err? - conditions = _id:project._id - inc = {} - inc["#{path.mongo}.rev"] = 1 - set = {} - set["#{path.mongo}.created"] = new Date() - update = - "$inc": inc - "$set": set - Project.findOneAndUpdate conditions, update, { "new": true}, (err) -> - return callback(err) if err? - newFiles = [ - file: fileRef - path: path.fileSystem - url: fileStoreUrl - ] - DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, callback - - copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)-> - project_id = project._id - logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project" - return callback(err) if err? - confirmFolder project, folder_id, (folder_id)=> - if !origonalFileRef? - logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null" - return callback() - # convert any invalid characters in original file to '_' - fileRef = new File name : SafePath.clean(origonalFileRef.name) - FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)-> - if err? - logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3" - return callback(err) - ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=> - if err? - logger.err { err, project_id, folder_id }, "error putting element as part of copy" - return callback(err) - tpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) -> - if err? - logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker" - newFiles = [ - file: fileRef - path: result?.path?.fileSystem - url: fileStoreUrl - ] - DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) -> - return callback(error) if error? - callback null, fileRef, folder_id - - mkdirp: (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)-> - self = @ - folders = path.split('/') - folders = _.select folders, (folder)-> - return folder.length != 0 - - ProjectGetter.getProjectWithOnlyFolders project_id, (err, project)=> - if path == '/' - logger.log project_id: project._id, "mkdir is only trying to make path of / so sending back root folder" - return callback(null, [], project.rootFolder[0]) - logger.log project_id: project._id, path:path, folders:folders, "running mkdirp" - - builtUpPath = '' - procesFolder = (previousFolders, folderName, callback)=> - previousFolders = previousFolders || [] - parentFolder = previousFolders[previousFolders.length-1] - if parentFolder? - parentFolder_id = parentFolder._id - builtUpPath = "#{builtUpPath}/#{folderName}" - projectLocator.findElementByPath project, builtUpPath, (err, foundFolder)=> - if !foundFolder? - logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp" - @addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)-> - return callback(err) if err? - newFolder.parentFolder_id = parentFolder_id - previousFolders.push newFolder - callback null, previousFolders - else - foundFolder.filterOut = true - previousFolders.push foundFolder - callback null, previousFolders - - - async.reduce folders, [], procesFolder, (err, folders)-> - return callback(err) if err? - lastFolder = folders[folders.length-1] - folders = _.select folders, (folder)-> - !folder.filterOut - callback(null, folders, lastFolder) - - addFolder: (project_id, parentFolder_id, folderName, callback) -> - ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project)=> - if err? - logger.err project_id:project_id, err:err, "error getting project for add folder" - return callback(err) - ProjectEntityHandler.addFolderWithProject project, parentFolder_id, folderName, callback - - addFolderWithProject: (project, parentFolder_id, folderName, callback = (err, folder, parentFolder_id)->) -> - # check if folder name is allowed - if not SafePath.isCleanFilename folderName - return callback new Errors.InvalidNameError("invalid element name") - confirmFolder project, parentFolder_id, (parentFolder_id)=> - folder = new Folder name: folderName - logger.log project: project._id, parentFolder_id:parentFolder_id, folderName:folderName, "adding new folder" - ProjectEntityHandler._putElement project, parentFolder_id, folder, "folder", (err, result)=> - if err? - logger.err err:err, project_id:project._id, "error adding folder to project" - return callback(err) - callback(err, folder, parentFolder_id) - - updateDocLines : (project_id, doc_id, lines, version, ranges, callback = (error) ->)-> - ProjectGetter.getProjectWithoutDocLines project_id, (err, project)-> - return callback(err) if err? - return callback(new Errors.NotFoundError("project not found")) if !project? - logger.log project_id: project_id, doc_id: doc_id, "updating doc lines" - projectLocator.findElement {project:project, element_id:doc_id, type:"docs"}, (err, doc, path)-> - if err? - logger.error err: err, doc_id: doc_id, project_id: project_id, lines: lines, "error finding doc while updating doc lines" - return callback err - if !doc? - error = new Errors.NotFoundError("doc not found") - logger.error err: error, doc_id: doc_id, project_id: project_id, lines: lines, "doc not found while updating doc lines" - return callback(error) - - logger.log project_id: project_id, doc_id: doc_id, "telling docstore manager to update doc" - DocstoreManager.updateDoc project_id, doc_id, lines, version, ranges, (err, modified, rev) -> - if err? - logger.error err: err, doc_id: doc_id, project_id:project_id, lines: lines, "error sending doc to docstore" - return callback(err) - logger.log project_id: project_id, doc_id: doc_id, modified:modified, "finished updating doc lines" - if modified - # Don't need to block for marking as updated - projectUpdateHandler.markAsUpdated project_id - tpdsUpdateSender.addDoc {project_id:project_id, path:path.fileSystem, doc_id:doc_id, project_name:project.name, rev:rev}, callback - else - callback() - - moveEntity: (project_id, entity_id, destFolderId, entityType, userId, callback = (error) ->)-> - self = @ - logger.log {entityType, entity_id, project_id, destFolderId}, "moving entity" - if !entityType? - logger.err {err: "No entityType set", project_id, entity_id} - return callback("No entityType set") - entityType = entityType.toLowerCase() - ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project)=> - return callback(err) if err? - projectLocator.findElement {project, element_id: entity_id, type: entityType}, (err, entity, entityPath)-> - return callback(err) if err? - self._checkValidMove project, entityType, entity, entityPath, destFolderId, (error) -> - return callback(error) if error? - self.getAllEntitiesFromProject project, (error, oldDocs, oldFiles) => - return callback(error) if error? - self._removeElementFromMongoArray Project, project_id, entityPath.mongo, (err, newProject)-> - return callback(err) if err? - self._putElement newProject, destFolderId, entity, entityType, (err, result, newProject)-> - return callback(err) if err? - opts = - project_id: project_id - project_name: project.name - startPath: entityPath.fileSystem - endPath: result.path.fileSystem, - rev: entity.rev - tpdsUpdateSender.moveEntity opts - self.getAllEntitiesFromProject newProject, (error, newDocs, newFiles) => - return callback(error) if error? - DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback - - _checkValidMove: (project, entityType, entity, entityPath, destFolderId, callback = (error) ->) -> - projectLocator.findElement { project, element_id: destFolderId, type:"folder"}, (err, destEntity, destFolderPath) -> - return callback(err) if err? - # check if there is already a doc/file/folder with the same name - # in the destination folder - ProjectEntityHandler.checkValidElementName destEntity, entity.name, (err)-> - return callback(err) if err? - if entityType.match(/folder/) - logger.log destFolderPath: destFolderPath.fileSystem, folderPath: entityPath.fileSystem, "checking folder is not moving into child folder" - isNestedFolder = destFolderPath.fileSystem.slice(0, entityPath.fileSystem.length) == entityPath.fileSystem - if isNestedFolder - return callback(new Errors.InvalidNameError("destination folder is a child folder of me")) - callback() - - deleteEntity: (project_id, entity_id, entityType, userId, callback = (error) ->)-> - self = @ - logger.log entity_id:entity_id, entityType:entityType, project_id:project_id, "deleting project entity" - if !entityType? - logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id - return callback("No entityType set") - entityType = entityType.toLowerCase() - ProjectGetter.getProject project_id, {name:true, rootFolder:true}, (err, project)=> - return callback(error) if error? - projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=> - return callback(error) if error? - ProjectEntityHandler._cleanUpEntity project, entity, entityType, path.fileSystem, userId, (error) -> - return callback(error) if error? - tpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:project.name, (error) -> - return callback(error) if error? - self._removeElementFromMongoArray Project, project_id, path.mongo, (error) -> - return callback(error) if error? - callback null - - - renameEntity: (project_id, entity_id, entityType, newName, userId, callback)-> - # check if name is allowed - if not SafePath.isCleanFilename newName - return callback new Errors.InvalidNameError("invalid element name") - logger.log(entity_id: entity_id, project_id: project_id, ('renaming '+entityType)) - if !entityType? - logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id - return callback("No entityType set") - entityType = entityType.toLowerCase() - ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (error, project)=> - return callback(error) if error? - ProjectEntityHandler.getAllEntitiesFromProject project, (error, oldDocs, oldFiles) => - return callback(error) if error? - projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (error, entity, entPath, parentFolder)=> - return callback(error) if error? - # check if the new name already exists in the current folder - ProjectEntityHandler.checkValidElementName parentFolder, newName, (error) => - return callback(error) if error? - endPath = path.join(path.dirname(entPath.fileSystem), newName) - conditions = {_id:project_id} - update = "$set":{} - namePath = entPath.mongo+".name" - update["$set"][namePath] = newName - tpdsUpdateSender.moveEntity({project_id:project_id, startPath:entPath.fileSystem, endPath:endPath, project_name:project.name, rev:entity.rev}) - Project.findOneAndUpdate conditions, update, { "new": true}, (error, newProject) -> - return callback(error) if error? - ProjectEntityHandler.getAllEntitiesFromProject newProject, (error, newDocs, newFiles) => - return callback(error) if error? - DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback - - _cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) -> - if(entityType.indexOf("file") != -1) - ProjectEntityHandler._cleanUpFile project, entity, path, userId, callback - else if (entityType.indexOf("doc") != -1) - ProjectEntityHandler._cleanUpDoc project, entity, path, userId, callback - else if (entityType.indexOf("folder") != -1) - ProjectEntityHandler._cleanUpFolder project, entity, path, userId, callback - else - callback() - - _cleanUpDoc: (project, doc, path, userId, callback = (error) ->) -> - project_id = project._id.toString() - doc_id = doc._id.toString() - unsetRootDocIfRequired = (callback) => - if project.rootDoc_id? and project.rootDoc_id.toString() == doc_id - @unsetRootDoc project_id, callback - else - callback() - - unsetRootDocIfRequired (error) -> - return callback(error) if error? - DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) -> - return callback(error) if error? - ProjectEntityHandler._insertDeletedDocReference project._id, doc, (error) -> - return callback(error) if error? - DocstoreManager.deleteDoc project_id, doc_id, (error) -> - return callback(error) if error? - changes = oldDocs: [ {doc, path} ] - DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback - - _cleanUpFile: (project, file, path, userId, callback = (error) ->) -> - project_id = project._id.toString() - file_id = file._id.toString() - FileStoreHandler.deleteFile project_id, file_id, (error) -> - return callback(error) if error? - changes = oldFiles: [ {file, path} ] - DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback - - _cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) -> - jobs = [] - for doc in folder.docs - do (doc) -> - docPath = path.join(folderPath, doc.name) - jobs.push (callback) -> ProjectEntityHandler._cleanUpDoc project, doc, docPath, userId, callback - - for file in folder.fileRefs - do (file) -> - filePath = path.join(folderPath, file.name) - jobs.push (callback) -> ProjectEntityHandler._cleanUpFile project, file, filePath, userId, callback - - for childFolder in folder.folders - do (childFolder) -> - folderPath = path.join(folderPath, childFolder.name) - jobs.push (callback) -> ProjectEntityHandler._cleanUpFolder project, childFolder, folderPath, userId, callback - - async.series jobs, callback - - _removeElementFromMongoArray : (model, model_id, path, callback = (err, project) ->)-> - conditions = {_id:model_id} - update = {"$unset":{}} - update["$unset"][path] = 1 - model.update conditions, update, {}, (err)-> - pullUpdate = {"$pull":{}} - nonArrayPath = path.slice(0, path.lastIndexOf(".")) - pullUpdate["$pull"][nonArrayPath] = null - model.findOneAndUpdate conditions, pullUpdate, {"new": true}, callback - - _insertDeletedDocReference: (project_id, doc, callback = (error) ->) -> - Project.update { - _id: project_id - }, { - $push: { - deletedDocs: { - _id: doc._id - name: doc.name - } - } - }, {}, callback - - - _countElements : (project, callback)-> - - countFolder = (folder, cb = (err, count)->)-> - - jobs = _.map folder?.folders, (folder)-> - (asyncCb)-> countFolder folder, asyncCb - - async.series jobs, (err, subfolderCounts)-> - total = 0 - - if subfolderCounts?.length > 0 - total = _.reduce subfolderCounts, (a, b)-> return a + b - if folder?.folders?.length? - total += folder?.folders?.length - if folder?.docs?.length? - total += folder?.docs?.length - if folder?.fileRefs?.length? - total += folder?.fileRefs?.length - cb(null, total) - - countFolder project.rootFolder[0], callback - - _putElement: (project, folder_id, element, type, callback = (err, path, project)->)-> - sanitizeTypeOfElement = (elementType)-> - lastChar = elementType.slice -1 - if lastChar != "s" - elementType +="s" - if elementType == "files" - elementType = "fileRefs" - return elementType - - if !element? or !element._id? - e = new Error("no element passed to be inserted") - logger.err project_id:project._id, folder_id:folder_id, element:element, type:type, "failed trying to insert element as it was null" - return callback(e) - type = sanitizeTypeOfElement type - - # original check path.resolve("/", element.name) isnt "/#{element.name}" or element.name.match("/") - # check if name is allowed - if not SafePath.isCleanFilename element.name - e = new Errors.InvalidNameError("invalid element name") - logger.err project_id:project._id, folder_id:folder_id, element:element, type:type, "failed trying to insert element as name was invalid" - return callback(e) - - if !folder_id? - folder_id = project.rootFolder[0]._id - ProjectEntityHandler._countElements project, (err, count)-> - if count > settings.maxEntitiesPerProject - logger.warn project_id:project._id, "project too big, stopping insertions" - CooldownManager.putProjectOnCooldown(project._id) - return callback("project_has_to_many_files") - projectLocator.findElement {project:project, element_id:folder_id, type:"folders"}, (err, folder, path)=> - if err? - logger.err err:err, project_id:project._id, folder_id:folder_id, type:type, element:element, "error finding folder for _putElement" - return callback(err) - newPath = - fileSystem: "#{path.fileSystem}/#{element.name}" - mongo: path.mongo - # check if the path would be too long - if not SafePath.isAllowedLength newPath.fileSystem - return callback new Errors.InvalidNameError("path too long") - ProjectEntityHandler.checkValidElementName folder, element.name, (err) => - return callback(err) if err? - id = element._id+'' - element._id = require('mongoose').Types.ObjectId(id) - conditions = _id:project._id - mongopath = "#{path.mongo}.#{type}" - update = "$push":{} - update["$push"][mongopath] = element - logger.log project_id: project._id, element_id: element._id, fileType: type, folder_id: folder_id, mongopath:mongopath, "adding element to project" - Project.findOneAndUpdate conditions, update, {"new": true}, (err, project)-> - if err? - logger.err err: err, project_id: project._id, 'error saving in putElement project' - return callback(err) - callback(err, {path:newPath}, project) - - - checkValidElementName: (folder, name, callback = (err) ->) -> - # check if the name is already taken by a doc, file or - # folder. If so, return an error "file already exists". - err = new Errors.InvalidNameError("file already exists") - for doc in folder?.docs or [] - return callback(err) if doc.name is name - for file in folder?.fileRefs or [] - return callback(err) if file.name is name - for folder in folder?.folders or [] - return callback(err) if folder.name is name - callback() - -confirmFolder = (project, folder_id, callback)-> - logger.log folder_id:folder_id, project_id:project._id, "confirming folder in project" - if folder_id+'' == 'undefined' - callback(project.rootFolder[0]._id) - else if folder_id != null - callback folder_id - else - callback(project.rootFolder[0]._id) + processFolder "/", project.rootFolder[0] + callback null, folders diff --git a/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee new file mode 100644 index 0000000000..251ff10577 --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectEntityMongoUpdateHandler.coffee @@ -0,0 +1,273 @@ +_ = require('underscore') +async = require 'async' +logger = require('logger-sharelatex') +path = require('path') +settings = require('settings-sharelatex') +CooldownManager = require '../Cooldown/CooldownManager' +Errors = require '../Errors/Errors' +Folder = require('../../models/Folder').Folder +Project = require('../../models/Project').Project +ProjectEntityHandler = require('./ProjectEntityHandler') +ProjectGetter = require('./ProjectGetter') +ProjectLocator = require('./ProjectLocator') +SafePath = require './SafePath' + +module.exports = ProjectEntityMongoUpdateHandler = self = + addDoc: (project_id, folder_id, doc, callback = (err, result) ->) -> + ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project) -> + if err? + logger.err project_id:project_id, err:err, "error getting project for add doc" + return callback(err) + logger.log project_id: project_id, folder_id: folder_id, doc_name: doc.name, "adding doc to project with project" + self._confirmFolder project, folder_id, (folder_id) => + self._putElement project, folder_id, doc, "doc", callback + + addFile: (project_id, folder_id, fileRef, callback = (error, result, project) ->)-> + ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project) -> + if err? + logger.err project_id:project_id, err:err, "error getting project for add file" + return callback(err) + logger.log project_id: project._id, folder_id: folder_id, file_name: fileRef.name, "adding file" + self._confirmFolder project, folder_id, (folder_id)-> + self._putElement project, folder_id, fileRef, "file", callback + + replaceFile: (project_id, file_id, callback) -> + ProjectGetter.getProject project_id, {rootFolder: true, name:true}, (err, project) -> + return callback(err) if err? + ProjectLocator.findElement {project:project, element_id: file_id, type: 'file'}, (err, fileRef, path)=> + return callback(err) if err? + conditions = _id:project._id + inc = {} + inc["#{path.mongo}.rev"] = 1 + set = {} + set["#{path.mongo}.created"] = new Date() + update = + "$inc": inc + "$set": set + Project.update conditions, update, {}, (err) -> + return callback(err) if err? + callback null, fileRef, project, path + + mkdirp: (project_id, path, callback) -> + folders = path.split('/') + folders = _.select folders, (folder)-> + return folder.length != 0 + + ProjectGetter.getProjectWithOnlyFolders project_id, (err, project)=> + if path == '/' + logger.log project_id: project._id, "mkdir is only trying to make path of / so sending back root folder" + return callback(null, [], project.rootFolder[0]) + logger.log project_id: project._id, path:path, folders:folders, "running mkdirp" + + builtUpPath = '' + procesFolder = (previousFolders, folderName, callback)=> + previousFolders = previousFolders || [] + parentFolder = previousFolders[previousFolders.length-1] + if parentFolder? + parentFolder_id = parentFolder._id + builtUpPath = "#{builtUpPath}/#{folderName}" + ProjectLocator.findElementByPath project, builtUpPath, (err, foundFolder)=> + if !foundFolder? + logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp" + self.addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)-> + return callback(err) if err? + newFolder.parentFolder_id = parentFolder_id + previousFolders.push newFolder + callback null, previousFolders + else + foundFolder.filterOut = true + previousFolders.push foundFolder + callback null, previousFolders + + async.reduce folders, [], procesFolder, (err, folders) -> + return callback(err) if err? + lastFolder = folders[folders.length-1] + folders = _.select folders, (folder)-> + !folder.filterOut + callback null, folders, lastFolder + + moveEntity: (project_id, entity_id, destFolderId, entityType, callback = (error) ->) -> + ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project) -> + return callback(err) if err? + ProjectLocator.findElement {project, element_id: entity_id, type: entityType}, (err, entity, entityPath)-> + return callback(err) if err? + self._checkValidMove project, entityType, entity, entityPath, destFolderId, (error) -> + return callback(error) if error? + ProjectEntityHandler.getAllEntitiesFromProject project, (error, oldDocs, oldFiles) -> + return callback(error) if error? + self._removeElementFromMongoArray Project, project_id, entityPath.mongo, (err, newProject)-> + return callback(err) if err? + self._putElement newProject, destFolderId, entity, entityType, (err, result, newProject)-> + return callback(err) if err? + ProjectEntityHandler.getAllEntitiesFromProject newProject, (err, newDocs, newFiles) -> + return callback(err) if err? + startPath = entityPath.fileSystem + endPath = result.path.fileSystem + changes = {oldDocs, newDocs, oldFiles, newFiles} + callback null, project.name, startPath, endPath, entity.rev, changes, callback + + deleteEntity: (project_id, entity_id, entityType, callback) -> + ProjectGetter.getProject project_id, {name:true, rootFolder:true}, (error, project) -> + return callback(error) if error? + ProjectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path) -> + return callback(error) if error? + self._removeElementFromMongoArray Project, project_id, path.mongo, (error) -> + return callback(error) if error? + callback null, entity, path, project + + renameEntity: (project_id, entity_id, entityType, newName, callback) -> + ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (error, project)=> + return callback(error) if error? + ProjectEntityHandler.getAllEntitiesFromProject project, (error, oldDocs, oldFiles) => + return callback(error) if error? + ProjectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (error, entity, entPath, parentFolder)=> + return callback(error) if error? + # check if the new name already exists in the current folder + self._checkValidElementName parentFolder, newName, (error) => + return callback(error) if error? + endPath = path.join(path.dirname(entPath.fileSystem), newName) + conditions = {_id:project_id} + update = "$set":{} + namePath = entPath.mongo+".name" + update["$set"][namePath] = newName + Project.findOneAndUpdate conditions, update, { "new": true}, (error, newProject) -> + return callback(error) if error? + ProjectEntityHandler.getAllEntitiesFromProject newProject, (error, newDocs, newFiles) => + return callback(error) if error? + startPath = entPath.fileSystem + changes = {oldDocs, newDocs, oldFiles, newFiles} + callback null, project.name, startPath, endPath, entity.rev, changes, callback + + addFolder: (project_id, parentFolder_id, folderName, callback) -> + ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project) -> + if err? + logger.err project_id:project_id, err:err, "error getting project for add folder" + return callback(err) + self._confirmFolder project, parentFolder_id, (parentFolder_id) => + folder = new Folder name: folderName + logger.log project: project._id, parentFolder_id:parentFolder_id, folderName:folderName, "adding new folder" + self._putElement project, parentFolder_id, folder, "folder", (err)=> + if err? + logger.err err:err, project_id:project._id, "error adding folder to project" + return callback(err) + callback null, folder, parentFolder_id + + _removeElementFromMongoArray: (model, model_id, path, callback = (err, project) ->)-> + conditions = {_id:model_id} + update = {"$unset":{}} + update["$unset"][path] = 1 + model.update conditions, update, {}, (err)-> + pullUpdate = {"$pull":{}} + nonArrayPath = path.slice(0, path.lastIndexOf(".")) + pullUpdate["$pull"][nonArrayPath] = null + model.findOneAndUpdate conditions, pullUpdate, {"new": true}, callback + + _countElements: (project)-> + countFolder = (folder)-> + total = 0 + + for subfolder in folder?.folders or [] + total += countFolder(subfolder) + + if folder?.folders?.length? + total += folder.folders.length + + if folder?.docs?.length? + total += folder.docs.length + + if folder?.fileRefs?.length? + total += folder.fileRefs.length + + total + + countFolder project.rootFolder[0] + + _putElement: (project, folder_id, element, type, callback = (err, path, project)->)-> + sanitizeTypeOfElement = (elementType)-> + lastChar = elementType.slice -1 + if lastChar != "s" + elementType +="s" + if elementType == "files" + elementType = "fileRefs" + return elementType + + if !element? or !element._id? + e = new Error("no element passed to be inserted") + logger.err project_id:project._id, folder_id:folder_id, element:element, type:type, "failed trying to insert element as it was null" + return callback(e) + type = sanitizeTypeOfElement type + + # original check path.resolve("/", element.name) isnt "/#{element.name}" or element.name.match("/") + # check if name is allowed + if not SafePath.isCleanFilename element.name + e = new Errors.InvalidNameError("invalid element name") + logger.err project_id:project._id, folder_id:folder_id, element:element, type:type, "failed trying to insert element as name was invalid" + return callback(e) + + if !folder_id? + folder_id = project.rootFolder[0]._id + + if self._countElements(project) > settings.maxEntitiesPerProject + logger.warn project_id:project._id, "project too big, stopping insertions" + CooldownManager.putProjectOnCooldown(project._id) + return callback("project_has_to_many_files") + + ProjectLocator.findElement {project:project, element_id:folder_id, type:"folders"}, (err, folder, path)=> + if err? + logger.err err:err, project_id:project._id, folder_id:folder_id, type:type, element:element, "error finding folder for _putElement" + return callback(err) + newPath = + fileSystem: "#{path.fileSystem}/#{element.name}" + mongo: path.mongo + # check if the path would be too long + if not SafePath.isAllowedLength newPath.fileSystem + return callback new Errors.InvalidNameError("path too long") + self._checkValidElementName folder, element.name, (err) => + return callback(err) if err? + id = element._id+'' + element._id = require('mongoose').Types.ObjectId(id) + conditions = _id:project._id + mongopath = "#{path.mongo}.#{type}" + update = "$push":{} + update["$push"][mongopath] = element + logger.log project_id: project._id, element_id: element._id, fileType: type, folder_id: folder_id, mongopath:mongopath, "adding element to project" + Project.findOneAndUpdate conditions, update, {"new": true}, (err, project)-> + if err? + logger.err err: err, project_id: project._id, 'error saving in putElement project' + return callback(err) + callback(err, {path:newPath}, project) + + _checkValidElementName: (folder, name, callback = (err) ->) -> + # check if the name is already taken by a doc, file or + # folder. If so, return an error "file already exists". + err = new Errors.InvalidNameError("file already exists") + for doc in folder?.docs or [] + return callback(err) if doc.name is name + for file in folder?.fileRefs or [] + return callback(err) if file.name is name + for folder in folder?.folders or [] + return callback(err) if folder.name is name + callback() + + _confirmFolder: (project, folder_id, callback)-> + logger.log folder_id:folder_id, project_id:project._id, "confirming folder in project" + if folder_id+'' == 'undefined' + callback(project.rootFolder[0]._id) + else if folder_id != null + callback folder_id + else + callback(project.rootFolder[0]._id) + + _checkValidMove: (project, entityType, entity, entityPath, destFolderId, callback = (error) ->) -> + ProjectLocator.findElement { project, element_id: destFolderId, type:"folder"}, (err, destEntity, destFolderPath) -> + return callback(err) if err? + # check if there is already a doc/file/folder with the same name + # in the destination folder + self._checkValidElementName destEntity, entity.name, (err)-> + return callback(err) if err? + if entityType.match(/folder/) + logger.log destFolderPath: destFolderPath.fileSystem, folderPath: entityPath.fileSystem, "checking folder is not moving into child folder" + isNestedFolder = destFolderPath.fileSystem.slice(0, entityPath.fileSystem.length) == entityPath.fileSystem + if isNestedFolder + return callback(new Errors.InvalidNameError("destination folder is a child folder of me")) + callback() diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee new file mode 100644 index 0000000000..0d2864081c --- /dev/null +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -0,0 +1,375 @@ +_ = require 'lodash' +async = require 'async' +logger = require('logger-sharelatex') +path = require('path') +Doc = require('../../models/Doc').Doc +DocstoreManager = require('../Docstore/DocstoreManager') +DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') +Errors = require '../Errors/Errors' +File = require('../../models/File').File +FileStoreHandler = require('../FileStore/FileStoreHandler') +LockManager = require('../../infrastructure/LockManager') +Project = require('../../models/Project').Project +ProjectEntityHandler = require('./ProjectEntityHandler') +ProjectGetter = require('./ProjectGetter') +ProjectLocator = require('./ProjectLocator') +ProjectUpdateHandler = require('./ProjectUpdateHandler') +ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler') +SafePath = require './SafePath' +TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') + +wrapWithLock = (methodWithoutLock) -> + methodWithLock = (project_id, args..., callback) -> + LockManager.runWithLock project_id, + (cb) -> methodWithoutLock project_id, args..., cb + callback + methodWithLock.withoutLock = methodWithoutLock + methodWithLock + +module.exports = ProjectEntityUpdateHandler = self = + # this doesn't need any locking because it's only called by ProjectDuplicator + copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)-> + project_id = project._id + logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project" + return callback(err) if err? + ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id)=> + if !origonalFileRef? + logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null" + return callback() + # convert any invalid characters in original file to '_' + fileRef = new File name : SafePath.clean(origonalFileRef.name) + FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)-> + if err? + logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3" + return callback(err) + ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result)=> + if err? + logger.err { err, project_id, folder_id }, "error putting element as part of copy" + return callback(err) + TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) -> + if err? + logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker" + newFiles = [ + file: fileRef + path: result?.path?.fileSystem + url: fileStoreUrl + ] + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) -> + return callback(error) if error? + callback null, fileRef, folder_id + + updateDocLines: (project_id, doc_id, lines, version, ranges, callback = (error) ->)-> + ProjectGetter.getProjectWithoutDocLines project_id, (err, project)-> + return callback(err) if err? + return callback(new Errors.NotFoundError("project not found")) if !project? + logger.log project_id: project_id, doc_id: doc_id, "updating doc lines" + ProjectLocator.findElement {project:project, element_id:doc_id, type:"docs"}, (err, doc, path)-> + isDeletedDoc = false + if err? + if err instanceof Errors.NotFoundError + # We need to be able to update the doclines of deleted docs. This is + # so the doc-updater can flush a doc's content to the doc-store after + # the doc is deleted. + isDeletedDoc = true + doc = _.find project.deletedDocs, (doc) -> + doc._id.toString() == doc_id.toString() + else + return callback(err) + + if !doc? + # Do not allow an update to a doc which has never exist on this project + logger.error {doc_id, project_id, lines}, "doc not found while updating doc lines" + return callback(new Errors.NotFoundError('doc not found')) + + logger.log {project_id, doc_id}, "telling docstore manager to update doc" + DocstoreManager.updateDoc project_id, doc_id, lines, version, ranges, (err, modified, rev) -> + if err? + logger.error {err, doc_id, project_id, lines}, "error sending doc to docstore" + return callback(err) + logger.log {project_id, doc_id, modified}, "finished updating doc lines" + # path will only be present if the doc is not deleted + if modified && !isDeletedDoc + # Don't need to block for marking as updated + ProjectUpdateHandler.markAsUpdated project_id + TpdsUpdateSender.addDoc {project_id:project_id, path:path.fileSystem, doc_id:doc_id, project_name:project.name, rev:rev}, callback + else + callback() + + setRootDoc: (project_id, newRootDocID, callback = (error) ->)-> + logger.log project_id: project_id, rootDocId: newRootDocID, "setting root doc" + Project.update {_id:project_id}, {rootDoc_id:newRootDocID}, {}, callback + + unsetRootDoc: (project_id, callback = (error) ->) -> + logger.log project_id: project_id, "removing root doc" + Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback + + restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) -> + if not SafePath.isCleanFilename name + return callback new Errors.InvalidNameError("invalid element name") + # getDoc will return the deleted doc's lines, but we don't actually remove + # the deleted doc, just create a new one from its lines. + ProjectEntityHandler.getDoc project_id, doc_id, include_deleted: true, (error, lines) -> + return callback(error) if error? + self.addDoc project_id, null, name, lines, callback + + addDoc: wrapWithLock (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=> + self.addDocWithoutUpdatingHistory.withoutLock project_id, folder_id, docName, docLines, userId, (error, doc, folder_id, path) -> + return callback(error) if error? + newDocs = [ + doc: doc + path: path + docLines: docLines.join('\n') + ] + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newDocs}, (error) -> + return callback(error) if error? + callback null, doc, folder_id + + addFile: wrapWithLock (project_id, folder_id, fileName, fsPath, userId, callback = (error, fileRef, folder_id) ->)-> + self.addFileWithoutUpdatingHistory.withoutLock project_id, folder_id, fileName, fsPath, userId, (error, fileRef, folder_id, path, fileStoreUrl) -> + return callback(error) if error? + newFiles = [ + file: fileRef + path: path + url: fileStoreUrl + ] + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) -> + return callback(error) if error? + callback null, fileRef, folder_id + + replaceFile: wrapWithLock (project_id, file_id, fsPath, userId, callback)-> + FileStoreHandler.uploadFileFromDisk project_id, file_id, fsPath, (err, fileStoreUrl)-> + return callback(err) if err? + ProjectEntityMongoUpdateHandler.replaceFile project_id, file_id, (err, fileRef, project, path) -> + return callback(err) if err? + newFiles = [ + file: fileRef + path: path.fileSystem + url: fileStoreUrl + ] + TpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:path.fileSystem, rev:fileRef.rev+1, project_name:project.name}, (err) -> + return callback(err) if err? + DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, callback + + addDocWithoutUpdatingHistory: wrapWithLock (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=> + # This method should never be called directly, except when importing a project + # from Overleaf. It skips sending updates to the project history, which will break + # the history unless you are making sure it is updated in some other way. + + if not SafePath.isCleanFilename docName + return callback new Errors.InvalidNameError("invalid element name") + + # Put doc in docstore first, so that if it errors, we don't have a doc_id in the project + # which hasn't been created in docstore. + doc = new Doc name: docName + DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, 0, {}, (err, modified, rev) -> + return callback(err) if err? + ProjectEntityMongoUpdateHandler.addDoc project_id, folder_id, doc, (err, result, project) -> + return callback(err) if err? + TpdsUpdateSender.addDoc { + project_id: project_id, + doc_id: doc?._id + path: result?.path?.fileSystem, + project_name: project.name, + rev: 0 + }, (err) -> + return callback(err) if err? + callback(null, doc, folder_id, result?.path?.fileSystem) + + addFileWithoutUpdatingHistory: wrapWithLock (project_id, folder_id, fileName, fsPath, userId, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)-> + # This method should never be called directly, except when importing a project + # from Overleaf. It skips sending updates to the project history, which will break + # the history unless you are making sure it is updated in some other way. + + if not SafePath.isCleanFilename fileName + return callback new Errors.InvalidNameError("invalid element name") + + fileRef = new File name : fileName + FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)-> + if err? + logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3" + return callback(err) + ProjectEntityMongoUpdateHandler.addFile project_id, folder_id, fileRef, (err, result, project) -> + if err? + logger.err err:err, project_id: project_id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project" + return callback(err) + TpdsUpdateSender.addFile {project_id:project_id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) -> + return callback(err) if err? + callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl) + + upsertDoc: wrapWithLock (project_id, folder_id, docName, docLines, source, userId, callback = (err, doc, folder_id, isNewDoc)->)-> + ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) -> + return callback(error) if error? + return callback(new Error("Couldn't find folder")) if !folder? + existingDoc = null + for doc in folder.docs + if doc.name == docName + existingDoc = doc + break + if existingDoc? + DocumentUpdaterHandler.setDocument project_id, existingDoc._id, userId, docLines, source, (err)=> + logger.log project_id:project_id, doc_id:existingDoc._id, "notifying users that the document has been updated" + DocumentUpdaterHandler.flushDocToMongo project_id, existingDoc._id, (err) -> + return callback(err) if err? + callback null, existingDoc, !existingDoc? + else + self.addDoc.withoutLock project_id, folder_id, docName, docLines, userId, (err, doc) -> + return callback(err) if err? + callback null, doc, !existingDoc? + + upsertFile: wrapWithLock (project_id, folder_id, fileName, fsPath, userId, callback = (err, file, isNewFile)->)-> + ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) -> + return callback(error) if error? + return callback(new Error("Couldn't find folder")) if !folder? + existingFile = null + for fileRef in folder.fileRefs + if fileRef.name == fileName + existingFile = fileRef + break + if existingFile? + self.replaceFile.withoutLock project_id, existingFile._id, fsPath, userId, (err) -> + return callback(err) if err? + callback null, existingFile, !existingFile? + else + self.addFile.withoutLock project_id, folder_id, fileName, fsPath, userId, (err, file) -> + return callback(err) if err? + callback null, file, !existingFile? + + upsertDocWithPath: wrapWithLock (project_id, elementPath, docLines, source, userId, callback) -> + docName = path.basename(elementPath) + folderPath = path.dirname(elementPath) + self.mkdirp.withoutLock project_id, folderPath, (err, newFolders, folder) -> + return callback(err) if err? + self.upsertDoc.withoutLock project_id, folder._id, docName, docLines, source, userId, (err, doc, isNewDoc) -> + return callback(err) if err? + callback null, doc, isNewDoc, newFolders, folder + + upsertFileWithPath: wrapWithLock (project_id, elementPath, fsPath, userId, callback) -> + fileName = path.basename(elementPath) + folderPath = path.dirname(elementPath) + self.mkdirp.withoutLock project_id, folderPath, (err, newFolders, folder) -> + return callback(err) if err? + self.upsertFile.withoutLock project_id, folder._id, fileName, fsPath, userId, (err, file, isNewFile) -> + return callback(err) if err? + callback null, file, isNewFile, newFolders, folder + + deleteEntity: wrapWithLock (project_id, entity_id, entityType, userId, callback = (error) ->)-> + logger.log entity_id:entity_id, entityType:entityType, project_id:project_id, "deleting project entity" + if !entityType? + logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id + return callback("No entityType set") + entityType = entityType.toLowerCase() + ProjectEntityMongoUpdateHandler.deleteEntity project_id, entity_id, entityType, (error, entity, path, projectBeforeDeletion) -> + return callback(error) if error? + self._cleanUpEntity projectBeforeDeletion, entity, entityType, path.fileSystem, userId, (error) -> + return callback(error) if error? + TpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:projectBeforeDeletion.name, (error) -> + return callback(error) if error? + callback null, entity_id + + deleteEntityWithPath: wrapWithLock (project_id, path, userId, callback) -> + ProjectLocator.findElementByPath project_id, path, (err, element, type)-> + return callback(err) if err? + return callback(new Errors.NotFoundError("project not found")) if !element? + self.deleteEntity.withoutLock project_id, element._id, type, userId, callback + + mkdirp: wrapWithLock (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)-> + ProjectEntityMongoUpdateHandler.mkdirp project_id, path, callback + + addFolder: wrapWithLock (project_id, parentFolder_id, folderName, callback) -> + if not SafePath.isCleanFilename folderName + return callback new Errors.InvalidNameError("invalid element name") + ProjectEntityMongoUpdateHandler.addFolder project_id, parentFolder_id, folderName, callback + + moveEntity: wrapWithLock (project_id, entity_id, destFolderId, entityType, userId, callback = (error) ->)-> + logger.log {entityType, entity_id, project_id, destFolderId}, "moving entity" + if !entityType? + logger.err {err: "No entityType set", project_id, entity_id} + return callback("No entityType set") + entityType = entityType.toLowerCase() + ProjectEntityMongoUpdateHandler.moveEntity project_id, entity_id, destFolderId, entityType, (err, project_name, startPath, endPath, rev, changes) -> + return callback(err) if err? + TpdsUpdateSender.moveEntity { project_id, project_name, startPath, endPath, rev } + DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback + + renameEntity: wrapWithLock (project_id, entity_id, entityType, newName, userId, callback)-> + if not SafePath.isCleanFilename newName + return callback new Errors.InvalidNameError("invalid element name") + logger.log(entity_id: entity_id, project_id: project_id, ('renaming '+entityType)) + if !entityType? + logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id + return callback("No entityType set") + entityType = entityType.toLowerCase() + + ProjectEntityMongoUpdateHandler.renameEntity project_id, entity_id, entityType, newName, (err, project_name, startPath, endPath, rev, changes) -> + return callback(err) if err? + TpdsUpdateSender.moveEntity({project_id, startPath, endPath, project_name, rev}) + DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback + + _cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) -> + if(entityType.indexOf("file") != -1) + self._cleanUpFile project, entity, path, userId, callback + else if (entityType.indexOf("doc") != -1) + self._cleanUpDoc project, entity, path, userId, callback + else if (entityType.indexOf("folder") != -1) + self._cleanUpFolder project, entity, path, userId, callback + else + callback() + + _cleanUpDoc: (project, doc, path, userId, callback = (error) ->) -> + project_id = project._id.toString() + doc_id = doc._id.toString() + unsetRootDocIfRequired = (callback) => + if project.rootDoc_id? and project.rootDoc_id.toString() == doc_id + @unsetRootDoc project_id, callback + else + callback() + + unsetRootDocIfRequired (error) -> + return callback(error) if error? + self._insertDeletedDocReference project._id, doc, (error) -> + return callback(error) if error? + DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) -> + return callback(error) if error? + DocstoreManager.deleteDoc project_id, doc_id, (error) -> + return callback(error) if error? + changes = oldDocs: [ {doc, path} ] + DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback + + _cleanUpFile: (project, file, path, userId, callback = (error) ->) -> + project_id = project._id.toString() + file_id = file._id.toString() + FileStoreHandler.deleteFile project_id, file_id, (error) -> + return callback(error) if error? + changes = oldFiles: [ {file, path} ] + DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback + + _cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) -> + jobs = [] + for doc in folder.docs + do (doc) -> + docPath = path.join(folderPath, doc.name) + jobs.push (callback) -> self._cleanUpDoc project, doc, docPath, userId, callback + + for file in folder.fileRefs + do (file) -> + filePath = path.join(folderPath, file.name) + jobs.push (callback) -> self._cleanUpFile project, file, filePath, userId, callback + + for childFolder in folder.folders + do (childFolder) -> + folderPath = path.join(folderPath, childFolder.name) + jobs.push (callback) -> self._cleanUpFolder project, childFolder, folderPath, userId, callback + + async.series jobs, callback + + _insertDeletedDocReference: (project_id, doc, callback = (error) ->) -> + Project.update { + _id: project_id + }, { + $push: { + deletedDocs: { + _id: doc._id + name: doc.name + } + } + }, {}, callback diff --git a/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee b/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee index a6f429dca2..d233b5ab68 100644 --- a/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee +++ b/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee @@ -1,4 +1,5 @@ ProjectEntityHandler = require "./ProjectEntityHandler" +ProjectEntityUpdateHandler = require "./ProjectEntityUpdateHandler" Path = require "path" async = require("async") _ = require("underscore") @@ -27,7 +28,7 @@ module.exports = ProjectRootDocManager = async.series jobs, (root_doc_id)-> if root_doc_id? - ProjectEntityHandler.setRootDoc project_id, root_doc_id, callback + ProjectEntityUpdateHandler.setRootDoc project_id, root_doc_id, callback else callback() diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee index 3196458817..d5c22b45ef 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee @@ -1,82 +1,52 @@ _ = require('underscore') -projectLocator = require('../Project/ProjectLocator') -editorController = require('../Editor/EditorController') -logger = require('logger-sharelatex') -Settings = require('settings-sharelatex') -FileTypeManager = require('../Uploads/FileTypeManager') -uuid = require('uuid') fs = require('fs') -LockManager = require("../../infrastructure/LockManager") +logger = require('logger-sharelatex') +uuid = require('uuid') +EditorController = require('../Editor/EditorController') +FileTypeManager = require('../Uploads/FileTypeManager') +Settings = require('settings-sharelatex') module.exports = UpdateMerger = mergeUpdate: (user_id, project_id, path, updateRequest, source, callback = (error) ->)-> logger.log project_id:project_id, path:path, "merging update from tpds" UpdateMerger.p.writeStreamToDisk project_id, updateRequest, (err, fsPath)-> return callback(err) if err? - LockManager.runWithLock project_id, - (cb) => UpdateMerger.mergeUpdateWithoutLock user_id, project_id, path, fsPath, source, cb - (mergeErr) -> - fs.unlink fsPath, (deleteErr) -> - if deleteErr? - logger.err project_id:project_id, fsPath:fsPath, "error deleting file" - callback mergeErr + UpdateMerger._mergeUpdate user_id, project_id, path, fsPath, source, (mergeErr) -> + fs.unlink fsPath, (deleteErr) -> + if deleteErr? + logger.err project_id:project_id, fsPath:fsPath, "error deleting file" + callback mergeErr - mergeUpdateWithoutLock: (user_id, project_id, path, fsPath, source, callback = (error) ->)-> - projectLocator.findElementByPath project_id, path, (err, element)=> - logger.log {project_id, path, fsPath}, "found element by path for merging update from tpds" - elementId = element?._id - FileTypeManager.isBinary path, fsPath, (err, isFile)-> - return callback(err) if err? - if isFile - UpdateMerger.p.processFile project_id, elementId, fsPath, path, source, user_id, callback - else - UpdateMerger.p.processDoc project_id, elementId, user_id, fsPath, path, source, callback + _mergeUpdate: (user_id, project_id, path, fsPath, source, callback = (error) ->)-> + FileTypeManager.isBinary path, fsPath, (err, isFile)-> + return callback(err) if err? + if isFile + UpdateMerger.p.processFile project_id, fsPath, path, source, user_id, callback + else + UpdateMerger.p.processDoc project_id, user_id, fsPath, path, source, callback - deleteUpdate: (user_id, project_id, path, source, callback)-> - LockManager.runWithLock project_id, - (cb) => UpdateMerger.deleteUpdateWithoutLock(user_id, project_id, path, source, cb) - (err, doc) -> - logger.log project_id:project_id, path:path, "finished processing update to delete entity from tpds" - callback() - - deleteUpdateWithoutLock: (user_id, project_id, path, source, callback)-> - projectLocator.findElementByPath project_id, path, (err, element, type)-> - if err? || !element? - logger.log element:element, project_id:project_id, path:path, "could not find entity for deleting, assuming it was already deleted" - return callback() - logger.log project_id:project_id, path:path, type:type, element:element, "processing update to delete entity from tpds" - editorController.deleteEntityWithoutLock project_id, element._id, type, source, user_id, callback + deleteUpdate: (user_id, project_id, path, source, callback = () ->)-> + EditorController.deleteEntityWithPath project_id, path, source, user_id, () -> + logger.log project_id:project_id, path:path, "finished processing update to delete entity from tpds" + callback() p: - processDoc: (project_id, doc_id, user_id, fsPath, path, source, callback)-> - readFileIntoTextArray fsPath, (err, docLines)-> + processDoc: (project_id, user_id, fsPath, path, source, callback)-> + UpdateMerger.p.readFileIntoTextArray fsPath, (err, docLines)-> if err? - logger.err project_id:project_id, doc_id:doc_id, fsPath:fsPath, "error reading file into text array for process doc update" + logger.err project_id:project_id, "error reading file into text array for process doc update" return callback(err) - logger.log docLines:docLines, doc_id:doc_id, project_id:project_id, "processing doc update from tpds" - if doc_id? - editorController.setDoc project_id, doc_id, user_id, docLines, source, callback - else - setupNewEntity project_id, path, (err, folder, fileName)-> - if err? - logger.err err:err, project_id:project_id, doc_id:doc_id, path:path, "error processing file" - return callback(err) - editorController.addDocWithoutLock project_id, folder._id, fileName, docLines, source, user_id, callback + logger.log docLines:docLines, "processing doc update from tpds" + EditorController.upsertDocWithPath project_id, path, docLines, source, user_id, (err) -> + logger.log project_id:project_id, "completed processing file update from tpds" + callback(err) - processFile: (project_id, file_id, fsPath, path, source, user_id, callback)-> - finish = (err)-> - logger.log project_id:project_id, file_id:file_id, path:path, "completed processing file update from tpds" + processFile: (project_id, fsPath, path, source, user_id, callback)-> + logger.log project_id:project_id, "processing file update from tpds" + EditorController.upsertFileWithPath project_id, path, fsPath, source, user_id, (err) -> + logger.log project_id:project_id, "completed processing file update from tpds" callback(err) - logger.log project_id:project_id, file_id:file_id, path:path, "processing file update from tpds" - setupNewEntity project_id, path, (err, folder, fileName) => - if err? - logger.err err:err, project_id:project_id, file_id:file_id, path:path, "error processing file" - return callback(err) - else if file_id? - editorController.replaceFileWithoutLock project_id, file_id, fsPath, source, user_id, finish - else - editorController.addFileWithoutLock project_id, folder?._id, fileName, fsPath, source, user_id, finish writeStreamToDisk: (project_id, stream, callback = (err, fsPath)->)-> dumpPath = "#{Settings.path.dumpFolder}/#{project_id}_#{uuid.v4()}" @@ -97,18 +67,10 @@ module.exports = UpdateMerger = logger.log {project_id, dumpPath}, "tpds update write stream finished" callback null, dumpPath -readFileIntoTextArray = (path, callback)-> - fs.readFile path, "utf8", (error, content = "") -> - if error? - logger.err path:path, "error reading file into text array" - return callback(err) - lines = content.split(/\r\n|\n|\r/) - callback error, lines - - -setupNewEntity = (project_id, path, callback)-> - lastIndexOfSlash = path.lastIndexOf("/") - fileName = path[lastIndexOfSlash+1 .. -1] - folderPath = path[0 .. lastIndexOfSlash] - editorController.mkdirpWithoutLock project_id, folderPath, (err, newFolders, lastFolder)-> - callback err, lastFolder, fileName + readFileIntoTextArray: (path, callback)-> + fs.readFile path, "utf8", (error, content = "") -> + if error? + logger.err path:path, "error reading file into text array" + return callback(error) + lines = content.split(/\r\n|\n|\r/) + callback error, lines diff --git a/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee b/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee index 7d0d69871c..778610f6ef 100644 --- a/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee +++ b/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee @@ -3,7 +3,6 @@ fs = require "fs" _ = require "underscore" FileTypeManager = require "./FileTypeManager" EditorController = require "../Editor/EditorController" -ProjectLocator = require "../Project/ProjectLocator" logger = require("logger-sharelatex") module.exports = FileSystemImportManager = @@ -17,20 +16,9 @@ module.exports = FileSystemImportManager = content = content.replace(/\r/g, "") lines = content.split("\n") if replace - ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) -> - return callback(error) if error? - return callback(new Error("Couldn't find folder")) if !folder? - existingDoc = null - for doc in folder.docs - if doc.name == name - existingDoc = doc - break - if existingDoc? - EditorController.setDoc project_id, existingDoc._id, user_id, lines, "upload", callback - else - EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", user_id, callback + EditorController.upsertDoc project_id, folder_id, name, lines, "upload", user_id, callback else - EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", user_id, callback + EditorController.addDoc project_id, folder_id, name, lines, "upload", user_id, callback addFile: (user_id, project_id, folder_id, name, path, replace, callback = (error, file)-> )-> FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)-> @@ -38,28 +26,17 @@ module.exports = FileSystemImportManager = logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, name:name, path:path, "add file is from symlink, stopping insert" return callback("path is symlink") - if !replace - EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", user_id, callback + if replace + EditorController.upsertFile project_id, folder_id, name, path, "upload", user_id, callback else - ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) -> - return callback(error) if error? - return callback(new Error("Couldn't find folder")) if !folder? - existingFile = null - for fileRef in folder.fileRefs - if fileRef.name == name - existingFile = fileRef - break - if existingFile? - EditorController.replaceFileWithoutLock project_id, existingFile._id, path, "upload", user_id, callback - else - EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", user_id, callback + EditorController.addFile project_id, folder_id, name, path, "upload", user_id, callback addFolder: (user_id, project_id, folder_id, name, path, replace, callback = (error)-> ) -> FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)-> if !isSafe logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, path:path, "add folder is from symlink, stopping insert" return callback("path is symlink") - EditorController.addFolderWithoutLock project_id, folder_id, name, "upload", (error, new_folder) => + EditorController.addFolder project_id, folder_id, name, "upload", (error, new_folder) => return callback(error) if error? FileSystemImportManager.addFolderContents user_id, project_id, new_folder._id, path, replace, (error) -> return callback(error) if error? diff --git a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee index 1d81fdb831..c7ac0f4229 100644 --- a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee +++ b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee @@ -5,7 +5,6 @@ Path = require "path" FileSystemImportManager = require "./FileSystemImportManager" ProjectUploadManager = require "./ProjectUploadManager" AuthenticationController = require('../Authentication/AuthenticationController') -LockManager = require("../../infrastructure/LockManager") module.exports = ProjectUploadController = uploadProject: (req, res, next) -> @@ -39,20 +38,18 @@ module.exports = ProjectUploadController = logger.log folder_id:folder_id, project_id:project_id, "getting upload file request" user_id = AuthenticationController.getLoggedInUserId(req) - LockManager.runWithLock project_id, - (cb) -> FileSystemImportManager.addEntity user_id, project_id, folder_id, name, path, true, cb - (error, entity) -> - fs.unlink path, -> - timer.done() - if error? - logger.error - err: error, project_id: project_id, file_path: path, - file_name: name, folder_id: folder_id, - "error uploading file" - res.send success: false - else - logger.log - project_id: project_id, file_path: path, file_name: name, folder_id: folder_id - "uploaded file" - res.send success: true, entity_id: entity?._id, entity_type: entity?.type + FileSystemImportManager.addEntity user_id, project_id, folder_id, name, path, true, (error, entity) -> + fs.unlink path, -> + timer.done() + if error? + logger.error + err: error, project_id: project_id, file_path: path, + file_name: name, folder_id: folder_id, + "error uploading file" + res.send success: false + else + logger.log + project_id: project_id, file_path: path, file_name: name, folder_id: folder_id + "uploaded file" + res.send success: true, entity_id: entity?._id, entity_type: entity?.type diff --git a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee index e037deb5a3..6a303f3fc7 100644 --- a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee @@ -288,6 +288,74 @@ describe "ProjectStructureChanges", -> done() + describe "renaming entities", -> + beforeEach () -> + MockDocUpdaterApi.clearProjectStructureUpdates() + + it "should version renaming a doc", (done) -> + @owner.request.post { + uri: "project/#{@example_project_id}/Doc/#{@example_doc_id}/rename", + json: + name: 'new_renamed.tex' + }, (error, res, body) => + throw error if error? + if res.statusCode < 200 || res.statusCode >= 300 + throw new Error("failed to move doc #{res.statusCode}") + + updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates + expect(updates.length).to.equal(1) + update = updates[0] + expect(update.userId).to.equal(@owner._id) + expect(update.pathname).to.equal("/bar/foo/new.tex") + expect(update.newPathname).to.equal("/bar/foo/new_renamed.tex") + + done() + + it "should version renaming a file", (done) -> + @owner.request.post { + uri: "project/#{@example_project_id}/File/#{@example_file_id}/rename", + json: + name: '1pixel_renamed.png' + }, (error, res, body) => + throw error if error? + if res.statusCode < 200 || res.statusCode >= 300 + throw new Error("failed to move file #{res.statusCode}") + + updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates + expect(updates.length).to.equal(1) + update = updates[0] + expect(update.userId).to.equal(@owner._id) + expect(update.pathname).to.equal("/bar/foo/1pixel.png") + expect(update.newPathname).to.equal("/bar/foo/1pixel_renamed.png") + + done() + + it "should version renaming a folder", (done) -> + @owner.request.post { + uri: "project/#{@example_project_id}/Folder/#{@example_folder_id_1}/rename", + json: + name: 'foo_renamed' + }, (error, res, body) => + throw error if error? + if res.statusCode < 200 || res.statusCode >= 300 + throw new Error("failed to move folder #{res.statusCode}") + + updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates + expect(updates.length).to.equal(1) + update = updates[0] + expect(update.userId).to.equal(@owner._id) + expect(update.pathname).to.equal("/bar/foo/new_renamed.tex") + expect(update.newPathname).to.equal("/bar/foo_renamed/new_renamed.tex") + + updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates + expect(updates.length).to.equal(1) + update = updates[0] + expect(update.userId).to.equal(@owner._id) + expect(update.pathname).to.equal("/bar/foo/1pixel_renamed.png") + expect(update.newPathname).to.equal("/bar/foo_renamed/1pixel_renamed.png") + + done() + describe "deleting entities", -> beforeEach () -> MockDocUpdaterApi.clearProjectStructureUpdates() @@ -304,14 +372,14 @@ describe "ProjectStructureChanges", -> expect(updates.length).to.equal(1) update = updates[0] expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/bar/foo/new.tex") + expect(update.pathname).to.equal("/bar/foo_renamed/new_renamed.tex") expect(update.newPathname).to.equal("") updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates expect(updates.length).to.equal(1) update = updates[0] expect(update.userId).to.equal(@owner._id) - expect(update.pathname).to.equal("/bar/foo/1pixel.png") + expect(update.pathname).to.equal("/bar/foo_renamed/1pixel_renamed.png") expect(update.newPathname).to.equal("") done() diff --git a/services/web/test/unit/coffee/Documents/DocumentControllerTests.coffee b/services/web/test/unit/coffee/Documents/DocumentControllerTests.coffee index 1d4cef928f..30c12f48df 100644 --- a/services/web/test/unit/coffee/Documents/DocumentControllerTests.coffee +++ b/services/web/test/unit/coffee/Documents/DocumentControllerTests.coffee @@ -16,6 +16,7 @@ describe "DocumentController", -> log:-> err:-> "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} + "../Project/ProjectEntityUpdateHandler": @ProjectEntityUpdateHandler = {} @res = new MockResponse() @req = new MockRequest() @next = sinon.stub() @@ -68,7 +69,7 @@ describe "DocumentController", -> describe "when the document exists", -> beforeEach -> - @ProjectEntityHandler.updateDocLines = sinon.stub().yields() + @ProjectEntityUpdateHandler.updateDocLines = sinon.stub().yields() @req.body = lines: @doc_lines version: @version @@ -76,7 +77,7 @@ describe "DocumentController", -> @DocumentController.setDocument(@req, @res, @next) it "should update the document in Mongo", -> - @ProjectEntityHandler.updateDocLines + @ProjectEntityUpdateHandler.updateDocLines .calledWith(@project_id, @doc_id, @doc_lines, @version, @ranges) .should.equal true @@ -85,7 +86,7 @@ describe "DocumentController", -> describe "when the document doesn't exist", -> beforeEach -> - @ProjectEntityHandler.updateDocLines = sinon.stub().yields(new Errors.NotFoundError("document does not exist")) + @ProjectEntityUpdateHandler.updateDocLines = sinon.stub().yields(new Errors.NotFoundError("document does not exist")) @req.body = lines: @doc_lines @DocumentController.setDocument(@req, @res, @next) diff --git a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee index ace7fe4d19..4e928732ae 100644 --- a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee +++ b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee @@ -10,373 +10,251 @@ assert = require('assert') describe "EditorController", -> beforeEach -> @project_id = "test-project-id" - @project = - _id: @project_id - owner_ref:{_id:"something"} - - - @doc_id = "test-doc-id" @source = "dropbox" - @user = - _id: @user_id = "user-id" - projects: {} + @doc = _id: @doc_id = "test-doc-id" + @docName = "doc.tex" + @docLines = ["1234","dskl"] + @file = _id: @file_id ="dasdkjk" + @fileName = "file.png" + @fsPath = "/folder/file.png" - @rooms = {} - @io = - sockets : - clients : (room_id) => - @rooms[room_id] - @DocumentUpdaterHandler = {} - @ProjectOptionsHandler = - setCompiler : sinon.spy() - setSpellCheckLanguage: sinon.spy() - @ProjectEntityHandler = - flushProjectToThirdPartyDataStore:sinon.stub() - @Project = - findPopulatedById: sinon.stub().callsArgWith(1, null, @project) - @client = new MockClient() + @folder_id = "123ksajdn" + @folder = _id: @folder_id + @folderName = "folder" - @settings = - apis:{thirdPartyDataStore:{emptyProjectFlushDelayMiliseconds:0.5}} - redis: web:{} - @dropboxProjectLinker = {} @callback = sinon.stub() - @ProjectDetailsHandler = - setProjectDescription:sinon.stub() - @CollaboratorsHandler = - removeUserFromProject: sinon.stub().callsArgWith(2) - addUserToProject: sinon.stub().callsArgWith(3) - @ProjectDeleter = - deleteProject: sinon.stub() - @LockManager = - runWithLock : sinon.spy((key, runner, callback) -> runner(callback)) + @EditorController = SandboxedModule.require modulePath, requires: - "../../infrastructure/Server" : io : @io - '../Project/ProjectEntityHandler' : @ProjectEntityHandler - '../Project/ProjectOptionsHandler' : @ProjectOptionsHandler - '../Project/ProjectDetailsHandler': @ProjectDetailsHandler - '../Project/ProjectDeleter' : @ProjectDeleter - '../Collaborators/CollaboratorsHandler': @CollaboratorsHandler - '../DocumentUpdater/DocumentUpdaterHandler' : @DocumentUpdaterHandler - '../../models/Project' : Project: @Project - "settings-sharelatex":@settings - '../Dropbox/DropboxProjectLinker':@dropboxProjectLinker - './EditorRealTimeController':@EditorRealTimeController = {} - "metrics-sharelatex": @Metrics = { inc: sinon.stub() } - "../TrackChanges/TrackChangesManager": @TrackChangesManager = {} - "../../infrastructure/LockManager":@LockManager - 'redis-sharelatex':createClient:-> auth:-> + '../Project/ProjectEntityUpdateHandler' : @ProjectEntityUpdateHandler = {} + '../Project/ProjectOptionsHandler' : @ProjectOptionsHandler = + setCompiler: sinon.stub().yields() + setSpellCheckLanguage: sinon.stub().yields() + '../Project/ProjectDetailsHandler': @ProjectDetailsHandler = + setProjectDescription: sinon.stub().yields() + renameProject: sinon.stub().yields() + setPublicAccessLevel: sinon.stub().yields() + '../Project/ProjectDeleter' : @ProjectDeleter = {} + '../DocumentUpdater/DocumentUpdaterHandler' : @DocumentUpdaterHandler = + flushDocToMongo: sinon.stub().yields() + setDocument: sinon.stub().yields() + './EditorRealTimeController':@EditorRealTimeController = + emitToRoom: sinon.stub() + "metrics-sharelatex": @Metrics = inc: sinon.stub() "logger-sharelatex": @logger = log: sinon.stub() err: sinon.stub() - describe "updating compiler used for project", -> - it "should send the new compiler and project id to the project options handler", (done)-> - compiler = "latex" - @EditorRealTimeController.emitToRoom = sinon.stub() - @EditorController.setCompiler @project_id, compiler, (err) => - @ProjectOptionsHandler.setCompiler.calledWith(@project_id, compiler).should.equal true - @EditorRealTimeController.emitToRoom.calledWith(@project_id, "compilerUpdated", compiler).should.equal true - done() - @ProjectOptionsHandler.setCompiler.args[0][2]() - - - describe "updating language code used for project", -> - it "should send the new languageCode and project id to the project options handler", (done)-> - languageCode = "fr" - @EditorRealTimeController.emitToRoom = sinon.stub() - @EditorController.setSpellCheckLanguage @project_id, languageCode, (err) => - @ProjectOptionsHandler.setSpellCheckLanguage.calledWith(@project_id, languageCode).should.equal true - @EditorRealTimeController.emitToRoom.calledWith(@project_id, "spellCheckLanguageUpdated", languageCode).should.equal true - done() - @ProjectOptionsHandler.setSpellCheckLanguage.args[0][2]() - - - describe 'setDoc', -> + describe 'addDoc', -> beforeEach -> - @docLines = ["foo", "bar"] - @DocumentUpdaterHandler.flushDocToMongo = sinon.stub().callsArg(2) - @DocumentUpdaterHandler.setDocument = sinon.stub().callsArg(5) + @ProjectEntityUpdateHandler.addDoc = sinon.stub().yields(null, @doc, @folder_id) + @EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, @callback - it 'should send the document to the documentUpdaterHandler', (done)-> - @DocumentUpdaterHandler.setDocument = sinon.stub().withArgs(@project_id, @doc_id, @user_id, @docLines, @source).callsArg(5) - @EditorController.setDoc @project_id, @doc_id, @user_id, @docLines, @source, (err)-> - done() + it 'should add the doc using the project entity handler', -> + @ProjectEntityUpdateHandler.addDoc + .calledWith(@project_id, @folder_id, @docName, @docLines) + .should.equal true - it 'should send the new doc lines to the doucment updater', (done)-> - @DocumentUpdaterHandler.setDocument = -> - mock = sinon.mock(@DocumentUpdaterHandler).expects("setDocument").withArgs(@project_id, @doc_id, @user_id, @docLines, @source).once().callsArg(5) + it 'should send the update out to the users in the project', -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "reciveNewDoc", @folder_id, @doc, @source) + .should.equal true - @EditorController.setDoc @project_id, @doc_id, @user_id, @docLines, @source, (err)=> - mock.verify() - done() + it 'calls the callback', -> + @callback.calledWith(null, @doc).should.equal true - it 'should flush the doc to mongo', (done)-> - @EditorController.setDoc @project_id, @doc_id, @user_id, @docLines, @source, (err)=> - @DocumentUpdaterHandler.flushDocToMongo.calledWith(@project_id, @doc_id).should.equal true - done() - - - describe 'addDocWithoutLock', -> + describe 'addFile', -> beforeEach -> - @ProjectEntityHandler.addDoc = ()-> - @EditorRealTimeController.emitToRoom = sinon.stub() - @project_id = "12dsankj" - @folder_id = "213kjd" - @doc = {_id:"123ds"} - @folder_id = "123ksajdn" - @docName = "doc.tex" - @docLines = ["1234","dskl"] + @ProjectEntityUpdateHandler.addFile = sinon.stub().yields(null, @file, @folder_id) + @EditorController.addFile @project_id, @folder_id, @fileName, @fsPath, @source, @user_id, @callback - it 'should add the doc using the project entity handler', (done)-> - mock = sinon.mock(@ProjectEntityHandler).expects("addDoc").withArgs(@project_id, @folder_id, @docName, @docLines).callsArg(5) + it 'should add the folder using the project entity handler', -> + @ProjectEntityUpdateHandler.addFile + .calledWith(@project_id, @folder_id, @fileName, @fsPath, @user_id) + .should.equal true - @EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, @user_id, -> - mock.verify() - done() + it 'should send the update of a new folder out to the users in the project', -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "reciveNewFile", @folder_id, @file, @source) + .should.equal true - it 'should send the update out to the users in the project', (done)-> - @ProjectEntityHandler.addDoc = sinon.stub().callsArgWith(5, null, @doc, @folder_id) + it 'calls the callback', -> + @callback.calledWith(null, @file).should.equal true - @EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, @user_id, => + describe 'upsertDoc', -> + beforeEach -> + @ProjectEntityUpdateHandler.upsertDoc = sinon.stub().yields(null, @doc, false) + @EditorController.upsertDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, @callback + + it 'upserts the doc using the project entity handler', -> + @ProjectEntityUpdateHandler.upsertDoc + .calledWith(@project_id, @folder_id, @docName, @docLines, @source) + .should.equal true + + it 'returns the doc', -> + @callback.calledWith(null, @doc).should.equal true + + describe 'doc does not exist', -> + beforeEach -> + @ProjectEntityUpdateHandler.upsertDoc = sinon.stub().yields(null, @doc, true) + @EditorController.upsertDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, @callback + + it 'sends an update out to users in the project', -> @EditorRealTimeController.emitToRoom .calledWith(@project_id, "reciveNewDoc", @folder_id, @doc, @source) .should.equal true - done() - - it 'should return the doc to the callback', (done) -> - @ProjectEntityHandler.addDoc = sinon.stub().callsArgWith(5, null, @doc, @folder_id) - @EditorController.addDocWithoutLock @project_id, @folder_id, @docName, @docLines, @source, @user_id, (error, doc) => - doc.should.equal @doc - done() - - describe "addDoc", -> + describe 'upsertFile', -> beforeEach -> - @EditorController.addDocWithoutLock = sinon.stub().callsArgWith(6) + @ProjectEntityUpdateHandler.upsertFile = sinon.stub().yields(null, @file, false) + @EditorController.upsertFile @project_id, @folder_id, @fileName, @fsPath, @source, @user_id, @callback - it "should call addDocWithoutLock", (done)-> - @EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, => - @EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @docName, @docLines, @source, @user_id).should.equal true - done() + it 'upserts the file using the project entity handler', -> + @ProjectEntityUpdateHandler.upsertFile + .calledWith(@project_id, @folder_id, @fileName, @fsPath, @user_id) + .should.equal true - it "should take the lock", (done)-> - @EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, => - @LockManager.runWithLock.calledWith(@project_id).should.equal true - done() + it 'returns the file', -> + @callback.calledWith(null, @file).should.equal true - it "should propogate up any errors", (done)-> - @LockManager.runWithLock = sinon.stub().callsArgWith(2, "timed out") - @EditorController.addDoc @project_id, @folder_id, @docName, @docLines, @source, @user_id, (err) => - expect(err).to.exist - err.should.equal "timed out" - done() + describe 'file does not exist', -> + beforeEach -> + @ProjectEntityUpdateHandler.upsertFile = sinon.stub().yields(null, @file, true) + @EditorController.upsertFile @project_id, @folder_id, @fileName, @fsPath, @source, @user_id, @callback - describe 'addFileWithoutLock:', -> - beforeEach -> - @ProjectEntityHandler.addFile = -> - @EditorRealTimeController.emitToRoom = sinon.stub() - @project_id = "12dsankj" - @folder_id = "213kjd" - @fileName = "file.png" - @folder_id = "123ksajdn" - @file = {_id:"dasdkjk"} - @stream = new ArrayBuffer() - - it 'should add the folder using the project entity handler', (done)-> - @ProjectEntityHandler.addFile = sinon.stub().callsArgWith(5) - @EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, @user_id, => - @ProjectEntityHandler.addFile.calledWith(@project_id, @folder_id, @fileName, @stream, @user_id).should.equal true - done() - - it 'should send the update of a new folder out to the users in the project', (done)-> - @ProjectEntityHandler.addFile = sinon.stub().callsArgWith(5, null, @file, @folder_id) - - @EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, @user_id, => + it 'should send the update out to users in the project', -> @EditorRealTimeController.emitToRoom .calledWith(@project_id, "reciveNewFile", @folder_id, @file, @source) .should.equal true - done() - - it "should return the file in the callback", (done) -> - @ProjectEntityHandler.addFile = sinon.stub().callsArgWith(5, null, @file, @folder_id) - @EditorController.addFileWithoutLock @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) => - file.should.equal @file - done() - - - describe "addFile", -> + describe "upsertDocWithPath", -> beforeEach -> - @EditorController.addFileWithoutLock = sinon.stub().callsArgWith(6) + @docPath = '/folder/doc' - it "should call addFileWithoutLock", (done)-> - @EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) => - @EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @fileName, @stream, @source, @user_id).should.equal true - done() + @ProjectEntityUpdateHandler.upsertDocWithPath = sinon.stub().yields(null, @doc, false, [], @folder) + @EditorController.upsertDocWithPath @project_id, @docPath, @docLines, @source, @user_id, @callback - it "should take the lock", (done)-> - @EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) => - @LockManager.runWithLock.calledWith(@project_id).should.equal true - done() + it 'upserts the doc using the project entity handler', -> + @ProjectEntityUpdateHandler.upsertDocWithPath + .calledWith(@project_id, @docPath, @docLines, @source) + .should.equal true - it "should propogate up any errors", (done)-> - @LockManager.runWithLock = sinon.stub().callsArgWith(2, "timed out") - @EditorController.addFile @project_id, @folder_id, @fileName, @stream, @source, @user_id, (error, file) => - expect(error).to.exist - error.should.equal "timed out" - done() + describe 'doc does not exist', -> + beforeEach -> + @ProjectEntityUpdateHandler.upsertDocWithPath = sinon.stub().yields(null, @doc, true, [], @folder) + @EditorController.upsertDocWithPath @project_id, @docPath, @docLines, @source, @user_id, @callback - describe "replaceFileWithoutLock", -> - beforeEach -> - @project_id = "12dsankj" - @file_id = "file_id_here" - @fsPath = "/folder/file.png" - - it 'should send the replace file message to the editor controller', (done)-> - @ProjectEntityHandler.replaceFile = sinon.stub().callsArgWith(4) - @EditorController.replaceFileWithoutLock @project_id, @file_id, @fsPath, @source, @user_id, => - @ProjectEntityHandler.replaceFile - .calledWith(@project_id, @file_id, @fsPath, @user_id) - .should.equal true - done() - - describe 'addFolderWithoutLock :', -> - beforeEach -> - @ProjectEntityHandler.addFolder = -> - @EditorRealTimeController.emitToRoom = sinon.stub() - @project_id = "12dsankj" - @folder_id = "213kjd" - @folderName = "folder" - @folder = {_id:"123ds"} - - it 'should add the folder using the project entity handler', (done)-> - mock = sinon.mock(@ProjectEntityHandler).expects("addFolder").withArgs(@project_id, @folder_id, @folderName).callsArg(3) - - @EditorController.addFolderWithoutLock @project_id, @folder_id, @folderName, @source, -> - mock.verify() - done() - - it 'should notifyProjectUsersOfNewFolder', (done)-> - @ProjectEntityHandler.addFolder = (project_id, folder_id, folderName, callback)=> callback(null, @folder, @folder_id) - mock = sinon.mock(@EditorController.p).expects('notifyProjectUsersOfNewFolder').withArgs(@project_id, @folder_id, @folder).callsArg(3) - - @EditorController.addFolderWithoutLock @project_id, @folder_id, @folderName, @source, -> - mock.verify() - done() - - it 'notifyProjectUsersOfNewFolder should send update out to all users', (done)-> - @EditorController.p.notifyProjectUsersOfNewFolder @project_id, @folder_id, @folder, => + it 'should send the update for the doc out to users in the project', -> @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "reciveNewFolder", @folder_id, @folder) + .calledWith(@project_id, "reciveNewDoc", @folder_id, @doc, @source) .should.equal true - done() - - it 'should return the folder in the callback', (done) -> - @ProjectEntityHandler.addFolder = (project_id, folder_id, folderName, callback)=> callback(null, @folder, @folder_id) - @EditorController.addFolderWithoutLock @project_id, @folder_id, @folderName, @source, (error, folder) => - folder.should.equal @folder - done() + describe 'folders required for doc do not exist', -> + beforeEach -> + folders = [ + @folderA = { _id: 2, parentFolder_id: 1} + @folderB = { _id: 3, parentFolder_id: 2} + ] + @ProjectEntityUpdateHandler.upsertDocWithPath = sinon.stub().yields(null, @doc, true, folders, @folderB) + @EditorController.upsertDocWithPath @project_id, @docPath, @docLines, @source, @user_id, @callback - describe "addFolder", -> + it 'should send the update for each folder to users in the project', -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "reciveNewFolder", @folderA.parentFolder_id, @folderA) + .should.equal true + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "reciveNewFolder", @folderB.parentFolder_id, @folderB) + .should.equal true + + describe "upsertFileWithPath", -> beforeEach -> - @EditorController.addFolderWithoutLock = sinon.stub().callsArgWith(4) + @filePath = '/folder/file' - it "should call addFolderWithoutLock", (done)-> - @EditorController.addFolder @project_id, @folder_id, @folderName, @source, (error, file) => - @EditorController.addFolderWithoutLock.calledWith(@project_id, @folder_id, @folderName, @source).should.equal true - done() + @ProjectEntityUpdateHandler.upsertFileWithPath = sinon.stub().yields(null, @file, false, [], @folder) + @EditorController.upsertFileWithPath @project_id, @filePath, @fsPath, @source, @user_id, @callback - it "should take the lock", (done)-> - @EditorController.addFolder @project_id, @folder_id, @folderName, @source, (error, file) => - @LockManager.runWithLock.calledWith(@project_id).should.equal true - done() + it 'upserts the file using the project entity handler', -> + @ProjectEntityUpdateHandler.upsertFileWithPath + .calledWith(@project_id, @filePath, @fsPath) + .should.equal true - it "should propogate up any errors", (done)-> - @LockManager.runWithLock = sinon.stub().callsArgWith(2, "timed out") - @EditorController.addFolder @project_id, @folder_id, @folderName, @source, (err, file) => - expect(err).to.exist - err.should.equal "timed out" - done() + describe 'file does not exist', -> + beforeEach -> + @ProjectEntityUpdateHandler.upsertFileWithPath = sinon.stub().yields(null, @file, true, [], @folder) + @EditorController.upsertFileWithPath @project_id, @filePath, @fsPath, @source, @user_id, @callback + it 'should send the update for the file out to users in the project', -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "reciveNewFile", @folder_id, @file, @source) + .should.equal true - describe 'mkdirpWithoutLock :', -> + describe 'folders required for file do not exist', -> + beforeEach -> + folders = [ + @folderA = { _id: 2, parentFolder_id: 1} + @folderB = { _id: 3, parentFolder_id: 2} + ] + @ProjectEntityUpdateHandler.upsertFileWithPath = sinon.stub().yields(null, @file, true, folders, @folderB) + @EditorController.upsertFileWithPath @project_id, @filePath, @fsPath, @source, @user_id, @callback - it 'should make the dirs and notifyProjectUsersOfNewFolder', (done)-> - path = "folder1/folder2" - @folder1 = {_id:"folder_1_id_here"} - @folder2 = {_id:"folder_2_id_here", parentFolder_id:@folder1._id} - @folder3 = {_id:"folder_3_id_here", parentFolder_id:@folder2._id} + it 'should send the update for each folder to users in the project', -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "reciveNewFolder", @folderA.parentFolder_id, @folderA) + .should.equal true + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "reciveNewFolder", @folderB.parentFolder_id, @folderB) + .should.equal true - @ProjectEntityHandler.mkdirp = sinon.stub().withArgs(@project_id, path).callsArgWith(2, null, [@folder1, @folder2, @folder3], @folder3) + describe 'addFolder', -> + beforeEach -> + @EditorController._notifyProjectUsersOfNewFolder = sinon.stub().yields() + @ProjectEntityUpdateHandler.addFolder = sinon.stub().yields(null, @folder, @folder_id) + @EditorController.addFolder @project_id, @folder_id, @folderName, @source, @callback - @EditorController.p.notifyProjectUsersOfNewFolder = sinon.stub().callsArg(3) + it 'should add the folder using the project entity handler', -> + @ProjectEntityUpdateHandler.addFolder + .calledWith(@project_id, @folder_id, @folderName) + .should.equal true - @EditorController.mkdirpWithoutLock @project_id, path, (err, newFolders, lastFolder)=> - @EditorController.p.notifyProjectUsersOfNewFolder.calledWith(@project_id, @folder1._id, @folder2).should.equal true - @EditorController.p.notifyProjectUsersOfNewFolder.calledWith(@project_id, @folder2._id, @folder3).should.equal true - newFolders.should.deep.equal [@folder1, @folder2, @folder3] - lastFolder.should.equal @folder3 - done() + it 'should notifyProjectUsersOfNewFolder', -> + @EditorController._notifyProjectUsersOfNewFolder + .calledWith(@project_id, @folder_id, @folder) + it 'should return the folder in the callback', -> + @callback.calledWith(null, @folder).should.equal true - describe "mkdirp", -> + describe 'mkdirp', -> beforeEach -> @path = "folder1/folder2" - @EditorController.mkdirpWithoutLock = sinon.stub().callsArgWith(2) + @folders = [ + @folderA = { _id: 2, parentFolder_id: 1} + @folderB = { _id: 3, parentFolder_id: 2} + ] + @EditorController._notifyProjectUsersOfNewFolders = sinon.stub().yields() + @ProjectEntityUpdateHandler.mkdirp = sinon.stub().yields(null, @folders, @folder) + @EditorController.mkdirp @project_id, @path, @callback - it "should call mkdirpWithoutLock", (done)-> - @EditorController.mkdirp @project_id, @path, (error, file) => - @EditorController.mkdirpWithoutLock.calledWith(@project_id, @path).should.equal true - done() + it 'should create the folder using the project entity handler', -> + @ProjectEntityUpdateHandler.mkdirp + .calledWith(@project_id, @path) + .should.equal true - it "should take the lock", (done)-> - @EditorController.mkdirp @project_id, @path, (error, file) => - @LockManager.runWithLock.calledWith(@project_id).should.equal true - done() + it 'should notifyProjectUsersOfNewFolder', -> + @EditorController._notifyProjectUsersOfNewFolders + .calledWith(@project_id, @folders) - it "should propogate up any errors", (done)-> - @LockManager.runWithLock = sinon.stub().callsArgWith(2, "timed out") - @EditorController.mkdirp @project_id, @path, (err, file) => - expect(err).to.exist - err.should.equal "timed out" - done() + it 'should return the folder in the callback', -> + @callback.calledWith(null, @folders, @folder).should.equal true - describe "deleteEntity", -> + describe 'deleteEntity', -> beforeEach -> - @EditorController.deleteEntityWithoutLock = sinon.stub().callsArgWith(5) - - it "should call deleteEntityWithoutLock", (done)-> - @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, => - @EditorController.deleteEntityWithoutLock - .calledWith(@project_id, @entity_id, @type, @source, @user_id) - .should.equal true - done() - - it "should take the lock", (done)-> - @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, => - @LockManager.runWithLock.calledWith(@project_id).should.equal true - done() - - it "should propogate up any errors", (done)-> - @LockManager.runWithLock = sinon.stub().callsArgWith(2, "timed out") - @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, (error) => - expect(error).to.exist - error.should.equal "timed out" - done() - - describe 'deleteEntityWithoutLock', -> - beforeEach (done) -> @entity_id = "entity_id_here" @type = "doc" - @EditorRealTimeController.emitToRoom = sinon.stub() - @ProjectEntityHandler.deleteEntity = sinon.stub().callsArg(4) - @EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, @user_id, done + @ProjectEntityUpdateHandler.deleteEntity = sinon.stub().yields() + @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, @callback it 'should delete the folder using the project entity handler', -> - @ProjectEntityHandler.deleteEntity + @ProjectEntityUpdateHandler.deleteEntity .calledWith(@project_id, @entity_id, @type, @user_id) .should.equal.true @@ -385,9 +263,25 @@ describe "EditorController", -> .calledWith(@project_id, "removeEntity", @entity_id, @source) .should.equal true + describe "deleteEntityWithPath", -> + beforeEach () -> + @entity_id = "entity_id_here" + @ProjectEntityUpdateHandler.deleteEntityWithPath = sinon.stub().yields(null, @entity_id) + @path = "folder1/folder2" + @EditorController.deleteEntityWithPath @project_id, @path, @source, @user_id, @callback + + it 'should delete the folder using the project entity handler', -> + @ProjectEntityUpdateHandler.deleteEntityWithPath + .calledWith(@project_id, @path, @user_id) + .should.equal.true + + it 'notify users an entity has been deleted', -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "removeEntity", @entity_id, @source) + .should.equal true + describe "notifyUsersProjectHasBeenDeletedOrRenamed", -> it 'should emmit a message to all users in a project', (done)-> - @EditorRealTimeController.emitToRoom = sinon.stub() @EditorController.notifyUsersProjectHasBeenDeletedOrRenamed @project_id, (err)=> @EditorRealTimeController.emitToRoom .calledWith(@project_id, "projectRenamedOrDeletedByExternalSource") @@ -397,24 +291,15 @@ describe "EditorController", -> describe "updateProjectDescription", -> beforeEach -> @description = "new description" - @EditorRealTimeController.emitToRoom = sinon.stub() + @EditorController.updateProjectDescription @project_id, @description, @callback + it "should send the new description to the project details handler", -> + @ProjectDetailsHandler.setProjectDescription.calledWith(@project_id, @description).should.equal true - it "should send the new description to the project details handler", (done)-> - @ProjectDetailsHandler.setProjectDescription.callsArgWith(2) - @EditorController.updateProjectDescription @project_id, @description, => - @ProjectDetailsHandler.setProjectDescription.calledWith(@project_id, @description).should.equal true - done() - - it "should notify the other clients about the updated description", (done)-> - @ProjectDetailsHandler.setProjectDescription.callsArgWith(2) - @EditorController.updateProjectDescription @project_id, @description, => - @EditorRealTimeController.emitToRoom.calledWith(@project_id, "projectDescriptionUpdated", @description).should.equal true - done() - + it "should notify the other clients about the updated description", -> + @EditorRealTimeController.emitToRoom.calledWith(@project_id, "projectDescriptionUpdated", @description).should.equal true describe "deleteProject", -> - beforeEach -> @err = "errro" @ProjectDeleter.deleteProject = sinon.stub().callsArgWith(1, @err) @@ -425,27 +310,20 @@ describe "EditorController", -> @ProjectDeleter.deleteProject.calledWith(@project_id).should.equal true done() - describe "renameEntity", -> beforeEach (done) -> @entity_id = "entity_id_here" @entityType = "doc" @newName = "bobsfile.tex" - @ProjectEntityHandler.renameEntity = sinon.stub().callsArg(5) - @EditorRealTimeController.emitToRoom = sinon.stub() + @ProjectEntityUpdateHandler.renameEntity = sinon.stub().yields() @EditorController.renameEntity @project_id, @entity_id, @entityType, @newName, @user_id, done it "should call the project handler", -> - @ProjectEntityHandler.renameEntity + @ProjectEntityUpdateHandler.renameEntity .calledWith(@project_id, @entity_id, @entityType, @newName, @user_id) .should.equal true - it "should take the lock", -> - @LockManager.runWithLock - .calledWith(@project_id) - .should.equal true - it "should emit the update to the room", -> @EditorRealTimeController.emitToRoom .calledWith(@project_id, 'reciveEntityRename', @entity_id, @newName) @@ -455,162 +333,126 @@ describe "EditorController", -> beforeEach -> @entity_id = "entity_id_here" @entityType = "doc" - @folder_id = "313dasd21dasdsa" - @ProjectEntityHandler.moveEntity = sinon.stub().callsArg(5) - @EditorRealTimeController.emitToRoom = sinon.stub() + @ProjectEntityUpdateHandler.moveEntity = sinon.stub().yields() + @EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, @user_id, @callback - it "should call the ProjectEntityHandler", (done)-> - @EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, @user_id, => - @ProjectEntityHandler.moveEntity.calledWith(@project_id, @entity_id, @folder_id, @entityType, @user_id).should.equal true - done() + it "should call the ProjectEntityUpdateHandler", -> + @ProjectEntityUpdateHandler.moveEntity + .calledWith(@project_id, @entity_id, @folder_id, @entityType, @user_id) + .should.equal true - it "should take the lock", (done)-> - @EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, @user_id, => - @LockManager.runWithLock.calledWith(@project_id).should.equal true - done() + it "should emit the update to the room", -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, 'reciveEntityMove', @entity_id, @folder_id) + .should.equal true - it "should propogate up any errors", (done)-> - @LockManager.runWithLock = sinon.stub().callsArgWith(2, "timed out") - @EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, @user_id, (error) => - expect(error).to.exist - error.should.equal "timed out" - done() - - it "should emit the update to the room", (done)-> - @EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, @user_id, => - @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'reciveEntityMove', @entity_id, @folder_id).should.equal true - done() + it "calls the callback", -> + @callback.called.should.equal true describe "renameProject", -> - beforeEach -> @err = "errro" - @window_id = "kdsjklj290jlk" @newName = "new name here" - @ProjectDetailsHandler.renameProject = sinon.stub().callsArg(2) - @EditorRealTimeController.emitToRoom = sinon.stub() + @EditorController.renameProject @project_id, @newName, @callback - it "should call the EditorController", (done)-> - @EditorController.renameProject @project_id, @newName, => - @ProjectDetailsHandler.renameProject.calledWith(@project_id, @newName).should.equal true - done() + it "should call the EditorController", -> + @ProjectDetailsHandler.renameProject.calledWith(@project_id, @newName).should.equal true + it "should emit the update to the room", -> + @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'projectNameUpdated', @newName).should.equal true - it "should emit the update to the room", (done)-> - @EditorController.renameProject @project_id, @newName, => - @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'projectNameUpdated', @newName).should.equal true - done() + describe "setCompiler", -> + beforeEach -> + @compiler = "latex" + @EditorController.setCompiler @project_id, @compiler, @callback + it "should send the new compiler and project id to the project options handler", -> + @ProjectOptionsHandler.setCompiler + .calledWith(@project_id, @compiler) + .should.equal true + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "compilerUpdated", @compiler) + .should.equal true + + describe "setSpellCheckLanguage", -> + beforeEach -> + @languageCode = "fr" + @EditorController.setSpellCheckLanguage @project_id, @languageCode, @callback + + it "should send the new languageCode and project id to the project options handler", -> + @ProjectOptionsHandler.setSpellCheckLanguage + .calledWith(@project_id, @languageCode) + .should.equal true + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "spellCheckLanguageUpdated", @languageCode) + .should.equal true describe "setPublicAccessLevel", -> - describe 'when setting to private', -> beforeEach -> @newAccessLevel = 'private' - @ProjectDetailsHandler.setPublicAccessLevel = sinon.stub().callsArgWith(2, null) - @ProjectDetailsHandler.ensureTokensArePresent = sinon.stub() - .callsArgWith(1, null, @tokens) - @EditorRealTimeController.emitToRoom = sinon.stub() + @ProjectDetailsHandler.ensureTokensArePresent = sinon.stub().yields(null, @tokens) + @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, @callback - it 'should set the access level', (done) -> - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => + it 'should set the access level', -> @ProjectDetailsHandler.setPublicAccessLevel - .calledWith(@project_id, @newAccessLevel).should.equal true - done() + .calledWith(@project_id, @newAccessLevel) + .should.equal true - it 'should broadcast the access level change', (done) -> - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, 'project:publicAccessLevel:changed').should.equal true - done() + it 'should broadcast the access level change', -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, 'project:publicAccessLevel:changed') + .should.equal true - it 'should not ensure tokens are present for project', (done) -> - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => - @ProjectDetailsHandler.ensureTokensArePresent - .calledWith(@project_id).should.equal false - done() + it 'should not ensure tokens are present for project', -> + @ProjectDetailsHandler.ensureTokensArePresent + .calledWith(@project_id) + .should.equal false - it 'should not broadcast a token change', (done) -> - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, 'project:tokens:changed', {tokens: @tokens}) - .should.equal false - done() - - it 'should not produce an error', (done) -> - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, (err) => - expect(err).to.not.exist - done() + it 'should not broadcast a token change', -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, 'project:tokens:changed', {tokens: @tokens}) + .should.equal false describe 'when setting to tokenBased', -> beforeEach -> @newAccessLevel = 'tokenBased' @tokens = {readOnly: 'aaa', readAndWrite: '42bbb'} - @ProjectDetailsHandler.setPublicAccessLevel = sinon.stub() - .callsArgWith(2, null) - @ProjectDetailsHandler.ensureTokensArePresent = sinon.stub() - .callsArgWith(1, null, @tokens) - @EditorRealTimeController.emitToRoom = sinon.stub() + @ProjectDetailsHandler.ensureTokensArePresent = sinon.stub().yields(null, @tokens) + @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, @callback - it 'should set the access level', (done) -> - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => - @ProjectDetailsHandler.setPublicAccessLevel - .calledWith(@project_id, @newAccessLevel).should.equal true - done() + it 'should set the access level', -> + @ProjectDetailsHandler.setPublicAccessLevel + .calledWith(@project_id, @newAccessLevel) + .should.equal true - it 'should broadcast the access level change', (done) -> - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, 'project:publicAccessLevel:changed') - .should.equal true - done() + it 'should broadcast the access level change', -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, 'project:publicAccessLevel:changed') + .should.equal true - it 'should ensure tokens are present for project', (done) -> - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => - @ProjectDetailsHandler.ensureTokensArePresent - .calledWith(@project_id).should.equal true - done() + it 'should ensure tokens are present for project', -> + @ProjectDetailsHandler.ensureTokensArePresent + .calledWith(@project_id) + .should.equal true - it 'should broadcast the token change too', (done) -> - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, 'project:tokens:changed', {tokens: @tokens}) - .should.equal true - done() - - it 'should not produce an error', (done) -> - @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, (err) => - expect(err).to.not.exist - done() - - # beforeEach -> - # @newAccessLevel = "public" - # @ProjectDetailsHandler.setPublicAccessLevel = sinon.stub().callsArgWith(2, null) - # @EditorRealTimeController.emitToRoom = sinon.stub() - - # it "should call the EditorController", (done)-> - # @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => - # @ProjectDetailsHandler.setPublicAccessLevel.calledWith(@project_id, @newAccessLevel).should.equal true - # done() - - # it "should emit the update to the room", (done)-> - # @EditorController.setPublicAccessLevel @project_id, @newAccessLevel, => - # @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'publicAccessLevelUpdated', @newAccessLevel).should.equal true - # done() + it 'should broadcast the token change too', -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, 'project:tokens:changed', {tokens: @tokens}) + .should.equal true describe "setRootDoc", -> - beforeEach -> @newRootDocID = "21312321321" - @ProjectEntityHandler.setRootDoc = sinon.stub().callsArgWith(2, null) - @EditorRealTimeController.emitToRoom = sinon.stub() + @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().yields() + @EditorController.setRootDoc @project_id, @newRootDocID, @callback - it "should call the ProjectEntityHandler", (done)-> - @EditorController.setRootDoc @project_id, @newRootDocID, => - @ProjectEntityHandler.setRootDoc.calledWith(@project_id, @newRootDocID).should.equal true - done() + it "should call the ProjectEntityUpdateHandler", -> + @ProjectEntityUpdateHandler.setRootDoc + .calledWith(@project_id, @newRootDocID) + .should.equal true - it "should emit the update to the room", (done)-> - @EditorController.setRootDoc @project_id, @newRootDocID, => - @EditorRealTimeController.emitToRoom.calledWith(@project_id, 'rootDocUpdated', @newRootDocID).should.equal true - done() + it "should emit the update to the room", -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, 'rootDocUpdated', @newRootDocID) + .should.equal true diff --git a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee index 38419e6b46..54961d206e 100644 --- a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee @@ -6,7 +6,7 @@ modulePath = require('path').join __dirname, '../../../../app/js/Features/Editor describe "EditorHttpController", -> beforeEach -> @EditorHttpController = SandboxedModule.require modulePath, requires: - '../Project/ProjectEntityHandler' : @ProjectEntityHandler = {} + '../Project/ProjectEntityUpdateHandler' : @ProjectEntityUpdateHandler = {} '../Project/ProjectDeleter' : @ProjectDeleter = {} '../Project/ProjectGetter' : @ProjectGetter = {} '../User/UserGetter' : @UserGetter = {} @@ -171,7 +171,7 @@ describe "EditorHttpController", -> doc_id: @doc_id @req.body = name: @name = "doc-name" - @ProjectEntityHandler.restoreDoc = sinon.stub().callsArgWith(3, null, + @ProjectEntityUpdateHandler.restoreDoc = sinon.stub().callsArgWith(3, null, @doc = { "mock": "doc", _id: @new_doc_id = "new-doc-id" } @folder_id = "mock-folder-id" ) @@ -179,7 +179,7 @@ describe "EditorHttpController", -> @EditorHttpController.restoreDoc @req, @res it "should restore the doc", -> - @ProjectEntityHandler.restoreDoc + @ProjectEntityUpdateHandler.restoreDoc .calledWith(@project_id, @doc_id, @name) .should.equal true diff --git a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee index 8fef737ed8..1be6f1106a 100644 --- a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee @@ -32,7 +32,7 @@ describe 'ProjectCreationHandler', -> @FolderModel = class Folder constructor:(options)-> {@name} = options - @ProjectEntityHandler = + @ProjectEntityUpdateHandler = addDoc: sinon.stub().callsArgWith(5, null, {_id: docId}) addFile: sinon.stub().callsArg(5) setRootDoc: sinon.stub().callsArg(2) @@ -57,7 +57,7 @@ describe 'ProjectCreationHandler', -> '../../models/Project':{Project:@ProjectModel} '../../models/Folder':{Folder:@FolderModel} '../History/HistoryManager': @HistoryManager - './ProjectEntityHandler':@ProjectEntityHandler + './ProjectEntityUpdateHandler':@ProjectEntityUpdateHandler "./ProjectDetailsHandler":@ProjectDetailsHandler "settings-sharelatex": @Settings 'logger-sharelatex': {log:->} @@ -164,11 +164,11 @@ describe 'ProjectCreationHandler', -> .should.equal true it 'should insert main.tex', -> - @ProjectEntityHandler.addDoc.calledWith(project_id, rootFolderId, "main.tex", ["mainbasic.tex", "lines"], ownerId) + @ProjectEntityUpdateHandler.addDoc.calledWith(project_id, rootFolderId, "main.tex", ["mainbasic.tex", "lines"], ownerId) .should.equal true it 'should set the main doc id', -> - @ProjectEntityHandler.setRootDoc.calledWith(project_id, docId).should.equal true + @ProjectEntityUpdateHandler.setRootDoc.calledWith(project_id, docId).should.equal true it 'should build the mainbasic.tex template', -> @handler._buildTemplate @@ -194,17 +194,17 @@ describe 'ProjectCreationHandler', -> .should.equal true it 'should insert main.tex', -> - @ProjectEntityHandler.addDoc + @ProjectEntityUpdateHandler.addDoc .calledWith(project_id, rootFolderId, "main.tex", ["main.tex", "lines"], ownerId) .should.equal true it 'should insert references.bib', -> - @ProjectEntityHandler.addDoc + @ProjectEntityUpdateHandler.addDoc .calledWith(project_id, rootFolderId, "references.bib", ["references.bib", "lines"], ownerId) .should.equal true it 'should insert universe.jpg', -> - @ProjectEntityHandler.addFile + @ProjectEntityUpdateHandler.addFile .calledWith( project_id, rootFolderId, "universe.jpg", Path.resolve(__dirname + "/../../../../app/templates/project_files/universe.jpg"), @@ -213,7 +213,7 @@ describe 'ProjectCreationHandler', -> .should.equal true it 'should set the main doc id', -> - @ProjectEntityHandler.setRootDoc.calledWith(project_id, docId).should.equal true + @ProjectEntityUpdateHandler.setRootDoc.calledWith(project_id, docId).should.equal true it 'should build the main.tex template', -> @handler._buildTemplate diff --git a/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee b/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee index b7afb79f83..acd0bb15fc 100644 --- a/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee @@ -63,11 +63,11 @@ describe 'ProjectDuplicator', -> @projectOptionsHandler = setCompiler : sinon.stub() - @entityHandler = + @ProjectEntityUpdateHandler = addDoc: sinon.stub().callsArgWith(5, null, {name:"somDoc"}) copyFileFromExistingProjectWithProject: sinon.stub().callsArgWith(5) setRootDoc: sinon.stub() - addFolderWithProject: sinon.stub().callsArgWith(3, null, @newFolder) + addFolder: sinon.stub().callsArgWith(3, null, @newFolder) @DocumentUpdaterHandler = flushProjectToMongo: sinon.stub().callsArg(1) @@ -85,7 +85,7 @@ describe 'ProjectDuplicator', -> '../../models/Project':{Project:@Project} "../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler './ProjectCreationHandler': @creationHandler - './ProjectEntityHandler': @entityHandler + './ProjectEntityUpdateHandler': @ProjectEntityUpdateHandler './ProjectLocator': @locator './ProjectOptionsHandler': @projectOptionsHandler "../Docstore/DocstoreManager": @DocstoreManager @@ -112,15 +112,15 @@ describe 'ProjectDuplicator', -> done() it 'should use the same compiler', (done)-> - @entityHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id) + @ProjectEntityUpdateHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id) @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> @projectOptionsHandler.setCompiler.calledWith(@stubbedNewProject._id, @project.compiler).should.equal true done() it 'should use the same root doc', (done)-> - @entityHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id) + @ProjectEntityUpdateHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id) @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> - @entityHandler.setRootDoc.calledWith(@stubbedNewProject._id, @rootFolder.docs[0]._id).should.equal true + @ProjectEntityUpdateHandler.setRootDoc.calledWith(@stubbedNewProject._id, @rootFolder.docs[0]._id).should.equal true done() it 'should not copy the collaberators or read only refs', (done)-> @@ -131,34 +131,34 @@ describe 'ProjectDuplicator', -> it 'should copy all the folders', (done)-> @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> - @entityHandler.addFolderWithProject.calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @level1folder.name).should.equal true - @entityHandler.addFolderWithProject.calledWith(@stubbedNewProject, @newFolder._id, @level2folder.name).should.equal true - @entityHandler.addFolderWithProject.callCount.should.equal 2 + @ProjectEntityUpdateHandler.addFolder.calledWith(@new_project_id, @stubbedNewProject.rootFolder[0]._id, @level1folder.name).should.equal true + @ProjectEntityUpdateHandler.addFolder.calledWith(@new_project_id, @newFolder._id, @level2folder.name).should.equal true + @ProjectEntityUpdateHandler.addFolder.callCount.should.equal 2 done() it 'should copy all the docs', (done)-> @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> @DocstoreManager.getAllDocs.calledWith(@old_project_id).should.equal true - @entityHandler.addDoc - .calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @doc0.name, @doc0_lines, @owner._id) + @ProjectEntityUpdateHandler.addDoc + .calledWith(@new_project_id, @stubbedNewProject.rootFolder[0]._id, @doc0.name, @doc0_lines, @owner._id) .should.equal true - @entityHandler.addDoc - .calledWith(@stubbedNewProject, @newFolder._id, @doc1.name, @doc1_lines, @owner._id) + @ProjectEntityUpdateHandler.addDoc + .calledWith(@new_project_id, @newFolder._id, @doc1.name, @doc1_lines, @owner._id) .should.equal true - @entityHandler.addDoc - .calledWith(@stubbedNewProject, @newFolder._id, @doc2.name, @doc2_lines, @owner._id) + @ProjectEntityUpdateHandler.addDoc + .calledWith(@new_project_id, @newFolder._id, @doc2.name, @doc2_lines, @owner._id) .should.equal true done() it 'should copy all the files', (done)-> @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> - @entityHandler.copyFileFromExistingProjectWithProject + @ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject .calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @project._id, @rootFolder.fileRefs[0], @owner._id) .should.equal true - @entityHandler.copyFileFromExistingProjectWithProject + @ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject .calledWith(@stubbedNewProject, @newFolder._id, @project._id, @level1folder.fileRefs[0], @owner._id) .should.equal true - @entityHandler.copyFileFromExistingProjectWithProject + @ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject .calledWith(@stubbedNewProject, @newFolder._id, @project._id, @level2folder.fileRefs[0], @owner._id) .should.equal true done() diff --git a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee index e2e42fb51f..08f87b056f 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee @@ -6,7 +6,6 @@ sinon = require 'sinon' modulePath = "../../../../app/js/Features/Project/ProjectEntityHandler" SandboxedModule = require('sandboxed-module') ObjectId = require("mongoose").Types.ObjectId -tk = require 'timekeeper' Errors = require "../../../../app/js/Features/Errors/Errors" describe 'ProjectEntityHandler', -> @@ -17,902 +16,33 @@ describe 'ProjectEntityHandler', -> userId = 1234 beforeEach -> - @fileUrl = 'filestore.example.com/file' - @FileStoreHandler = - uploadFileFromDisk: sinon.stub().callsArgWith(3, null, @fileUrl) - copyFile: sinon.stub().callsArgWith(4, null, @fileUrl) - @tpdsUpdateSender = + @TpdsUpdateSender = addDoc:sinon.stub().callsArg(1) addFile:sinon.stub().callsArg(1) - addFolder:sinon.stub().callsArg(1) - @rootFolder = - _id:rootFolderId, - folders:[ - {name:"level1", folders:[]} - ] - @ProjectUpdateStub = sinon.stub() @ProjectModel = class Project constructor:(options)-> @._id = project_id @name = "project_name_here" @rev = 0 - save:(callback)->callback() rootFolder:[@rootFolder] - @ProjectModel.update = @ProjectUpdateStub - - @DocModel = class Doc - constructor:(options)-> - {@name, @lines} = options - @_id = doc_id - @rev = 0 - @FileModel = class File - constructor:(options)-> - {@name} = options - @._id = "file_id" - @rev = 0 - @FolderModel = class Folder - constructor:(options)-> - {@name} = options @project = new @ProjectModel() - @project.rootFolder = [@rootFolder] - @ProjectModel.findById = (project_id, callback)=> callback(null, @project) - @ProjectModel.getProject = (project_id, fields, callback)=> callback(null, @project) - @ProjectGetter = - getProjectWithOnlyFolders : (project_id, callback)=> callback(null, @project) - getProjectWithoutDocLines : (project_id, callback)=> callback(null, @project) - getProject: sinon.stub().callsArgWith(2, null, @project) - @projectUpdater = markAsUpdated:sinon.stub() - @projectLocator = + @ProjectLocator = findElement : sinon.stub() - @settings = - maxEntitiesPerProject:200 - @documentUpdaterHandler = + @DocumentUpdaterHandler = updateProjectStructure: sinon.stub().yields() - deleteDoc: sinon.stub().callsArg(2) + + @callback = sinon.stub() + @ProjectEntityHandler = SandboxedModule.require modulePath, requires: - '../../models/Project': Project:@ProjectModel - '../../models/Doc': Doc:@DocModel - '../../models/Folder': Folder:@FolderModel - '../../models/File': File:@FileModel - '../FileStore/FileStoreHandler':@FileStoreHandler - '../ThirdPartyDataStore/TpdsUpdateSender':@tpdsUpdateSender - './ProjectLocator': @projectLocator - '../../Features/DocumentUpdater/DocumentUpdaterHandler':@documentUpdaterHandler - '../Docstore/DocstoreManager': @DocstoreManager = {} 'logger-sharelatex': @logger = {log:sinon.stub(), error: sinon.stub(), err:->} - './ProjectUpdateHandler': @projectUpdater - "./ProjectGetter": @ProjectGetter - "settings-sharelatex":@settings - "../Cooldown/CooldownManager": @CooldownManager = {} - - - describe 'mkdirp', -> - beforeEach -> - @parentFolder_id = "1jnjknjk" - @newFolder = {_id:"newFolder_id_here"} - @lastFolder = {_id:"123das", folders:[]} - @ProjectGetter.getProjectWithOnlyFolders = sinon.stub().callsArgWith(1, null, @project) - @projectLocator.findElementByPath = (project_id, path, cb)=> - @parentFolder = {_id:"parentFolder_id_here"} - lastFolder = path.substring(path.lastIndexOf("/")) - if lastFolder.indexOf("level1") == -1 - cb "level1 is not the last foler " - else - cb null, @parentFolder - @ProjectEntityHandler.addFolder = (project_id, parentFolder_id, folderName, callback)=> - callback null, {name:folderName}, @parentFolder_id - - it 'should return the root folder if the path is just a slash', (done)-> - path = "/" - @ProjectEntityHandler.mkdirp project_id, path, (err, folders, lastFolder)=> - lastFolder.should.deep.equal @rootFolder - assert.equal lastFolder.parentFolder_id, undefined - done() - - - it 'should make just one folder', (done)-> - path = "/differentFolder/" - @ProjectEntityHandler.mkdirp project_id, path, (err, folders, lastFolder)=> - folders.length.should.equal 1 - lastFolder.name.should.equal "differentFolder" - lastFolder.parentFolder_id.should.equal @parentFolder_id - done() - - it 'should make the final folder in path if it doesnt exist with one level', (done)-> - path = "level1/level2" - @ProjectEntityHandler.mkdirp project_id, path, (err, folders, lastFolder)=> - folders.length.should.equal 1 - lastFolder.name.should.equal "level2" - lastFolder.parentFolder_id.should.equal @parentFolder_id - done() - - - it 'should make the final folder in path if it doesnt exist with mutliple levels', (done)-> - path = "level1/level2/level3" - - @ProjectEntityHandler.mkdirp project_id, path,(err, folders, lastFolder) => - folders.length.should.equal 2 - folders[0].name.should.equal "level2" - folders[0].parentFolder_id.should.equal @parentFolder_id - lastFolder.name.should.equal "level3" - lastFolder.parentFolder_id.should.equal @parentFolder_id - done() - - it 'should work with slashes either side', (done)-> - path = "/level1/level2/level3/" - - @ProjectEntityHandler.mkdirp project_id, path, (err, folders, lastFolder)=> - folders.length.should.equal 2 - folders[0].name.should.equal "level2" - folders[0].parentFolder_id.should.equal @parentFolder_id - lastFolder.name.should.equal "level3" - lastFolder.parentFolder_id.should.equal @parentFolder_id - done() - - describe 'deleteEntity', -> - entity_id = "4eecaffcbffa66588e000009" - beforeEach -> - @ProjectGetter.getProject.callsArgWith(2, null, @project) - @tpdsUpdateSender.deleteEntity = sinon.stub().callsArg(1) - @ProjectEntityHandler._removeElementFromMongoArray = sinon.stub().callsArg(3) - @ProjectEntityHandler._cleanUpEntity = sinon.stub().callsArg(5) - @path = mongo: "mongo.path", fileSystem: "/file/system/path" - @projectLocator.findElement = sinon.stub().callsArgWith(1, null, @entity = { _id: entity_id }, @path) - - describe "deleting from Mongo", -> - beforeEach (done) -> - @ProjectEntityHandler.deleteEntity project_id, entity_id, @type = 'file', userId, done - - it "should retreive the path", -> - @projectLocator.findElement.called.should.equal true - options = @projectLocator.findElement.args[0][0] - options.type.should.equal @type - options.project.should.equal @project - options.element_id.should.equal entity_id - - it "should remove the element from the database", -> - @ProjectEntityHandler._removeElementFromMongoArray.calledWith(@ProjectModel, project_id, @path.mongo).should.equal true - - it "should call the third party data store", -> - options = @tpdsUpdateSender.deleteEntity.args[0][0] - options.project_id.should.equal project_id - options.path.should.equal @path.fileSystem - - it "should clean up the entity from the rest of the system", -> - @ProjectEntityHandler._cleanUpEntity - .calledWith(@project, @entity, @type, @path.fileSystem, userId) - .should.equal true - - describe "_cleanUpEntity", -> - beforeEach -> - @entity_id = "4eecaffcbffa66588e000009" - @FileStoreHandler.deleteFile = sinon.stub().callsArg(2) - @ProjectEntityHandler.unsetRootDoc = sinon.stub().callsArg(1) - - describe "a file", -> - beforeEach (done) -> - @path = "/file/system/path.png" - @entity = _id: @entity_id - @ProjectEntityHandler._cleanUpEntity @project, @entity, 'file', @path, userId, done - - it "should delete the file from FileStoreHandler", -> - @FileStoreHandler.deleteFile.calledWith(project_id, @entity_id).should.equal true - - it "should not attempt to delete from the document updater", -> - @documentUpdaterHandler.deleteDoc.called.should.equal false - - it "should should send the update to the doc updater", -> - oldFiles = [ file: @entity, path: @path ] - @documentUpdaterHandler.updateProjectStructure - .calledWith(project_id, userId, {oldFiles}) - .should.equal true - - describe "a doc", -> - beforeEach (done) -> - @path = "/file/system/path.tex" - @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4) - @entity = {_id: @entity_id} - @ProjectEntityHandler._cleanUpEntity @project, @entity, 'doc', @path, userId, done - - it "should clean up the doc", -> - @ProjectEntityHandler._cleanUpDoc - .calledWith(@project, @entity, @path, userId) - .should.equal true - - describe "a folder", -> - beforeEach (done) -> - @folder = - folders: [ - name: "subfolder" - fileRefs: [ @file1 = { _id: "file-id-1", name: "file-name-1"} ] - docs: [ @doc1 = { _id: "doc-id-1", name: "doc-name-1" } ] - folders: [] - ] - fileRefs: [ @file2 = { _id: "file-id-2", name: "file-name-2" } ] - docs: [ @doc2 = { _id: "doc-id-2", name: "doc-name-2" } ] - - @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4) - @ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(4) - path = "/folder" - @ProjectEntityHandler._cleanUpEntity @project, @folder, "folder", path, userId, done - - it "should clean up all sub files", -> - @ProjectEntityHandler._cleanUpFile - .calledWith(@project, @file1, "/folder/subfolder/file-name-1", userId) - .should.equal true - @ProjectEntityHandler._cleanUpFile - .calledWith(@project, @file2, "/folder/file-name-2", userId) - .should.equal true - - it "should clean up all sub docs", -> - @ProjectEntityHandler._cleanUpDoc - .calledWith(@project, @doc1, "/folder/subfolder/doc-name-1", userId) - .should.equal true - @ProjectEntityHandler._cleanUpDoc - .calledWith(@project, @doc2, "/folder/doc-name-2", userId) - .should.equal true - - describe 'moveEntity', -> - beforeEach -> - @pathAfterMove = { - fileSystem: "/somewhere/else.txt" - } - @ProjectEntityHandler._removeElementFromMongoArray = sinon.stub().callsArgWith(3, null, @project) - @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, path: @pathAfterMove) - @ProjectGetter.getProject.callsArgWith(2, null, @project) - @tpdsUpdateSender.moveEntity = sinon.stub() - @ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub() - @ProjectEntityHandler.getAllEntitiesFromProject - .onFirstCall() - .callsArgWith(1, null, @oldDocs = ['old-doc'], @oldFiles = ['old-file']) - @ProjectEntityHandler.getAllEntitiesFromProject - .onSecondCall() - .callsArgWith(1, null, @newDocs = ['new-doc'], @newFiles = ['new-file']) - - describe "moving a doc", -> - beforeEach (done) -> - @docId = "4eecaffcbffa66588e000009" - @doc = {lines:["1234","312343d"], rev: "1234"} - @path = { - mongo:"folders[0]" - fileSystem:"/old_folder/somewhere.txt" - } - @destFolder = { name: "folder" } - @destFolderPath = { - mongo: "folders[0]" - fileSystem: "/dest_folder" - } - @projectLocator.findElement = sinon.stub() - @projectLocator.findElement.withArgs({project: @project, element_id: @docId, type: 'docs'}) - .callsArgWith(1, null, @doc, @path) - @projectLocator.findElement.withArgs({project: @project, element_id: folder_id, type:"folder"},) - .callsArgWith(1, null, @destFolder, @destFolderPath) - @ProjectEntityHandler.moveEntity project_id, @docId, folder_id, "docs", userId, done - - it 'should find the doc to move', -> - @projectLocator.findElement.calledWith({element_id: @docId, type: "docs", project: @project }).should.equal true - - it "should should send the update to the doc updater", -> - @documentUpdaterHandler.updateProjectStructure - .calledWith(project_id, userId, {@oldDocs, @newDocs, @oldFiles, @newFiles}) - .should.equal true - - it 'should remove the element from its current position', -> - @ProjectEntityHandler._removeElementFromMongoArray - .calledWith(@ProjectModel, project_id, @path.mongo ).should.equal true - - it "should put the element back in the new folder", -> - @ProjectEntityHandler._putElement.calledWith(@project, folder_id, @doc, "docs").should.equal true - - it 'should tell the third party data store', -> - @tpdsUpdateSender.moveEntity - .calledWith({ - project_id: project_id, - startPath: @path.fileSystem - endPath: @pathAfterMove.fileSystem - project_name: @project.name - rev: @doc.rev - }) - .should.equal true - - describe "moving a doc when another with the same name already exists", -> - beforeEach () -> - @docId = "4eecaffcbffa66588e000009" - @doc = { name: "another-doc.tex", lines:["1234","312343d"], rev: "1234"} - @path = { - mongo:"folders[0]" - fileSystem:"/old_folder/somewhere.txt" - } - @destFolder = { name: "folder", docs: [ {name:"another-doc.tex"} ] } - @destFolderPath = { - mongo: "folders[0]" - fileSystem: "/dest_folder" - } - @projectLocator.findElement = sinon.stub() - @projectLocator.findElement.withArgs({project: @project, element_id: @docId, type: 'docs'}) - .callsArgWith(1, null, @doc, @path) - @projectLocator.findElement.withArgs({project: @project, element_id: folder_id, type:"folder"},) - .callsArgWith(1, null, @destFolder, @destFolderPath) - @callback = sinon.stub() - @ProjectEntityHandler.moveEntity project_id, @docId, folder_id, "docs", userId, @callback - - it 'should return an error', -> - @callback.calledWith(new Errors.InvalidNameError("file already exists")).should.equal true - - it "should should not send the update to the doc updater", -> - @documentUpdaterHandler.updateProjectStructure - .called.should.equal false - - it 'should not remove the element from its current position', -> - @ProjectEntityHandler._removeElementFromMongoArray - .called.should.equal false - - it "should not put the element back in the new folder", -> - @ProjectEntityHandler._putElement.called.should.equal false - - it 'should not tell the third party data store', -> - @tpdsUpdateSender.moveEntity - .called.should.equal false - - - describe "moving a folder", -> - beforeEach -> - @folder_id = "folder-to-move" - @move_to_folder_id = "folder-to-move-to" - @folder = { name: "folder" } - @folder_to_move_to = { name: "folder to move to" } - @path = { mongo:"folders[0]" } - @pathToMoveTo = { mongo: "folders[0]" } - @projectLocator.findElement = sinon.stub() - @projectLocator.findElement.withArgs({project: @project, element_id: @folder_id, type: 'folder'}) - .callsArgWith(1, null, @folder, @path) - @projectLocator.findElement.withArgs({project: @project, element_id: @move_to_folder_id, type: 'folder'}) - .callsArgWith(1, null, @folder_to_move_to, @pathToMoveTo) - - describe "when the destination folder is outside the moving folder", -> - beforeEach (done) -> - @path.fileSystem = "/one/src_dir" - @pathToMoveTo.fileSystem = "/two/dest_dir" - @ProjectEntityHandler.moveEntity project_id, @folder_id, @move_to_folder_id, "folder", userId, done - - it 'should find the project then element', -> - @projectLocator.findElement - .calledWith({ - element_id: @folder_id, - type: "folder", - project: @project - }) - .should.equal true - - it "should should send the update to the doc updater", -> - @documentUpdaterHandler.updateProjectStructure - .calledWith(project_id, userId, {@oldDocs, @newDocs, @oldFiles, @newFiles}) - .should.equal true - - it 'should remove the element from its current position', -> - @ProjectEntityHandler._removeElementFromMongoArray - .calledWith( - @ProjectModel, - project_id, - @path.mongo - ) - .should.equal true - - it "should put the element back in the new folder", -> - @ProjectEntityHandler._putElement - .calledWith( - @project, - @move_to_folder_id, - @folder, - "folder" - ) - .should.equal true - - it 'should tell the third party data store', -> - @tpdsUpdateSender.moveEntity - .calledWith({ - project_id: project_id, - startPath: @path.fileSystem - endPath: @pathAfterMove.fileSystem - project_name: @project.name, - rev: @folder.rev - }) - .should.equal true - - describe "when the destination folder contains a file with the same name", -> - beforeEach -> - @path.fileSystem = "/one/src_dir" - @pathToMoveTo.fileSystem = "/two/dest_dir" - @folder_to_move_to = { name: "folder to move to", fileRefs: [ {name: "folder"}] } - @projectLocator.findElement.withArgs({project: @project, element_id: @move_to_folder_id, type: 'folder'}) - .callsArgWith(1, null, @folder_to_move_to, @pathToMoveTo) - @callback = sinon.stub() - @ProjectEntityHandler.moveEntity project_id, @folder_id, @move_to_folder_id, "folder", userId, @callback - - it 'should find the folder we are moving to element', -> - @projectLocator.findElement - .calledWith({ - element_id: @move_to_folder_id, - type: "folder", - project: @project - }) - .should.equal true - - it "should return an error", -> - @callback - .calledWith(new Errors.InvalidNameError("file already exists")) - .should.equal true - - describe "when the destination folder is inside the moving folder", -> - beforeEach -> - @path.fileSystem = "/one/two" - @pathToMoveTo.fileSystem = "/one/two/three" - - @projectLocator.findElement.withArgs({project: @project, element_id: @move_to_folder_id, type: 'folder'}) - .callsArgWith(1, null, @folder_to_move_to, @pathToMoveTo) - @callback = sinon.stub() - @ProjectEntityHandler.moveEntity project_id, @folder_id, @move_to_folder_id, "folder", userId, @callback - - it 'should find the folder we are moving to element', -> - @projectLocator.findElement - .calledWith({ - element_id: @move_to_folder_id, - type: "folder", - project: @project - }) - .should.equal true - - it "should return an error", -> - @callback - .calledWith(new Errors.InvalidNameError("destination folder is a child folder of me")) - .should.equal true - - describe '_removeElementFromMongoArray ', -> - beforeEach -> - @mongoPath = "folders[0].folders[5]" - @id = "12344" - @project = 'a project' - @ProjectModel.update = sinon.stub().callsArg(3) - @ProjectModel.findOneAndUpdate = sinon.stub().callsArgWith(3, null, @project) - @ProjectEntityHandler._removeElementFromMongoArray @ProjectModel, @id, @mongoPath, @callback - - it 'should unset', -> - update = { '$unset': { } } - update['$unset'][@mongoPath] = 1 - @ProjectModel.update - .calledWith({ _id: @id }, update, {}) - .should.equal true - - it 'should pull', -> - @ProjectModel.findOneAndUpdate - .calledWith({ _id: @id }, { '$pull': { 'folders[0]': null } }, {'new': true}) - .should.equal true - - it 'should call the callback', -> - @callback.calledWith(null, @project).should.equal true - - describe 'getDoc', -> - beforeEach -> - @lines = ["mock", "doc", "lines"] - @rev = 5 - @version = 42 - @ranges = {"mock": "ranges"} - - @DocstoreManager.getDoc = sinon.stub().callsArgWith(3, null, @lines, @rev, @version, @ranges) - - describe 'without pathname option', -> - beforeEach -> - @ProjectEntityHandler.getDoc project_id, doc_id, @callback - - it "should call the docstore", -> - @DocstoreManager.getDoc - .calledWith(project_id, doc_id) - .should.equal true - - it "should call the callback with the lines, version and rev", -> - @callback.calledWith(null, @lines, @rev, @version, @ranges).should.equal true - - describe 'with pathname option', -> - beforeEach -> - @project = 'a project' - @path = mongo: "mongo.path", fileSystem: "/file/system/path" - @projectLocator.findElement = sinon.stub().callsArgWith(1, null, {}, @path) - @ProjectEntityHandler.getDoc project_id, doc_id, {pathname: true}, @callback - - it "should call the project locator", -> - @projectLocator.findElement - .calledWith({project_id: project_id, element_id: doc_id, type: 'doc'}) - .should.equal true - - it "should call the docstore", -> - @DocstoreManager.getDoc - .calledWith(project_id, doc_id) - .should.equal true - - it "should return the pathname if option given", -> - @callback.calledWith(null, @lines, @rev, @version, @ranges, @path.fileSystem).should.equal true - - - describe 'addDoc', -> - beforeEach -> - @name = "some new doc" - @lines = ['1234','abc'] - @path = "/path/to/doc" - - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) - @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:@path}}) - @callback = sinon.stub() - @tpdsUpdateSender.addDoc = sinon.stub().callsArg(1) - @DocstoreManager.updateDoc = sinon.stub().yields(null, true, 0) - - @ProjectEntityHandler.addDoc project_id, folder_id, @name, @lines, userId, @callback - - # Created doc - @doc = @ProjectEntityHandler._putElement.args[0][2] - @doc.name.should.equal @name - expect(@doc.lines).to.be.undefined - - it 'should call put element', -> - @ProjectEntityHandler._putElement.calledWith(@project, folder_id, @doc).should.equal true - - it 'should return doc and parent folder', -> - @callback.calledWith(null, @doc, folder_id).should.equal true - - it 'should call third party data store', -> - @tpdsUpdateSender.addDoc - .calledWith({ - project_id: project_id - doc_id: doc_id - path: @path - project_name: @project.name - rev: 0 - }) - .should.equal true - - it "should send the doc lines to the doc store", -> - @DocstoreManager.updateDoc - .calledWith(project_id, @doc._id.toString(), @lines) - .should.equal true - - it "should should send the change in project structure to the doc updater", () -> - newDocs = [ - doc: @doc - path: @path - docLines: @lines.join('\n') - ] - @documentUpdaterHandler.updateProjectStructure - .calledWith(project_id, userId, {newDocs}) - .should.equal true - - describe 'addDocWithoutUpdatingHistory', -> - beforeEach -> - @name = "some new doc" - @lines = ['1234','abc'] - @path = "/path/to/doc" - - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) - @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:@path}}) - @callback = sinon.stub() - @tpdsUpdateSender.addDoc = sinon.stub().callsArg(1) - @DocstoreManager.updateDoc = sinon.stub().yields(null, true, 0) - - @ProjectEntityHandler.addDocWithoutUpdatingHistory project_id, folder_id, @name, @lines, userId, @callback - - # Created doc - @doc = @ProjectEntityHandler._putElement.args[0][2] - @doc.name.should.equal @name - expect(@doc.lines).to.be.undefined - - it 'should call put element', -> - @ProjectEntityHandler._putElement.calledWith(@project, folder_id, @doc).should.equal true - - it 'should return doc and parent folder', -> - @callback.calledWith(null, @doc, folder_id).should.equal true - - it 'should call third party data store', -> - @tpdsUpdateSender.addDoc - .calledWith({ - project_id: project_id - doc_id: doc_id - path: @path - project_name: @project.name - rev: 0 - }) - .should.equal true - - it "should send the doc lines to the doc store", -> - @DocstoreManager.updateDoc - .calledWith(project_id, @doc._id.toString(), @lines) - .should.equal true - - it "should not should send the change in project structure to the doc updater", () -> - @documentUpdaterHandler.updateProjectStructure - .called - .should.equal false - - describe "restoreDoc", -> - beforeEach -> - @name = "doc-name" - @lines = ['1234','abc'] - @doc = { "mock": "doc" } - @folder_id = "mock-folder-id" - @callback = sinon.stub() - @ProjectEntityHandler.getDoc = sinon.stub().callsArgWith(3, null, @lines) - @ProjectEntityHandler.addDoc = sinon.stub().callsArgWith(4, null, @doc, @folder_id) - - @ProjectEntityHandler.restoreDoc project_id, doc_id, @name, @callback - - it 'should get the doc lines', -> - @ProjectEntityHandler.getDoc - .calledWith(project_id, doc_id, include_deleted: true) - .should.equal true - - it "should add a new doc with these doc lines", -> - @ProjectEntityHandler.addDoc - .calledWith(project_id, null, @name, @lines) - .should.equal true - - it "should call the callback with the new folder and doc", -> - @callback.calledWith(null, @doc, @folder_id).should.equal true - - describe 'addFile', -> - fileName = "something.jpg" - beforeEach -> - @fileSystemPath = "somehintg" - @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem: @fileSystemPath}}) - @filePath = "somewhere" - - it 'should upload it via the FileStoreHandler', (done)-> - @FileStoreHandler.uploadFileFromDisk = (passedProject_id, file_id, filePath, callback)=> - file_id.should.equal "file_id" - passedProject_id.should.equal project_id - filePath.should.equal @filePath - done() - - @ProjectEntityHandler.addFile project_id, folder_id, fileName, @filePath, userId, (err, fileRef, parentFolder)-> - - it 'should put file into folder by calling put element', (done)-> - @ProjectEntityHandler._putElement = (passedProject, passedFolder_id, passedFileRef, passedType, callback)-> - passedProject._id.should.equal project_id - passedFolder_id.should.equal folder_id - passedFileRef.name.should.equal fileName - passedType.should.equal 'file' - done() - - @ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, (err, fileRef, parentFolder)-> - - it 'should return doc and parent folder', (done)-> - @ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, (err, fileRef, parentFolder)-> - parentFolder.should.equal folder_id - fileRef.name.should.equal fileName - done() - - it 'should call third party data store', (done)-> - @project.existsInVersioningApi = true - opts = - path : "/somehwere/idsadsds" - project_id : project_id - @ProjectEntityHandler._putElement = (project_id, folder_id, doc, type, callback)-> callback(null, {path:{fileSystem:opts.path}}) - - @tpdsUpdateSender.addFile = (options)=> - options.project_id.should.equal project_id - options.path.should.equal opts.path - options.project_name.should.equal @project.name - options.file_id.should.not.be.null - options.rev.should.equal 0 - done() - - @ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, (err, fileRef, parentFolder)-> - - it "should should send the change in project structure to the doc updater", (done) -> - @documentUpdaterHandler.updateProjectStructure = (passed_project_id, passed_user_id, changes) => - passed_project_id.should.equal project_id - passed_user_id.should.equal userId - { newFiles } = changes - newFiles.length.should.equal 1 - newFile = newFiles[0] - newFile.file.name.should.equal fileName - newFile.path.should.equal @fileSystemPath - newFile.url.should.equal @fileUrl - done() - - @ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, () -> - - it "should not send the change in project structure to the doc updater when called as addFileWithoutUpdatingHistory", (done) -> - @documentUpdaterHandler.updateProjectStructure = sinon.stub().yields() - @ProjectEntityHandler.addFileWithoutUpdatingHistory project_id, folder_id, fileName, {}, userId, () => - @documentUpdaterHandler.updateProjectStructure.called.should.equal false - done() - - describe 'replaceFile', -> - beforeEach -> - @projectLocator - @file_id = "file_id_here" - @fsPath = "fs_path_here.png" - @fileRef = {rev:3, _id: @file_id, name: @fileName = "fileName"} - @filePaths = {fileSystem: @fileSystemPath="/folder1/file.png", mongo:"folder.1.files.somewhere"} - @projectLocator.findElement = sinon.stub().callsArgWith(1, null, @fileRef, @filePaths) - @ProjectModel.findOneAndUpdate = sinon.stub().callsArgWith(3) - @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project) - - it 'should find the file', (done)-> - @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, => - @projectLocator.findElement - .calledWith({element_id:@file_id, type:"file", project: @project}) - .should.equal true - done() - - it 'should tell the file store handler to upload the file from disk', (done)-> - @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, => - @FileStoreHandler.uploadFileFromDisk.calledWith(project_id, @file_id, @fsPath).should.equal true - done() - - it 'should send the file to the tpds with an incremented rev', (done)-> - @tpdsUpdateSender.addFile = (options)=> - options.project_id.should.equal project_id - options.path.should.equal @filePaths.fileSystem - options.project_name.should.equal @project.name - options.file_id.should.equal @file_id - options.rev.should.equal @fileRef.rev + 1 - done() - - @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, => - - it 'should inc the rev id', (done)-> - @ProjectModel.findOneAndUpdate = (conditions, update, options, callback)=> - conditions._id.should.equal project_id - update.$inc["#{@filePaths.mongo}.rev"].should.equal 1 - done() - - @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, => - - it 'should update the created at date', (done)-> - d = new Date() - @ProjectModel.findOneAndUpdate = (conditions, update, options, callback)=> - conditions._id.should.equal project_id - differenceInMs = update.$set["#{@filePaths.mongo}.created"].getTime() - d.getTime() - differenceInMs.should.be.below(20) - done() - - @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, => - - it "should should send the old and new project structure to the doc updater", (done) -> - @documentUpdaterHandler.updateProjectStructure = (passed_project_id, passed_user_id, changes) => - passed_project_id.should.equal project_id - passed_user_id.should.equal userId - { newFiles } = changes - newFiles.length.should.equal 1 - newFile = newFiles[0] - newFile.file.name.should.equal @fileName - newFile.path.should.equal @fileSystemPath - newFile.url.should.equal @fileUrl - done() - - @ProjectEntityHandler.replaceFile project_id, @file_id, @fsPath, userId, => - - describe 'addFolder', -> - folderName = "folder1234" - beforeEach -> - @ProjectGetter.getProjectWithOnlyFolders = sinon.stub().callsArgWith(1, null, @project) - - it 'should call put element', (done)-> - @ProjectEntityHandler._putElement = (passedProject, passedFolder_id, passedFolder, passedType, callback)-> - passedProject._id.should.equal project_id - passedFolder_id.should.equal folder_id - passedFolder.name.should.equal folderName - passedType.should.equal 'folder' - done() - @ProjectEntityHandler.addFolder project_id, folder_id, folderName, (err, folder, parentFolder)-> - - it 'should return the folder and parent folder', (done)-> - @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4) - @ProjectEntityHandler.addFolder project_id, folder_id, folderName, (err, folder, parentFolder)-> - parentFolder.should.equal folder_id - folder.name.should.equal folderName - done() - - - describe 'updateDocLines', -> - beforeEach -> - @lines = ['mock', 'doc', 'lines'] - @path = "/somewhere/something.tex" - @doc = { - _id: doc_id - } - @version = 42 - @ranges = {"mock":"ranges"} - @ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project) - @projectLocator.findElement = sinon.stub().callsArgWith(1, null, @doc, {fileSystem: @path}) - @tpdsUpdateSender.addDoc = sinon.stub().callsArg(1) - @projectUpdater.markAsUpdated = sinon.stub() - @callback = sinon.stub() - - describe "when the doc has been modified", -> - beforeEach -> - @DocstoreManager.updateDoc = sinon.stub().yields(null, true, @rev = 5) - @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @ranges, @callback - - it "should get the project without doc lines", -> - @ProjectGetter.getProjectWithoutDocLines - .calledWith(project_id) - .should.equal true - - it "should find the doc", -> - @projectLocator.findElement - .calledWith({ - project: @project - type: "docs" - element_id: doc_id - }) - .should.equal true - - it "should update the doc in the docstore", -> - @DocstoreManager.updateDoc - .calledWith(project_id, doc_id, @lines, @version, @ranges) - .should.equal true - - it "should mark the project as updated", -> - @projectUpdater.markAsUpdated - .calledWith(project_id) - .should.equal true - - it "should send the doc the to the TPDS", -> - @tpdsUpdateSender.addDoc - .calledWith({ - project_id: project_id - project_name: @project.name - doc_id: doc_id - rev: @rev - path: @path - }) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the doc has not been modified", -> - beforeEach -> - @DocstoreManager.updateDoc = sinon.stub().yields(null, false, @rev = 5) - @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @ranges, @callback - - it "should not mark the project as updated", -> - @projectUpdater.markAsUpdated.called.should.equal false - - it "should not send the doc the to the TPDS", -> - @tpdsUpdateSender.addDoc.called.should.equal false - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the project is not found", -> - beforeEach -> - @ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, null) - @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @ranges, @version, @callback - - it "should return a not found error", -> - @callback.calledWith(new Errors.NotFoundError()).should.equal true - - describe "when the doc is not found", -> - beforeEach -> - @projectLocator.findElement = sinon.stub().callsArgWith(1, null, null, null) - @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @ranges, @version, @callback - - it "should log out the error", -> - @logger.error - .calledWith( - project_id: project_id - doc_id: doc_id - lines: @lines - err: new Errors.NotFoundError("doc not found") - "doc not found while updating doc lines" - ) - .should.equal true - - it "should return a not found error", -> - @callback.calledWith(new Errors.NotFoundError()).should.equal true - + '../Docstore/DocstoreManager': @DocstoreManager = {} + '../../Features/DocumentUpdater/DocumentUpdaterHandler':@DocumentUpdaterHandler + '../../models/Project': Project:@ProjectModel + './ProjectLocator': @ProjectLocator + "./ProjectGetter": @ProjectGetter = {} + '../ThirdPartyDataStore/TpdsUpdateSender':@TpdsUpdateSender describe "getting folders, docs and files", -> beforeEach -> @@ -940,38 +70,7 @@ describe 'ProjectEntityHandler', -> folders : [] }] ] - @ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project) - - describe "getAllFolders", -> - beforeEach -> - @callback = sinon.stub() - @ProjectEntityHandler.getAllFolders project_id, @callback - - it "should get the project without the docs lines", -> - @ProjectGetter.getProjectWithoutDocLines - .calledWith(project_id) - .should.equal true - - it "should call the callback with the folders", -> - @callback - .calledWith(null, { - "/": @project.rootFolder[0] - "/folder1": @folder1 - }) - .should.equal true - - describe "getAllFiles", -> - beforeEach -> - @callback = sinon.stub() - @ProjectEntityHandler.getAllFiles project_id, @callback - - it "should call the callback with the files", -> - @callback - .calledWith(null, { - "/file1": @file1 - "/folder1/file2": @file2 - }) - .should.equal true + @ProjectGetter.getProjectWithoutDocLines = sinon.stub().yields(null, @project) describe "getAllDocs", -> beforeEach -> @@ -1010,16 +109,16 @@ describe 'ProjectEntityHandler', -> }) .should.equal true - describe "getAllFoldersFromProject", -> + describe "getAllFiles", -> beforeEach -> @callback = sinon.stub() - @ProjectEntityHandler.getAllFoldersFromProject @project, @callback + @ProjectEntityHandler.getAllFiles project_id, @callback - it "should call the callback with the folders", -> + it "should call the callback with the files", -> @callback .calledWith(null, { - "/": @project.rootFolder[0] - "/folder1": @folder1 + "/file1": @file1 + "/folder1/file2": @file2 }) .should.equal true @@ -1045,16 +144,44 @@ describe 'ProjectEntityHandler', -> .calledWith(null, @expected) .should.equal true + describe "_getAllFolders", -> + beforeEach -> + @callback = sinon.stub() + @ProjectEntityHandler._getAllFolders project_id, @callback + + it "should get the project without the docs lines", -> + @ProjectGetter.getProjectWithoutDocLines + .calledWith(project_id) + .should.equal true + + it "should call the callback with the folders", -> + @callback + .calledWith(null, { + "/": @project.rootFolder[0] + "/folder1": @folder1 + }) + .should.equal true + + describe "_getAllFoldersFromProject", -> + beforeEach -> + @callback = sinon.stub() + @ProjectEntityHandler._getAllFoldersFromProject @project, @callback + + it "should call the callback with the folders", -> + @callback + .calledWith(null, { + "/": @project.rootFolder[0] + "/folder1": @folder1 + }) + .should.equal true + describe "flushProjectToThirdPartyDataStore", -> beforeEach (done) -> @project = { _id: project_id name: "Mock project name" } - @ProjectModel.findById = sinon.stub().callsArgWith(1, null, @project) - @documentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArg(1) - @tpdsUpdateSender.addDoc = sinon.stub().callsArg(1) - @tpdsUpdateSender.addFile = sinon.stub().callsArg(1) + @DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().yields() @docs = { "/doc/one": @doc1 = { _id: "mock-doc-1", lines: ["one"], rev: 5 } "/doc/two": @doc2 = { _id: "mock-doc-2", lines: ["two"], rev: 6 } @@ -1063,15 +190,15 @@ describe 'ProjectEntityHandler', -> "/file/one": @file1 = { _id: "mock-file-1", rev: 7 } "/file/two": @file2 = { _id: "mock-file-2", rev: 8 } } - @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) - @ProjectEntityHandler.getAllFiles = sinon.stub().callsArgWith(1, null, @files) + @ProjectEntityHandler.getAllDocs = sinon.stub().yields(null, @docs) + @ProjectEntityHandler.getAllFiles = sinon.stub().yields(null, @files) - @ProjectGetter.getProject.callsArgWith(2, null, @project) + @ProjectGetter.getProject = sinon.stub().yields(null, @project) @ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, () -> done() it "should flush the project from the doc updater", -> - @documentUpdaterHandler.flushProjectToMongo.calledWith(project_id).should.equal true + @DocumentUpdaterHandler.flushProjectToMongo.calledWith(project_id).should.equal true it "should look up the project in mongo", -> @ProjectGetter.getProject.calledWith(project_id).should.equal true @@ -1084,7 +211,7 @@ describe 'ProjectEntityHandler', -> it "should flush each doc to the TPDS", -> for path, doc of @docs - @tpdsUpdateSender.addDoc + @TpdsUpdateSender.addDoc .calledWith({ project_id: project_id, doc_id: doc._id @@ -1096,7 +223,7 @@ describe 'ProjectEntityHandler', -> it "should flush each file to the TPDS", -> for path, file of @files - @tpdsUpdateSender.addFile + @TpdsUpdateSender.addFile .calledWith({ project_id: project_id, file_id: file._id @@ -1106,412 +233,43 @@ describe 'ProjectEntityHandler', -> }) .should.equal true - describe "setRootDoc", -> - it "should call Project.update", -> - @project_id = "project-id-123234adfs" - @rootDoc_id = "root-doc-id-123123" - @ProjectModel.update = sinon.stub() - @ProjectEntityHandler.setRootDoc @project_id, @rootDoc_id - @ProjectModel.update.calledWith({_id : @project_id}, {rootDoc_id: @rootDoc_id}) - .should.equal true - - describe "unsetRootDoc", -> - it "should call Project.update", -> - @project_id = "project-id-123234adfs" - @rootDoc_id = "root-doc-id-123123" - @ProjectModel.update = sinon.stub() - @ProjectEntityHandler.unsetRootDoc @project_id - @ProjectModel.update.calledWith({_id : @project_id}, {$unset : {rootDoc_id: true}}) - .should.equal true - - describe 'copyFileFromExistingProjectWithProject', -> - fileName = "something.jpg" - filePath = "dumpFolder/somewhere/image.jpeg" - oldProject_id = "123kljadas" - oldFileRef = {name:fileName, _id:"oldFileRef"} - + describe 'getDoc', -> beforeEach -> - @fileSystemPath = "somehintg" - @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem: @fileSystemPath}}) + @lines = ["mock", "doc", "lines"] + @rev = 5 + @version = 42 + @ranges = {"mock": "ranges"} - it 'should copy the file in FileStoreHandler', (done)-> - @ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:"somehintg"}}) - @ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)=> - @FileStoreHandler.copyFile.calledWith(oldProject_id, oldFileRef._id, project_id, fileRef._id).should.equal true - done() + @DocstoreManager.getDoc = sinon.stub().callsArgWith(3, null, @lines, @rev, @version, @ranges) - it 'should put file into folder by calling put element', (done)-> - @ProjectEntityHandler._putElement = (passedProject, passedFolder_id, passedFileRef, passedType, callback)-> - passedProject._id.should.equal project_id - passedFolder_id.should.equal folder_id - passedFileRef.name.should.equal fileName - passedType.should.equal 'file' - done() - - @ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)-> - - it 'should return doc and parent folder', (done)-> - @ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)-> - parentFolder.should.equal folder_id - fileRef.name.should.equal fileName - done() - - it 'should call third party data store if versioning is enabled', (done)-> - @project.existsInVersioningApi = true - opts = - path : "/somehwere/idsadsds" - project_id : project_id - @ProjectEntityHandler._putElement = (project_id, folder_id, doc, type, callback)-> callback(null, {path:{fileSystem:opts.path}}) - - @tpdsUpdateSender.addFile = (options)=> - options.project_id.should.equal project_id - options.project_name.should.equal @project.name - options.path.should.equal opts.path - options.file_id.should.not.be.null - options.rev.should.equal 0 - done() - - @ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)-> - - it "should should send the change in project structure to the doc updater", (done) -> - @documentUpdaterHandler.updateProjectStructure = (passed_project_id, passed_user_id, changes) => - passed_project_id.should.equal project_id - passed_user_id.should.equal userId - { newFiles } = changes - newFiles.length.should.equal 1 - newFile = newFiles[0] - newFile.file.name.should.equal fileName - newFile.path.should.equal @fileSystemPath - newFile.url.should.equal @fileUrl - done() - - @ProjectEntityHandler.copyFileFromExistingProjectWithProject @project, folder_id, oldProject_id, oldFileRef, userId, (err, fileRef, parentFolder)-> - - describe "renameEntity", -> - beforeEach -> - @entity_id = "4eecaffcbffa66588e000009" - @entityType = "doc" - @newName = "new.tex" - @path = mongo: "mongo.path", fileSystem: "/oldnamepath/oldname" - - @project_id = project_id - @project = - _id: ObjectId(project_id) - rootFolder: [_id:ObjectId()] - @folder = - _id: ObjectId() - name: "someFolder" - docs: [ {name: "another-doc.tex"} ] - fileRefs: [ {name: "another-file.tex"} ] - folders: [ {name: "another-folder"} ] - @doc = - _id: ObjectId() - name: "new.tex" - - @ProjectGetter.getProject.callsArgWith(2, null, @project) - @ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub() - @ProjectEntityHandler.getAllEntitiesFromProject - .onFirstCall() - .callsArgWith(1, null, @oldDocs = ['old-doc'], @oldFiles = ['old-file']) - @ProjectEntityHandler.getAllEntitiesFromProject - .onSecondCall() - .callsArgWith(1, null, @newDocs = ['new-doc'], @newFiles = ['new-file']) - - @projectLocator.findElement = sinon.stub().callsArgWith(1, null, @entity = { _id: @entity_id, name:"oldname", rev:4 }, @path, @folder) - @tpdsUpdateSender.moveEntity = sinon.stub() - @ProjectModel.findOneAndUpdate = sinon.stub().callsArgWith(3, null, @project) - @documentUpdaterHandler.updateProjectStructure = sinon.stub().yields() - - it "should should send the old and new project structure to the doc updater", (done) -> - @ProjectEntityHandler.renameEntity project_id, @entity_id, @entityType, @newName, userId, => - @documentUpdaterHandler.updateProjectStructure - .calledWith(project_id, userId, {@oldDocs, @newDocs, @oldFiles, @newFiles}) - .should.equal true - done() - - it "should update the name in mongo", (done)-> - @ProjectEntityHandler.renameEntity project_id, @entity_id, @entityType, @newName, userId, => - @ProjectModel.findOneAndUpdate.calledWith({_id: project_id}, {"$set":{"mongo.path.name": @newName}}, {"new": true}).should.equal true - done() - - it "should send the update to the tpds", (done)-> - @ProjectEntityHandler.renameEntity project_id, @entity_id, @entityType, @newName, userId, => - @tpdsUpdateSender.moveEntity.calledWith({project_id:project_id, startPath:@path.fileSystem, endPath:"/oldnamepath/new.tex", project_name:@project.name, rev:4}).should.equal true - done() - - describe "when a document already exists with the same name", -> + describe 'without pathname option', -> beforeEach -> - @project = - _id: ObjectId(project_id) - rootFolder: [_id:ObjectId()] - @folder = - _id: ObjectId() - name: "someFolder" - docs: [ {name: "another-doc.tex"} ] - fileRefs: [ {name: "another-file.tex"} ] - folders: [ {name: "another-folder"} ] - @doc = - _id: ObjectId() - name: "new.tex" - @newName = "another-doc.tex" + @ProjectEntityHandler.getDoc project_id, doc_id, @callback - it "should return an error", (done)-> - @ProjectEntityHandler.renameEntity project_id, @entity_id, @entityType, @newName, userId, (err)=> - err.should.deep.equal new Errors.InvalidNameError("file already exists") - done() + it "should call the docstore", -> + @DocstoreManager.getDoc + .calledWith(project_id, doc_id) + .should.equal true - describe "_insertDeletedDocReference", -> - beforeEach -> - @doc = - _id: ObjectId() - name: "test.tex" - @callback = sinon.stub() - @ProjectModel.update = sinon.stub().callsArgWith(3) - @ProjectEntityHandler._insertDeletedDocReference project_id, @doc, @callback + it "should call the callback with the lines, version and rev", -> + @callback.calledWith(null, @lines, @rev, @version, @ranges).should.equal true - it "should insert the doc into deletedDocs", -> - @ProjectModel.update - .calledWith({ - _id: project_id - }, { - $push: { - deletedDocs: { - _id: @doc._id - name: @doc.name - } - } - }) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "_cleanUpDoc", -> - beforeEach -> - @project = - _id: ObjectId(project_id) - @doc = - _id: ObjectId() - name: "test.tex" - @path = "/path/to/doc" - @ProjectEntityHandler.unsetRootDoc = sinon.stub().callsArg(1) - @ProjectEntityHandler._insertDeletedDocReference = sinon.stub().callsArg(2) - @documentUpdaterHandler.deleteDoc = sinon.stub().callsArg(2) - @DocstoreManager.deleteDoc = sinon.stub().callsArg(2) - @callback = sinon.stub() - - describe "when the doc is the root doc", -> + describe 'with pathname option', -> beforeEach -> - @project.rootDoc_id = @doc._id - @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, userId, @callback + @project = 'a project' + @path = mongo: "mongo.path", fileSystem: "/file/system/path" + @ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, {}, @path) + @ProjectEntityHandler.getDoc project_id, doc_id, {pathname: true}, @callback - it "should unset the root doc", -> - @ProjectEntityHandler.unsetRootDoc - .calledWith(project_id) + it "should call the project locator", -> + @ProjectLocator.findElement + .calledWith({project_id: project_id, element_id: doc_id, type: 'doc'}) .should.equal true - it "should delete the doc in the doc updater", -> - @documentUpdaterHandler.deleteDoc - .calledWith(project_id, @doc._id.toString()) - - it "should insert the doc into the deletedDocs array", -> - @ProjectEntityHandler._insertDeletedDocReference - .calledWith(@project._id, @doc) + it "should call the docstore", -> + @DocstoreManager.getDoc + .calledWith(project_id, doc_id) .should.equal true - it "should delete the doc in the doc store", -> - @DocstoreManager.deleteDoc - .calledWith(project_id, @doc._id.toString()) - .should.equal true - - it "should should send the update to the doc updater", -> - oldDocs = [ doc: @doc, path: @path ] - @documentUpdaterHandler.updateProjectStructure - .calledWith(project_id, userId, {oldDocs}) - .should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - describe "when the doc is not the root doc", -> - beforeEach -> - @project.rootDoc_id = ObjectId() - @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, userId, @callback - - it "should not unset the root doc", -> - @ProjectEntityHandler.unsetRootDoc.called.should.equal false - - it "should call the callback", -> - @callback.called.should.equal true - - - describe "_putElement", -> - beforeEach -> - @project_id = project_id - @project = - _id: ObjectId(project_id) - rootFolder: [_id:ObjectId()] - @folder = - _id: ObjectId() - name: "someFolder" - docs: [ {name: "another-doc.tex"} ] - fileRefs: [ {name: "another-file.tex"} ] - folders: [ {name: "another-folder"} ] - @doc = - _id: ObjectId() - name: "new.tex" - @path = mongo: "mongo.path", fileSystem: "/file/system/old.tex" - @ProjectGetter.getProject.callsArgWith(2, null, @project) - @projectLocator.findElement.callsArgWith(1, null, @folder, @path) - @ProjectModel.findOneAndUpdate = sinon.stub().callsArgWith(3, null, @project) - - describe "updating the project", -> - it "should use the correct mongo path", (done)-> - @ProjectEntityHandler._putElement @project, @folder._id, @doc, "docs", (err)=> - @ProjectModel.findOneAndUpdate.args[0][0]._id.should.equal @project._id - assert.deepEqual @ProjectModel.findOneAndUpdate.args[0][1].$push[@path.mongo+".docs"], @doc - done() - - it "should return the project in the callback", (done)-> - @ProjectEntityHandler._putElement @project, @folder._id, @doc, "docs", (err, path, project)=> - expect(project).to.equal @project - done() - - it "should add an s onto the type if not included", (done)-> - @ProjectEntityHandler._putElement @project, @folder._id, @doc, "doc", (err)=> - assert.deepEqual @ProjectModel.findOneAndUpdate.args[0][1].$push[@path.mongo+".docs"], @doc - done() - - it "should not call update if element is null", (done)-> - @ProjectEntityHandler._putElement @project, @folder._id, null, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - done() - - it "should default to root folder insert", (done)-> - @ProjectEntityHandler._putElement @project, null, @doc, "doc", (err)=> - @projectLocator.findElement.args[0][0].element_id.should.equal @project.rootFolder[0]._id - done() - - it "should error if the element has no _id", (done)-> - doc = - name:"something" - @ProjectEntityHandler._putElement @project, @folder._id, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - done() - - it "should error if element name contains invalid characters", (done)-> - doc = - _id: ObjectId() - name: "something*bad" - @ProjectEntityHandler._putElement @project, @folder._id, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - err.should.deep.equal new Errors.InvalidNameError("invalid element name") - done() - - it "should error if element name is too long", (done)-> - doc = - _id: ObjectId() - name: new Array(200).join("long-") + "something" - @ProjectEntityHandler._putElement @project, @folder._id, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - err.should.deep.equal new Errors.InvalidNameError("invalid element name") - done() - - it "should error if the folder name is too long", (done)-> - @path = - mongo: "mongo.path", - fileSystem: new Array(200).join("subdir/") + "foo" - @projectLocator.findElement.callsArgWith(1, null, @folder, @path) - doc = - _id: ObjectId() - name: "something" - @ProjectEntityHandler._putElement @project, @folder._id, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - err.should.deep.equal new Errors.InvalidNameError("path too long") - done() - - it "should error if a document already exists with the same name", (done)-> - doc = - _id: ObjectId() - name: "another-doc.tex" - @ProjectEntityHandler._putElement @project, @folder, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - err.should.deep.equal new Errors.InvalidNameError("file already exists") - done() - - it "should error if a file already exists with the same name", (done)-> - doc = - _id: ObjectId() - name: "another-file.tex" - @ProjectEntityHandler._putElement @project, @folder, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - err.should.deep.equal new Errors.InvalidNameError("file already exists") - done() - - it "should error if a folder already exists with the same name", (done)-> - doc = - _id: ObjectId() - name: "another-folder" - @ProjectEntityHandler._putElement @project, @folder, doc, "doc", (err)=> - @ProjectModel.findOneAndUpdate.called.should.equal false - err.should.deep.equal new Errors.InvalidNameError("file already exists") - done() - - describe "_countElements", -> - - beforeEach -> - @project.rootFolder[0].docs = [{_id:123}, {_id:345}] - @project.rootFolder[0].fileRefs = [{_id:123}, {_id:345}, {_id:456}] - @project.rootFolder[0].folders = [ - { - docs: - [{_id:123}, {_id:345}, {_id:456}] - fileRefs:{} - folders: [ - { - docs:[_id:1234], - fileRefs:[{_id:23123}, {_id:123213}, {_id:2312}] - folders:[ - { - docs:[{_id:321321}, {_id:123213}] - fileRefs:[{_id:312321}] - folders:[] - } - ] - } - ] - },{ - docs:[{_id:123}, {_id:32131}] - fileRefs:[] - folders:[ - { - docs:[{_id:3123}] - fileRefs:[{_id:321321}, {_id:321321}, {_id:313122}] - folders:0 - } - ] - } - ] - - it "should return the correct number", (done)-> - @ProjectEntityHandler._countElements @project, (err, count)-> - count.should.equal 26 - done() - - it "should deal with null folders", (done)-> - @project.rootFolder[0].folders[0].folders = undefined - @ProjectEntityHandler._countElements @project, (err, count)-> - count.should.equal 17 - done() - - it "should deal with null docs", (done)-> - @project.rootFolder[0].folders[0].docs = undefined - @ProjectEntityHandler._countElements @project, (err, count)-> - count.should.equal 23 - done() - - it "should deal with null fileRefs", (done)-> - @project.rootFolder[0].folders[0].folders[0].fileRefs = undefined - @ProjectEntityHandler._countElements @project, (err, count)-> - count.should.equal 23 - done() + it "should return the pathname if option given", -> + @callback.calledWith(null, @lines, @rev, @version, @ranges, @path.fileSystem).should.equal true diff --git a/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee new file mode 100644 index 0000000000..6b45537465 --- /dev/null +++ b/services/web/test/unit/coffee/Project/ProjectEntityMongoUpdateHandlerTests.coffee @@ -0,0 +1,592 @@ +chai = require('chai') +expect = chai.expect +assert = require('chai').assert +should = chai.should() +sinon = require 'sinon' +tk = require("timekeeper") +modulePath = "../../../../app/js/Features/Project/ProjectEntityMongoUpdateHandler" +Errors = require "../../../../app/js/Features/Errors/Errors" +ObjectId = require("mongoose").Types.ObjectId +SandboxedModule = require('sandboxed-module') + +describe 'ProjectEntityMongoUpdateHandler', -> + project_id = '4eecb1c1bffa66588e0000a1' + doc_id = '4eecb1c1bffa66588e0000a2' + file_id = '4eecb1c1bffa66588e0000a3' + folder_id = "4eecaffcbffa66588e000008" + + beforeEach -> + @FolderModel = class Folder + constructor:(options)-> + {@name} = options + @._id = "folder_id" + + @docName = "doc-name" + @fileName = "something.jpg" + @project = _id: project_id, name: 'project name' + + @callback = sinon.stub() + + tk.freeze(Date.now()) + @subject = SandboxedModule.require modulePath, requires: + 'logger-sharelatex': @logger = {log:sinon.stub(), error: sinon.stub(), err:->} + "settings-sharelatex":@settings = { + maxEntitiesPerProject: 100 + } + "../Cooldown/CooldownManager": @CooldownManager = {} + '../../models/Folder': Folder:@FolderModel + '../../models/Project': Project:@ProjectModel = {} + './ProjectEntityHandler': @ProjectEntityHandler = {} + './ProjectLocator': @ProjectLocator = {} + "./ProjectGetter": @ProjectGetter = + getProject: sinon.stub().yields(null, @project) + + afterEach -> + tk.reset() + + describe 'addDoc', -> + beforeEach -> + @subject._confirmFolder = sinon.stub().yields(folder_id) + @subject._putElement = sinon.stub() + + @doc = _id: doc_id + @subject.addDoc project_id, folder_id, @doc, @callback + + it 'gets the project', -> + @ProjectGetter.getProject + .calledWith(project_id, {rootFolder:true, name: true}) + .should.equal true + + it 'checks the folder exists', -> + @subject._confirmFolder + .calledWith(@project, folder_id) + .should.equal true + + it 'puts the element in mongo', -> + @subject._putElement + .calledWith(@project, folder_id, @doc, 'doc', @callback) + .should.equal true + + describe 'addFile', -> + beforeEach -> + @subject._confirmFolder = sinon.stub().yields(folder_id) + @subject._putElement = sinon.stub() + + @file = _id: file_id + @subject.addFile project_id, folder_id, @file, @callback + + it 'gets the project', -> + @ProjectGetter.getProject + .calledWith(project_id, {rootFolder:true, name: true}) + .should.equal true + + it 'checks the folder exists', -> + @subject._confirmFolder + .calledWith(@project, folder_id) + .should.equal true + + it 'puts the element in mongo', -> + @subject._putElement + .calledWith(@project, folder_id, @file, 'file', @callback) + .should.equal true + + describe 'replaceFile', -> + beforeEach -> + @file = _id: file_id + @path = mongo: 'file.png' + @ProjectLocator.findElement = sinon.stub().yields(null, @file, @path) + @ProjectModel.update = sinon.stub().yields() + + @subject.replaceFile project_id, file_id, @callback + + it 'gets the project', -> + @ProjectGetter.getProject + .calledWith(project_id, {rootFolder:true, name: true}) + .should.equal true + + it 'finds the element', -> + @ProjectLocator.findElement + .calledWith({ @project, element_id: file_id, type: 'file' }) + .should.equal true + + it 'increments the rev and sets the created_at', -> + @ProjectModel.update + .calledWith( + { _id: project_id }, + { + '$inc': { 'file.png.rev': 1 } + '$set': { 'file.png.created': new Date() } + } + {} + ) + .should.equal true + + it 'calls the callback', -> + @callback.calledWith(null, @file, @project, @path).should.equal true + + describe 'mkdirp', -> + beforeEach -> + @parentFolder_id = "1jnjknjk" + @newFolder = {_id:"newFolder_id_here"} + @lastFolder = {_id:"123das", folders:[]} + + @rootFolder = {_id: "rootFolderId" } + @project = _id: project_id, rootFolder: [@rootFolder] + + @ProjectGetter.getProjectWithOnlyFolders = sinon.stub().yields(null, @project) + @ProjectLocator.findElementByPath = (project_id, path, cb) => + @parentFolder = {_id:"parentFolder_id_here"} + lastFolder = path.substring(path.lastIndexOf("/")) + if lastFolder.indexOf("level1") == -1 + cb "level1 is not the last foler " + else + cb null, @parentFolder + @subject.addFolder = (project_id, parentFolder_id, folderName, callback) => + callback null, {name:folderName}, @parentFolder_id + + it 'should return the root folder if the path is just a slash', (done)-> + path = "/" + @subject.mkdirp project_id, path, (err, folders, lastFolder)=> + lastFolder.should.deep.equal @rootFolder + assert.equal lastFolder.parentFolder_id, undefined + done() + + it 'should make just one folder', (done)-> + path = "/differentFolder/" + @subject.mkdirp project_id, path, (err, folders, lastFolder)=> + folders.length.should.equal 1 + lastFolder.name.should.equal "differentFolder" + lastFolder.parentFolder_id.should.equal @parentFolder_id + done() + + it 'should make the final folder in path if it doesnt exist with one level', (done)-> + path = "level1/level2" + @subject.mkdirp project_id, path, (err, folders, lastFolder)=> + folders.length.should.equal 1 + lastFolder.name.should.equal "level2" + lastFolder.parentFolder_id.should.equal @parentFolder_id + done() + + it 'should make the final folder in path if it doesnt exist with mutliple levels', (done)-> + path = "level1/level2/level3" + + @subject.mkdirp project_id, path,(err, folders, lastFolder) => + folders.length.should.equal 2 + folders[0].name.should.equal "level2" + folders[0].parentFolder_id.should.equal @parentFolder_id + lastFolder.name.should.equal "level3" + lastFolder.parentFolder_id.should.equal @parentFolder_id + done() + + it 'should work with slashes either side', (done)-> + path = "/level1/level2/level3/" + + @subject.mkdirp project_id, path, (err, folders, lastFolder)=> + folders.length.should.equal 2 + folders[0].name.should.equal "level2" + folders[0].parentFolder_id.should.equal @parentFolder_id + lastFolder.name.should.equal "level3" + lastFolder.parentFolder_id.should.equal @parentFolder_id + done() + + describe 'moveEntity', -> + beforeEach -> + @pathAfterMove = { + fileSystem: "/somewhere/else.txt" + } + + @ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub() + @ProjectEntityHandler.getAllEntitiesFromProject + .onFirstCall() + .yields(null, @oldDocs = ['old-doc'], @oldFiles = ['old-file']) + @ProjectEntityHandler.getAllEntitiesFromProject + .onSecondCall() + .yields(null, @newDocs = ['new-doc'], @newFiles = ['new-file']) + + @doc = {lines:["1234","312343d"], rev: "1234"} + @path = { mongo:"folders[0]", fileSystem:"/old_folder/somewhere.txt" } + @ProjectLocator.findElement = sinon.stub() + .withArgs({@project, element_id: @docId, type: 'docs'}) + .yields(null, @doc, @path) + + @subject._checkValidMove = sinon.stub().yields() + + @subject._removeElementFromMongoArray = sinon.stub().yields(null, @project) + @subject._putElement = sinon.stub().yields(null, path: @pathAfterMove) + + @subject.moveEntity project_id, doc_id, folder_id, "docs", @callback + + it 'should get the project', -> + @ProjectGetter.getProject + .calledWith(project_id, {rootFolder:true, name:true}) + .should.equal true + + it 'should find the doc to move', -> + @ProjectLocator.findElement + .calledWith({element_id: doc_id, type: "docs", project: @project }) + .should.equal true + + it 'should check this is a valid move', -> + @subject._checkValidMove + .calledWith(@project, 'docs', @doc, @path, folder_id) + .should.equal true + + it 'should remove the element from its current position', -> + @subject._removeElementFromMongoArray + .calledWith(@ProjectModel, project_id, @path.mongo) + .should.equal true + + it "should put the element back in the new folder", -> + @subject._putElement + .calledWith(@project, folder_id, @doc, "docs") + .should.equal true + + it "calls the callback", -> + changes = { @oldDocs, @newDocs, @oldFiles, @newFiles } + @callback.calledWith( + null, @project.name, @path.fileSystem, @pathAfterMove.fileSystem, @doc.rev, changes + ).should.equal true + + describe 'deleteEntity', -> + beforeEach -> + @path = mongo: "mongo.path", fileSystem: "/file/system/path" + @doc = _id: doc_id + @ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @doc, @path) + @subject._removeElementFromMongoArray = sinon.stub().yields() + @subject.deleteEntity project_id, doc_id, 'doc', @callback + + it "should get the project", -> + @ProjectGetter.getProject + .calledWith(project_id, {name:true, rootFolder:true}) + .should.equal true + + it "should find the element", -> + @ProjectLocator.findElement + .calledWith({@project, element_id: @doc._id, type: 'doc'}) + .should.equal true + + it "should remove the element from the database", -> + @subject._removeElementFromMongoArray + .calledWith(@ProjectModel, project_id, @path.mongo) + .should.equal true + + it "calls the callbck", -> + @callback.calledWith(null, @doc, @path, @project).should.equal true + + describe "renameEntity", -> + beforeEach -> + @newName = "new.tex" + @path = mongo: "mongo.path", fileSystem: "/old.tex" + + @project = + _id: ObjectId(project_id) + rootFolder: [_id:ObjectId()] + @doc = _id: doc_id, name: "old.tex", rev: 1 + @folder = _id: folder_id + + @ProjectGetter.getProject = sinon.stub().yields(null, @project) + + @ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub() + @ProjectEntityHandler.getAllEntitiesFromProject + .onFirstCall() + .yields( null, @oldDocs = ['old-doc'], @oldFiles = ['old-file']) + @ProjectEntityHandler.getAllEntitiesFromProject + .onSecondCall() + .yields( null, @newDocs = ['new-doc'], @newFiles = ['new-file']) + + @ProjectLocator.findElement = sinon.stub().yields(null, @doc, @path, @folder) + @subject._checkValidElementName = sinon.stub().yields() + @ProjectModel.findOneAndUpdate = sinon.stub().callsArgWith(3, null, @project) + + @subject.renameEntity project_id, doc_id, 'doc', @newName, @callback + + it 'should get the project', -> + @ProjectGetter.getProject + .calledWith(project_id, {rootFolder:true, name:true}) + .should.equal true + + it 'should find the doc', -> + @ProjectLocator.findElement + .calledWith({element_id: doc_id, type: 'doc', project: @project }) + .should.equal true + + it 'should check the new name is valid', -> + @subject._checkValidElementName + .calledWith(@folder, @newName) + .should.equal true + + it 'should update the doc name', -> + @ProjectModel.findOneAndUpdate + .calledWith( + { _id: project_id }, + { $set: { "mongo.path.name": @newName } }, + { new: true } + ).should.equal true + + it 'calls the callback', -> + changes = { @oldDocs, @newDocs, @oldFiles, @newFiles } + @callback.calledWith( + null, @project.name, '/old.tex', '/new.tex', @doc.rev, changes + ).should.equal true + + describe 'addFolder', -> + beforeEach -> + @folderName = "folder1234" + @ProjectGetter.getProjectWithOnlyFolders = sinon.stub().callsArgWith(1, null, @project) + @subject._confirmFolder = sinon.stub().yields(folder_id) + @subject._putElement = sinon.stub().yields() + + @subject.addFolder project_id, folder_id, @folderName, @callback + + it 'gets the project', -> + @ProjectGetter.getProject + .calledWith(project_id, {rootFolder:true, name: true}) + .should.equal true + + it 'checks the parent folder exists', -> + @subject._confirmFolder + .calledWith(@project, folder_id) + .should.equal true + + it 'puts the element in mongo', -> + folderMatcher = sinon.match (folder) => + folder.name == @folderName + + @subject._putElement + .calledWithMatch(@project, folder_id, folderMatcher, 'folder') + .should.equal true + + it 'calls the callback', -> + folderMatcher = sinon.match (folder) => + folder.name == @folderName + + @callback.calledWithMatch(null, folderMatcher, folder_id).should.equal true + + describe '_removeElementFromMongoArray ', -> + beforeEach -> + @mongoPath = "folders[0].folders[5]" + @id = "12344" + @ProjectModel.update = sinon.stub().yields() + @ProjectModel.findOneAndUpdate = sinon.stub().yields(null, @project) + @subject._removeElementFromMongoArray @ProjectModel, @id, @mongoPath, @callback + + it 'should unset', -> + update = { '$unset': { } } + update['$unset'][@mongoPath] = 1 + @ProjectModel.update + .calledWith({ _id: @id }, update, {}) + .should.equal true + + it 'should pull', -> + @ProjectModel.findOneAndUpdate + .calledWith({ _id: @id }, { '$pull': { 'folders[0]': null } }, {'new': true}) + .should.equal true + + it 'should call the callback', -> + @callback.calledWith(null, @project).should.equal true + + describe "_countElements", -> + beforeEach -> + @project = + _id: project_id, + rootFolder: [ + docs: [{_id:123}, {_id:345}] + fileRefs: [{_id:123}, {_id:345}, {_id:456}] + folders: [ + { + docs: [{_id:123}, {_id:345}, {_id:456}] + fileRefs:{} + folders: [ + { + docs:[_id:1234], + fileRefs:[{_id:23123}, {_id:123213}, {_id:2312}] + folders:[ + { + docs:[{_id:321321}, {_id:123213}] + fileRefs:[{_id:312321}] + folders:[] + } + ] + } + ] + },{ + docs:[{_id:123}, {_id:32131}] + fileRefs:[] + folders:[ + { + docs:[{_id:3123}] + fileRefs:[{_id:321321}, {_id:321321}, {_id:313122}] + folders:0 + } + ] + } + ] + ] + + it "should return the correct number", -> + expect(@subject._countElements @project).to.equal(26) + + it "should deal with null folders", -> + @project.rootFolder[0].folders[0].folders = undefined + expect(@subject._countElements @project).to.equal(17) + + it "should deal with null docs", -> + @project.rootFolder[0].folders[0].docs = undefined + expect(@subject._countElements @project).to.equal(23) + + it "should deal with null fileRefs", -> + @project.rootFolder[0].folders[0].folders[0].fileRefs = undefined + expect(@subject._countElements @project).to.equal(23) + + describe "_putElement", -> + beforeEach -> + @project = + _id: project_id + rootFolder: [_id:ObjectId()] + @folder = + _id: ObjectId() + name: "someFolder" + docs: [ {name: "another-doc.tex"} ] + fileRefs: [ {name: "another-file.tex"} ] + folders: [ {name: "another-folder"} ] + @doc = + _id: ObjectId() + name: "new.tex" + @path = mongo: "mongo.path", fileSystem: "/file/system/old.tex" + @ProjectLocator.findElement = sinon.stub().yields(null, @folder, @path) + @ProjectModel.findOneAndUpdate = sinon.stub().yields(null, @project) + + describe "updating the project", -> + it "should use the correct mongo path", (done)-> + @subject._putElement @project, @folder._id, @doc, "docs", (err)=> + @ProjectModel.findOneAndUpdate.args[0][0]._id.should.equal @project._id + assert.deepEqual @ProjectModel.findOneAndUpdate.args[0][1].$push[@path.mongo+".docs"], @doc + done() + + it "should return the project in the callback", (done)-> + @subject._putElement @project, @folder._id, @doc, "docs", (err, path, project)=> + assert.equal project, @project + done() + + it "should add an s onto the type if not included", (done)-> + @subject._putElement @project, @folder._id, @doc, "doc", (err)=> + assert.deepEqual @ProjectModel.findOneAndUpdate.args[0][1].$push[@path.mongo+".docs"], @doc + done() + + it "should not call update if element is null", (done)-> + @subject._putElement @project, @folder._id, null, "doc", (err)=> + @ProjectModel.findOneAndUpdate.called.should.equal false + done() + + it "should default to root folder insert", (done)-> + @subject._putElement @project, null, @doc, "doc", (err)=> + @ProjectLocator.findElement.args[0][0].element_id.should.equal @project.rootFolder[0]._id + done() + + it "should error if the element has no _id", (done)-> + doc = + name:"something" + @subject._putElement @project, @folder._id, doc, "doc", (err)=> + @ProjectModel.findOneAndUpdate.called.should.equal false + done() + + it "should error if element name contains invalid characters", (done)-> + doc = + _id: ObjectId() + name: "something*bad" + @subject._putElement @project, @folder._id, doc, "doc", (err)=> + @ProjectModel.findOneAndUpdate.called.should.equal false + err.should.deep.equal new Errors.InvalidNameError("invalid element name") + done() + + it "should error if element name is too long", (done)-> + doc = + _id: ObjectId() + name: new Array(200).join("long-") + "something" + @subject._putElement @project, @folder._id, doc, "doc", (err)=> + @ProjectModel.findOneAndUpdate.called.should.equal false + err.should.deep.equal new Errors.InvalidNameError("invalid element name") + done() + + it "should error if the folder name is too long", (done)-> + @path = + mongo: "mongo.path", + fileSystem: new Array(200).join("subdir/") + "foo" + @ProjectLocator.findElement.callsArgWith(1, null, @folder, @path) + doc = + _id: ObjectId() + name: "something" + @subject._putElement @project, @folder._id, doc, "doc", (err)=> + @ProjectModel.findOneAndUpdate.called.should.equal false + err.should.deep.equal new Errors.InvalidNameError("path too long") + done() + + it "should error if a document already exists with the same name", (done)-> + doc = + _id: ObjectId() + name: "another-doc.tex" + @subject._putElement @project, @folder, doc, "doc", (err)=> + @ProjectModel.findOneAndUpdate.called.should.equal false + err.should.deep.equal new Errors.InvalidNameError("file already exists") + done() + + it "should error if a file already exists with the same name", (done)-> + doc = + _id: ObjectId() + name: "another-file.tex" + @subject._putElement @project, @folder, doc, "doc", (err)=> + @ProjectModel.findOneAndUpdate.called.should.equal false + err.should.deep.equal new Errors.InvalidNameError("file already exists") + done() + + it "should error if a folder already exists with the same name", (done)-> + doc = + _id: ObjectId() + name: "another-folder" + @subject._putElement @project, @folder, doc, "doc", (err)=> + @ProjectModel.findOneAndUpdate.called.should.equal false + err.should.deep.equal new Errors.InvalidNameError("file already exists") + done() + + describe '_checkValidElementName', -> + beforeEach -> + @folder = + docs: [ name: 'doc_name' ] + fileRefs: [ name: 'file_name' ] + folders: [ name: 'folder_name' ] + + it 'returns an error if name matches any doc name', -> + @subject._checkValidElementName @folder, 'doc_name', (err) -> + expect(err).to.deep.equal new Errors.InvalidNameError("file already exists") + + it 'returns an error if name matches any file name', -> + @subject._checkValidElementName @folder, 'file_name', (err) -> + expect(err).to.deep.equal new Errors.InvalidNameError("file already exists") + + it 'returns an error if name matches any folder name', -> + @subject._checkValidElementName @folder, 'folder_name', (err) -> + expect(err).to.deep.equal new Errors.InvalidNameError("file already exists") + + it 'returns nothing if name is valid', -> + @subject._checkValidElementName @folder, 'unique_name', (err) -> + expect(err).to.be.undefined + + describe '_checkValidMove', -> + beforeEach -> + @destFolder = _id: folder_id + @destFolderPath = fileSystem: '/foo/bar' + @ProjectLocator.findElement = sinon.stub().yields(null, @destFolder, @destFolderPath) + @subject._checkValidElementName = sinon.stub().yields() + + it 'checks the element name is valid', -> + @doc = _id: doc_id, name: 'doc_name' + @subject._checkValidMove @project, 'doc', @doc, fileSystem: '/main.tex', @destFolder._id, (err) => + expect(err).to.be.undefined + @subject._checkValidElementName + .calledWith(@destFolder, @doc.name) + .should.equal true + + it 'returns an error if trying to move a folder inside itself', -> + folder = name: 'folder_name' + @subject._checkValidMove @project, 'folder', folder, fileSystem: '/foo', @destFolder._id, (err) => + expect(err).to.deep.equal new Errors.InvalidNameError("destination folder is a child folder of me") diff --git a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee new file mode 100644 index 0000000000..d1fca71bc1 --- /dev/null +++ b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee @@ -0,0 +1,898 @@ +chai = require('chai') +assert = require('chai').assert +should = chai.should() +expect = chai.expect +modulePath = "../../../../app/js/Features/Project/ProjectEntityUpdateHandler" +sinon = require 'sinon' +Errors = require "../../../../app/js/Features/Errors/Errors" +SandboxedModule = require('sandboxed-module') +ObjectId = require("mongoose").Types.ObjectId + +describe 'ProjectEntityUpdateHandler', -> + project_id = '4eecb1c1bffa66588e0000a1' + doc_id = '4eecb1c1bffa66588e0000a2' + file_id = "4eecaffcbffa66588e000009" + folder_id = "4eecaffcbffa66588e000008" + rootFolderId = "4eecaffcbffa66588e000007" + userId = 1234 + + beforeEach -> + @project = _id: project_id, name: 'project name' + @fileUrl = 'filestore.example.com/file' + @FileStoreHandler = + uploadFileFromDisk: sinon.stub().yields(null, @fileUrl) + copyFile: sinon.stub().yields(null, @fileUrl) + + @DocModel = class Doc + constructor:(options)-> + {@name, @lines} = options + @_id = doc_id + @rev = 0 + @FileModel = class File + constructor:(options)-> + {@name} = options + @._id = file_id + @rev = 0 + + @docName = "doc-name" + @docLines = ['1234','abc'] + + @fileName = "something.jpg" + @fileSystemPath = "somehintg" + + @source = 'editor' + @callback = sinon.stub() + @ProjectEntityUpdateHandler = SandboxedModule.require modulePath, requires: + 'logger-sharelatex': @logger = {log:sinon.stub(), error: sinon.stub(), err:->} + '../../models/Doc': Doc:@DocModel + '../Docstore/DocstoreManager': @DocstoreManager = {} + '../Errors/Errors': Errors + '../../Features/DocumentUpdater/DocumentUpdaterHandler':@DocumentUpdaterHandler = + updateProjectStructure: sinon.stub().yields() + '../../models/File': File:@FileModel + '../FileStore/FileStoreHandler':@FileStoreHandler + "../../infrastructure/LockManager":@LockManager = + runWithLock: + sinon.spy((key, runner, callback) -> runner(callback)) + '../../models/Project': Project:@ProjectModel = {} + "./ProjectGetter": @ProjectGetter = {} + './ProjectLocator': @ProjectLocator = {} + './ProjectUpdateHandler': @ProjectUpdater = {} + './ProjectEntityHandler': @ProjectEntityHandler = {} + './ProjectEntityMongoUpdateHandler': @ProjectEntityMongoUpdateHandler = {} + '../ThirdPartyDataStore/TpdsUpdateSender':@TpdsUpdateSender = + addFile: sinon.stub().yields() + + describe 'copyFileFromExistingProjectWithProject', -> + + beforeEach -> + @oldProject_id = "123kljadas" + @oldFileRef = {name:@fileName, _id:"oldFileRef"} + @ProjectEntityMongoUpdateHandler._confirmFolder = sinon.stub().yields(folder_id) + @ProjectEntityMongoUpdateHandler._putElement = sinon.stub().yields(null, {path:{fileSystem: @fileSystemPath}}) + + @ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject @project, folder_id, @oldProject_id, @oldFileRef, userId, @callback + + it 'should copy the file in FileStoreHandler', -> + @FileStoreHandler.copyFile + .calledWith(@oldProject_id, @oldFileRef._id, project_id, file_id) + .should.equal true + + it 'should put file into folder by calling put element', -> + @ProjectEntityMongoUpdateHandler._putElement + .calledWithMatch(@project, folder_id, { _id: file_id, name: @fileName }, "file") + .should.equal true + + it 'should return doc and parent folder', -> + @callback.calledWithMatch(null,{ _id: file_id, name: @fileName }, folder_id).should.equal true + + it 'should call third party data store if versioning is enabled', -> + @TpdsUpdateSender.addFile.calledWith( + project_id: project_id + file_id: file_id + path: @fileSystemPath + rev: 0 + project_name: @project.name + ).should.equal true + + it "should should send the change in project structure to the doc updater", -> + changesMatcher = sinon.match (changes) => + { newFiles } = changes + return false if newFiles.length != 1 + newFile = newFiles[0] + newFile.file._id == file_id && + newFile.path == @fileSystemPath && + newFile.url == @fileUrl + + @DocumentUpdaterHandler.updateProjectStructure + .calledWithMatch(project_id, userId, changesMatcher) + .should.equal true + + describe 'updateDocLines', -> + beforeEach -> + @path = "/somewhere/something.tex" + @doc = { + _id: doc_id + } + @version = 42 + @ranges = {"mock":"ranges"} + @ProjectGetter.getProjectWithoutDocLines = sinon.stub().yields(null, @project) + @ProjectLocator.findElement = sinon.stub().yields(null, @doc, {fileSystem: @path}) + @TpdsUpdateSender.addDoc = sinon.stub().yields() + @ProjectUpdater.markAsUpdated = sinon.stub() + @callback = sinon.stub() + + describe "when the doc has been modified", -> + beforeEach -> + @DocstoreManager.updateDoc = sinon.stub().yields(null, true, @rev = 5) + @ProjectEntityUpdateHandler.updateDocLines project_id, doc_id, @docLines, @version, @ranges, @callback + + it "should get the project without doc lines", -> + @ProjectGetter.getProjectWithoutDocLines + .calledWith(project_id) + .should.equal true + + it "should find the doc", -> + @ProjectLocator.findElement + .calledWith({ + project: @project + type: "docs" + element_id: doc_id + }) + .should.equal true + + it "should update the doc in the docstore", -> + @DocstoreManager.updateDoc + .calledWith(project_id, doc_id, @docLines, @version, @ranges) + .should.equal true + + it "should mark the project as updated", -> + @ProjectUpdater.markAsUpdated + .calledWith(project_id) + .should.equal true + + it "should send the doc the to the TPDS", -> + @TpdsUpdateSender.addDoc + .calledWith({ + project_id: project_id + project_name: @project.name + doc_id: doc_id + rev: @rev + path: @path + }) + .should.equal true + + it "should call the callback", -> + @callback.called.should.equal true + + describe "when the doc has not been modified", -> + beforeEach -> + @DocstoreManager.updateDoc = sinon.stub().yields(null, false, @rev = 5) + @ProjectEntityUpdateHandler.updateDocLines project_id, doc_id, @docLines, @version, @ranges, @callback + + it "should not mark the project as updated", -> + @ProjectUpdater.markAsUpdated.called.should.equal false + + it "should not send the doc the to the TPDS", -> + @TpdsUpdateSender.addDoc.called.should.equal false + + it "should call the callback", -> + @callback.called.should.equal true + + describe "when the doc has been deleted", -> + beforeEach -> + @project.deletedDocs = [ _id: doc_id ] + @ProjectGetter.getProjectWithoutDocLines = sinon.stub().yields(null, @project) + @ProjectLocator.findElement = sinon.stub().yields(new Errors.NotFoundError) + @DocstoreManager.updateDoc = sinon.stub().yields() + @ProjectEntityUpdateHandler.updateDocLines project_id, doc_id, @docLines, @version, @ranges, @callback + + it "should update the doc in the docstore", -> + @DocstoreManager.updateDoc + .calledWith(project_id, doc_id, @docLines, @version, @ranges) + .should.equal true + + it "should not mark the project as updated", -> + @ProjectUpdater.markAsUpdated.called.should.equal false + + it "should not send the doc the to the TPDS", -> + @TpdsUpdateSender.addDoc.called.should.equal false + + it "should call the callback", -> + @callback.called.should.equal true + + describe "when the doc is not related to the project", -> + beforeEach -> + @ProjectLocator.findElement = sinon.stub().yields() + @ProjectEntityUpdateHandler.updateDocLines project_id, doc_id, @docLines, @version, @ranges, @callback + + it "should log out the error", -> + @logger.error + .calledWith( + project_id: project_id + doc_id: doc_id + lines: @docLines + "doc not found while updating doc lines" + ) + .should.equal true + + it "should return a not found error", -> + @callback.calledWith(new Errors.NotFoundError()).should.equal true + + describe "when the project is not found", -> + beforeEach -> + @ProjectGetter.getProjectWithoutDocLines = sinon.stub().yields() + @ProjectEntityUpdateHandler.updateDocLines project_id, doc_id, @docLines, @version, @ranges, @callback + + it "should return a not found error", -> + @callback.calledWith(new Errors.NotFoundError()).should.equal true + + describe "setRootDoc", -> + it "should call Project.update", -> + rootDoc_id = "root-doc-id-123123" + @ProjectModel.update = sinon.stub() + @ProjectEntityUpdateHandler.setRootDoc project_id, rootDoc_id + @ProjectModel.update + .calledWith({_id : project_id}, {rootDoc_id}) + .should.equal true + + describe "unsetRootDoc", -> + it "should call Project.update", -> + @ProjectModel.update = sinon.stub() + @ProjectEntityUpdateHandler.unsetRootDoc project_id + @ProjectModel.update + .calledWith({_id : project_id}, {$unset : {rootDoc_id: true}}) + .should.equal true + + describe "restoreDoc", -> + beforeEach -> + @doc = { "mock": "doc" } + @ProjectEntityHandler.getDoc = sinon.stub().yields(null, @docLines) + @ProjectEntityUpdateHandler.addDoc = sinon.stub().yields(null, @doc, folder_id) + + @ProjectEntityUpdateHandler.restoreDoc project_id, doc_id, @docName, @callback + + it 'should get the doc lines', -> + @ProjectEntityHandler.getDoc + .calledWith(project_id, doc_id, include_deleted: true) + .should.equal true + + it "should add a new doc with these doc lines", -> + @ProjectEntityUpdateHandler.addDoc + .calledWith(project_id, null, @docName, @docLines) + .should.equal true + + it "should call the callback with the new folder and doc", -> + @callback.calledWith(null, @doc, folder_id).should.equal true + + describe 'addDoc', -> + beforeEach -> + @path = "/path/to/doc" + + @newDoc = _id: doc_id + @ProjectEntityUpdateHandler.addDocWithoutUpdatingHistory = + withoutLock: sinon.stub().yields(null, @newDoc, folder_id, @path) + @ProjectEntityUpdateHandler.addDoc project_id, folder_id, @docName, @docLines, userId, @callback + + it "creates the doc without history", () -> + @ProjectEntityUpdateHandler.addDocWithoutUpdatingHistory.withoutLock + .calledWith(project_id, folder_id, @docName, @docLines, userId) + .should.equal true + + it "sends the change in project structure to the doc updater", () -> + newDocs = [ + doc: @newDoc + path: @path + docLines: @docLines.join('\n') + ] + @DocumentUpdaterHandler.updateProjectStructure + .calledWith(project_id, userId, {newDocs}) + .should.equal true + + describe 'addFile', -> + beforeEach -> + @path = "/path/to/file" + + @newFile = _id: file_id + @ProjectEntityUpdateHandler.addFileWithoutUpdatingHistory = + withoutLock: sinon.stub().yields(null, @newFile, folder_id, @path, @fileUrl) + @ProjectEntityUpdateHandler.addFile project_id, folder_id, @docName, @fileSystemPath, userId, @callback + + it "creates the doc without history", () -> + @ProjectEntityUpdateHandler.addFileWithoutUpdatingHistory.withoutLock + .calledWith(project_id, folder_id, @docName, @fileSystemPath, userId) + .should.equal true + + it "sends the change in project structure to the doc updater", () -> + newFiles = [ + file: @newFile + path: @path + url: @fileUrl + ] + @DocumentUpdaterHandler.updateProjectStructure + .calledWith(project_id, userId, {newFiles}) + .should.equal true + + describe 'replaceFile', -> + beforeEach -> + @newFile = _id: file_id, rev: 0 + @path = "/path/to/file" + @project = _id: project_id, name: 'some project' + @ProjectEntityMongoUpdateHandler.replaceFile = sinon.stub().yields(null, @newFile, @project, fileSystem: @path) + + @ProjectEntityUpdateHandler.replaceFile project_id, file_id, @fileSystemPath, userId, @callback + + it 'uploads a new version of the file', -> + @FileStoreHandler.uploadFileFromDisk + .calledWith(project_id, file_id, @fileSystemPath) + .should.equal true + + it 'replaces the file in mongo', -> + @ProjectEntityMongoUpdateHandler.replaceFile + .calledWith(project_id, file_id) + .should.equal true + + it 'notifies the tpds', -> + @TpdsUpdateSender.addFile + .calledWith({ + project_id: project_id + project_name: @project.name + file_id: file_id + rev: @newFile.rev + 1 + path: @path + }) + .should.equal true + + it 'updates the project structure in the doc updater', -> + newFiles = [ + file: @newFile + path: @path + url: @fileUrl + ] + @DocumentUpdaterHandler.updateProjectStructure + .calledWith(project_id, userId, {newFiles}) + .should.equal true + + describe 'addDocWithoutUpdatingHistory', -> + beforeEach -> + @path = "/path/to/doc" + + @project = _id: project_id, name: 'some project' + + @TpdsUpdateSender.addDoc = sinon.stub().yields() + @DocstoreManager.updateDoc = sinon.stub().yields(null, false, @rev = 5) + @ProjectEntityMongoUpdateHandler.addDoc = sinon.stub().yields(null, {path: fileSystem: @path}, @project) + @ProjectEntityUpdateHandler.addDocWithoutUpdatingHistory project_id, folder_id, @docName, @docLines, userId, @callback + + it "updates the doc in the docstore", () -> + @DocstoreManager.updateDoc + .calledWith(project_id, doc_id, @docLines, 0, {}) + .should.equal true + + it "updates the doc in mongo", () -> + docMatcher = sinon.match (doc) => + doc.name == @docName + + @ProjectEntityMongoUpdateHandler.addDoc + .calledWithMatch(project_id, folder_id, docMatcher) + .should.equal true + + it "notifies the tpds", () -> + @TpdsUpdateSender.addDoc + .calledWith({ + project_id: project_id + project_name: @project.name + doc_id: doc_id + rev: 0 + path: @path + }) + .should.equal true + + it "should not should send the change in project structure to the doc updater", () -> + @DocumentUpdaterHandler.updateProjectStructure + .called + .should.equal false + + describe 'addFileWithoutUpdatingHistory', -> + beforeEach -> + @path = "/path/to/file" + + @project = _id: project_id, name: 'some project' + + @TpdsUpdateSender.addFile = sinon.stub().yields() + @ProjectEntityMongoUpdateHandler.addFile = sinon.stub().yields(null, {path: fileSystem: @path}, @project) + @ProjectEntityUpdateHandler.addFileWithoutUpdatingHistory project_id, folder_id, @fileName, @fileSystemPath, userId, @callback + + it "updates the file in the filestore", () -> + @FileStoreHandler.uploadFileFromDisk + .calledWith(project_id, file_id, @fileSystemPath) + .should.equal true + + it "updates the file in mongo", () -> + fileMatcher = sinon.match (file) => + file.name == @fileName + + @ProjectEntityMongoUpdateHandler.addFile + .calledWithMatch(project_id, folder_id, fileMatcher) + .should.equal true + + it "notifies the tpds", () -> + @TpdsUpdateSender.addFile + .calledWith({ + project_id: project_id + project_name: @project.name + file_id: file_id + rev: 0 + path: @path + }) + .should.equal true + + it "should not should send the change in project structure to the doc updater", () -> + @DocumentUpdaterHandler.updateProjectStructure + .called + .should.equal false + + describe 'upsertDoc', -> + describe 'upserting into an invalid folder', -> + beforeEach -> + @ProjectLocator.findElement = sinon.stub().yields() + @ProjectEntityUpdateHandler.upsertDoc project_id, folder_id, @docName, @docLines, @source, userId, @callback + + it 'returns an error', -> + errorMatcher = sinon.match.instanceOf(Error) + @callback.calledWithMatch(errorMatcher) + .should.equal true + + describe 'updating an existing doc', -> + beforeEach -> + @existingDoc = _id: doc_id, name: @docName + @folder = _id: folder_id, docs: [@existingDoc] + @ProjectLocator.findElement = sinon.stub().yields(null, @folder) + @DocumentUpdaterHandler.setDocument = sinon.stub().yields() + @DocumentUpdaterHandler.flushDocToMongo = sinon.stub().yields() + + @ProjectEntityUpdateHandler.upsertDoc project_id, folder_id, @docName, @docLines, @source, userId, @callback + + it 'tries to find the folder', -> + @ProjectLocator.findElement + .calledWith({project_id, element_id: folder_id, type: "folder"}) + .should.equal true + + it 'updates the doc contents', -> + @DocumentUpdaterHandler.setDocument + .calledWith(project_id, @existingDoc._id, userId, @docLines, @source) + .should.equal true + + it 'flushes the doc contents', -> + @DocumentUpdaterHandler.flushDocToMongo + .calledWith(project_id, @existingDoc._id ) + .should.equal true + + it 'returns the doc', -> + @callback.calledWith(null, @existingDoc, false) + + describe 'creating a new doc', -> + beforeEach -> + @folder = _id: folder_id, docs: [] + @newDoc = _id: doc_id + @ProjectLocator.findElement = sinon.stub().yields(null, @folder) + @ProjectEntityUpdateHandler.addDoc = withoutLock: sinon.stub().yields(null, @newDoc) + + @ProjectEntityUpdateHandler.upsertDoc project_id, folder_id, @docName, @docLines, @source, userId, @callback + + it 'tries to find the folder', -> + @ProjectLocator.findElement + .calledWith({project_id, element_id: folder_id, type: "folder"}) + .should.equal true + + it 'adds the doc', -> + @ProjectEntityUpdateHandler.addDoc.withoutLock + .calledWith(project_id, folder_id, @docName, @docLines, userId) + .should.equal true + + it 'returns the doc', -> + @callback.calledWith(null, @newDoc, true) + + describe 'upsertFile', -> + describe 'upserting into an invalid folder', -> + beforeEach -> + @ProjectLocator.findElement = sinon.stub().yields() + @ProjectEntityUpdateHandler.upsertFile project_id, folder_id, @fileName, @fileSystemPath, userId, @callback + + it 'returns an error', -> + errorMatcher = sinon.match.instanceOf(Error) + @callback.calledWithMatch(errorMatcher) + .should.equal true + + describe 'updating an existing file', -> + beforeEach -> + @existingFile = _id: file_id, name: @fileName + @folder = _id: folder_id, fileRefs: [@existingFile] + @ProjectLocator.findElement = sinon.stub().yields(null, @folder) + @ProjectEntityUpdateHandler.replaceFile = withoutLock: sinon.stub().yields(null, @newFile) + + @ProjectEntityUpdateHandler.upsertFile project_id, folder_id, @fileName, @fileSystemPath, userId, @callback + + it 'replaces the file', -> + @ProjectEntityUpdateHandler.replaceFile.withoutLock + .calledWith(project_id, file_id, @fileSystemPath, userId) + .should.equal true + + it 'returns the file', -> + @callback.calledWith(null, @existingFile, false) + + describe 'creating a new file', -> + beforeEach -> + @folder = _id: folder_id, fileRefs: [] + @newFile = _id: file_id + @ProjectLocator.findElement = sinon.stub().yields(null, @folder) + @ProjectEntityUpdateHandler.addFile = withoutLock: sinon.stub().yields(null, @newFile) + + @ProjectEntityUpdateHandler.upsertFile project_id, folder_id, @fileName, @fileSystemPath, userId, @callback + + it 'tries to find the folder', -> + @ProjectLocator.findElement + .calledWith({project_id, element_id: folder_id, type: "folder"}) + .should.equal true + + it 'adds the file', -> + @ProjectEntityUpdateHandler.addFile.withoutLock + .calledWith(project_id, folder_id, @fileName, @fileSystemPath, userId) + .should.equal true + + it 'returns the file', -> + @callback.calledWith(null, @newFile, true) + + describe 'upsertDocWithPath', -> + beforeEach -> + @path = "/folder/doc.tex" + @newFolders = [ 'mock-a', 'mock-b' ] + @folder = _id: folder_id + @doc = _id: doc_id + @isNewDoc = true + @ProjectEntityUpdateHandler.mkdirp = + withoutLock: sinon.stub().yields(null, @newFolders, @folder) + @ProjectEntityUpdateHandler.upsertDoc = + withoutLock: sinon.stub().yields(null, @doc, @isNewDoc) + + @ProjectEntityUpdateHandler.upsertDocWithPath project_id, @path, @docLines, @source, userId, @callback + + it 'creates any necessary folders', -> + @ProjectEntityUpdateHandler.mkdirp.withoutLock + .calledWith(project_id, '/folder') + .should.equal true + + it 'upserts the doc', -> + @ProjectEntityUpdateHandler.upsertDoc.withoutLock + .calledWith(project_id, @folder._id, 'doc.tex', @docLines, @source, userId) + .should.equal true + + it 'calls the callback', -> + @callback + .calledWith(null, @doc, @isNewDoc, @newFolders, @folder) + .should.equal true + + describe 'upsertFileWithPath', -> + beforeEach -> + @path = "/folder/file.png" + @newFolders = [ 'mock-a', 'mock-b' ] + @folder = _id: folder_id + @file = _id: file_id + @isNewFile = true + @ProjectEntityUpdateHandler.mkdirp = + withoutLock: sinon.stub().yields(null, @newFolders, @folder) + @ProjectEntityUpdateHandler.upsertFile = + withoutLock: sinon.stub().yields(null, @file, @isNewFile) + + @ProjectEntityUpdateHandler.upsertFileWithPath project_id, @path, @fileSystemPath, userId, @callback + + it 'creates any necessary folders', -> + @ProjectEntityUpdateHandler.mkdirp.withoutLock + .calledWith(project_id, '/folder') + .should.equal true + + it 'upserts the file', -> + @ProjectEntityUpdateHandler.upsertFile.withoutLock + .calledWith(project_id, @folder._id, 'file.png', @fileSystemPath, userId) + .should.equal true + + it 'calls the callback', -> + @callback + .calledWith(null, @file, @isNewFile, @newFolders, @folder) + .should.equal true + + describe 'deleteEntity', -> + beforeEach -> + @path = '/path/to/doc.tex' + @doc = _id: doc_id + @projectBeforeDeletion = _id: project_id, name: 'project' + @ProjectEntityMongoUpdateHandler.deleteEntity = sinon.stub().yields(null, @doc, {fileSystem: @path}, @projectBeforeDeletion) + @ProjectEntityUpdateHandler._cleanUpEntity = sinon.stub().yields() + @TpdsUpdateSender.deleteEntity = sinon.stub().yields() + + @ProjectEntityUpdateHandler.deleteEntity project_id, doc_id, 'doc', userId, @callback + + it 'deletes the entity in mongo', -> + @ProjectEntityMongoUpdateHandler.deleteEntity + .calledWith(project_id, doc_id, 'doc') + .should.equal true + + it 'cleans up the doc in the docstore', -> + @ProjectEntityUpdateHandler._cleanUpEntity + .calledWith(@projectBeforeDeletion, @doc, 'doc', @path, userId) + .should.equal true + + it 'it notifies the tpds', -> + @TpdsUpdateSender.deleteEntity + .calledWith({ project_id, @path, project_name: @projectBeforeDeletion.name }) + .should.equal true + + it 'retuns the entity_id', -> + @callback.calledWith(null, doc_id).should.equal true + + describe 'deleteEntityWithPath', -> + describe 'when the entity exists', -> + beforeEach -> + @doc = _id: doc_id + @ProjectLocator.findElementByPath = sinon.stub().yields(null, @doc, 'doc') + @ProjectEntityUpdateHandler.deleteEntity = + withoutLock: sinon.stub().yields() + @path = '/path/to/doc.tex' + @ProjectEntityUpdateHandler.deleteEntityWithPath project_id, @path, userId, @callback + + it 'finds the entity', -> + @ProjectLocator.findElementByPath + .calledWith(project_id, @path) + .should.equal true + + it 'deletes the entity', -> + @ProjectEntityUpdateHandler.deleteEntity.withoutLock + .calledWith(project_id, @doc._id, 'doc', userId, @callback) + .should.equal true + + describe 'when the entity does not exist', -> + beforeEach -> + @ProjectLocator.findElementByPath = sinon.stub().yields() + @path = '/doc.tex' + @ProjectEntityUpdateHandler.deleteEntityWithPath project_id, @path, userId, @callback + + it 'returns an error', -> + @callback.calledWith(new Errors.NotFoundError()).should.equal true + + describe 'mkdirp', -> + beforeEach -> + @docPath = '/folder/doc.tex' + @ProjectEntityMongoUpdateHandler.mkdirp = sinon.stub().yields() + @ProjectEntityUpdateHandler.mkdirp project_id, @docPath, @callback + + it 'calls ProjectEntityMongoUpdateHandler', -> + @ProjectEntityMongoUpdateHandler.mkdirp + .calledWith(project_id, @docPath) + .should.equal true + + describe 'addFolder', -> + beforeEach -> + @parentFolder_id = '123asdf' + @folderName = 'new-folder' + @ProjectEntityMongoUpdateHandler.addFolder = sinon.stub().yields() + @ProjectEntityUpdateHandler.addFolder project_id, @parentFolder_id, @folderName, @callback + + it 'calls ProjectEntityMongoUpdateHandler', -> + @ProjectEntityMongoUpdateHandler.addFolder + .calledWith(project_id, @parentFolder_id, @folderName) + .should.equal true + + describe 'moveEntity', -> + beforeEach -> + @project_name = 'project name' + @startPath = '/a.tex' + @endPath = '/folder/b.tex' + @rev = 2 + @changes = newDocs: ['old-doc'], newFiles: ['old-file'] + @ProjectEntityMongoUpdateHandler.moveEntity = sinon.stub().yields( + null, @project_name, @startPath, @endPath, @rev, @changes + ) + @TpdsUpdateSender.moveEntity = sinon.stub() + @DocumentUpdaterHandler.updateProjectStructure = sinon.stub() + + @ProjectEntityUpdateHandler.moveEntity project_id, doc_id, folder_id, 'doc', userId, @callback + + it 'moves the entity in mongo', -> + @ProjectEntityMongoUpdateHandler.moveEntity + .calledWith(project_id, doc_id, folder_id, 'doc') + .should.equal true + + it 'notifies tpds', -> + @TpdsUpdateSender.moveEntity + .calledWith({project_id, @project_name, @startPath, @endPath, @rev}) + .should.equal true + + it 'sends the changes in project structure to the doc updater', -> + @DocumentUpdaterHandler.updateProjectStructure + .calledWith(project_id, userId, @changes, @callback) + .should.equal true + + describe "renameEntity", -> + beforeEach -> + @project_name = 'project name' + @startPath = '/folder/a.tex' + @endPath = '/folder/b.tex' + @rev = 2 + @changes = newDocs: ['old-doc'], newFiles: ['old-file'] + @newDocName = 'b.tex' + @ProjectEntityMongoUpdateHandler.renameEntity = sinon.stub().yields( + null, @project_name, @startPath, @endPath, @rev, @changes + ) + @TpdsUpdateSender.moveEntity = sinon.stub() + @DocumentUpdaterHandler.updateProjectStructure = sinon.stub() + + @ProjectEntityUpdateHandler.renameEntity project_id, doc_id, 'doc', @newDocName, userId, @callback + + it 'moves the entity in mongo', -> + @ProjectEntityMongoUpdateHandler.renameEntity + .calledWith(project_id, doc_id, 'doc', @newDocName) + .should.equal true + + it 'notifies tpds', -> + @TpdsUpdateSender.moveEntity + .calledWith({project_id, @project_name, @startPath, @endPath, @rev}) + .should.equal true + + it 'sends the changes in project structure to the doc updater', -> + @DocumentUpdaterHandler.updateProjectStructure + .calledWith(project_id, userId, @changes, @callback) + .should.equal true + + describe "_cleanUpEntity", -> + beforeEach -> + @entity_id = "4eecaffcbffa66588e000009" + @FileStoreHandler.deleteFile = sinon.stub().yields() + @DocumentUpdaterHandler.deleteDoc = sinon.stub().yields() + @ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields() + + describe "a file", -> + beforeEach (done) -> + @path = "/file/system/path.png" + @entity = _id: @entity_id + @ProjectEntityUpdateHandler._cleanUpEntity @project, @entity, 'file', @path, userId, done + + it "should delete the file from FileStoreHandler", -> + @FileStoreHandler.deleteFile.calledWith(project_id, @entity_id).should.equal true + + it "should not attempt to delete from the document updater", -> + @DocumentUpdaterHandler.deleteDoc.called.should.equal false + + it "should should send the update to the doc updater", -> + oldFiles = [ file: @entity, path: @path ] + @DocumentUpdaterHandler.updateProjectStructure + .calledWith(project_id, userId, {oldFiles}) + .should.equal true + + describe "a doc", -> + beforeEach (done) -> + @path = "/file/system/path.tex" + @ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().yields() + @entity = {_id: @entity_id} + @ProjectEntityUpdateHandler._cleanUpEntity @project, @entity, 'doc', @path, userId, done + + it "should clean up the doc", -> + @ProjectEntityUpdateHandler._cleanUpDoc + .calledWith(@project, @entity, @path, userId) + .should.equal true + + describe "a folder", -> + beforeEach (done) -> + @folder = + folders: [ + name: "subfolder" + fileRefs: [ @file1 = { _id: "file-id-1", name: "file-name-1"} ] + docs: [ @doc1 = { _id: "doc-id-1", name: "doc-name-1" } ] + folders: [] + ] + fileRefs: [ @file2 = { _id: "file-id-2", name: "file-name-2" } ] + docs: [ @doc2 = { _id: "doc-id-2", name: "doc-name-2" } ] + + @ProjectEntityUpdateHandler._cleanUpDoc = sinon.stub().yields() + @ProjectEntityUpdateHandler._cleanUpFile = sinon.stub().yields() + path = "/folder" + @ProjectEntityUpdateHandler._cleanUpEntity @project, @folder, "folder", path, userId, done + + it "should clean up all sub files", -> + @ProjectEntityUpdateHandler._cleanUpFile + .calledWith(@project, @file1, "/folder/subfolder/file-name-1", userId) + .should.equal true + @ProjectEntityUpdateHandler._cleanUpFile + .calledWith(@project, @file2, "/folder/file-name-2", userId) + .should.equal true + + it "should clean up all sub docs", -> + @ProjectEntityUpdateHandler._cleanUpDoc + .calledWith(@project, @doc1, "/folder/subfolder/doc-name-1", userId) + .should.equal true + @ProjectEntityUpdateHandler._cleanUpDoc + .calledWith(@project, @doc2, "/folder/doc-name-2", userId) + .should.equal true + + describe "_cleanUpDoc", -> + beforeEach -> + @project = + _id: ObjectId(project_id) + @doc = + _id: ObjectId() + name: "test.tex" + @path = "/path/to/doc" + @ProjectEntityUpdateHandler.unsetRootDoc = sinon.stub().yields() + @ProjectEntityUpdateHandler._insertDeletedDocReference = sinon.stub().yields() + @DocumentUpdaterHandler.deleteDoc = sinon.stub().yields() + @DocstoreManager.deleteDoc = sinon.stub().yields() + @callback = sinon.stub() + + describe "when the doc is the root doc", -> + beforeEach -> + @project.rootDoc_id = @doc._id + @ProjectEntityUpdateHandler._cleanUpDoc @project, @doc, @path, userId, @callback + + it "should unset the root doc", -> + @ProjectEntityUpdateHandler.unsetRootDoc + .calledWith(project_id) + .should.equal true + + it "should delete the doc in the doc updater", -> + @DocumentUpdaterHandler.deleteDoc + .calledWith(project_id, @doc._id.toString()) + + it "should insert the doc into the deletedDocs array", -> + @ProjectEntityUpdateHandler._insertDeletedDocReference + .calledWith(@project._id, @doc) + .should.equal true + + it "should delete the doc in the doc store", -> + @DocstoreManager.deleteDoc + .calledWith(project_id, @doc._id.toString()) + .should.equal true + + it "should should send the update to the doc updater", -> + oldDocs = [ doc: @doc, path: @path ] + @DocumentUpdaterHandler.updateProjectStructure + .calledWith(project_id, userId, {oldDocs}) + .should.equal true + + it "should call the callback", -> + @callback.called.should.equal true + + describe "when the doc is not the root doc", -> + beforeEach -> + @project.rootDoc_id = ObjectId() + @ProjectEntityUpdateHandler._cleanUpDoc @project, @doc, @path, userId, @callback + + it "should not unset the root doc", -> + @ProjectEntityUpdateHandler.unsetRootDoc.called.should.equal false + + it "should call the callback", -> + @callback.called.should.equal true + + describe "_insertDeletedDocReference", -> + beforeEach -> + @doc = + _id: ObjectId() + name: "test.tex" + @callback = sinon.stub() + @ProjectModel.update = sinon.stub().yields() + @ProjectEntityUpdateHandler._insertDeletedDocReference project_id, @doc, @callback + + it "should insert the doc into deletedDocs", -> + @ProjectModel.update + .calledWith({ + _id: project_id + }, { + $push: { + deletedDocs: { + _id: @doc._id + name: @doc.name + } + } + }) + .should.equal true + + it "should call the callback", -> + @callback.called.should.equal true diff --git a/services/web/test/unit/coffee/Project/ProjectGetterTests.coffee b/services/web/test/unit/coffee/Project/ProjectGetterTests.coffee index 3a9138cd64..f7dbbb6a62 100644 --- a/services/web/test/unit/coffee/Project/ProjectGetterTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectGetterTests.coffee @@ -19,6 +19,9 @@ describe "ProjectGetter", -> "metrics-sharelatex": timeAsyncMethod: sinon.stub() "../../models/Project": Project: @Project = {} "../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {} + "../../infrastructure/LockManager": @LockManager = + mongoTransactionLock: + runWithLock : sinon.spy((key, runner, callback) -> runner(callback)) "logger-sharelatex": err:-> log:-> @@ -61,7 +64,7 @@ describe "ProjectGetter", -> @project = _id: @project_id = "56d46b0a1d3422b87c5ebcb1" @db.projects.find = sinon.stub().callsArgWith(2, null, [@project]) - + describe "passing an id", -> beforeEach -> @ProjectGetter.getProjectWithOnlyFolders @project_id, @callback @@ -101,7 +104,7 @@ describe "ProjectGetter", -> @project = _id: @project_id = "56d46b0a1d3422b87c5ebcb1" @db.projects.find = sinon.stub().callsArgWith(2, null, [@project]) - + describe "passing an id", -> beforeEach -> @ProjectGetter.getProjectWithOnlyFolders @project_id, @callback @@ -159,7 +162,7 @@ describe "ProjectGetter", -> } ) @ProjectGetter.findAllUsersProjects @user_id, @fields, @callback - + it "should call the callback with all the projects", -> @callback .calledWith(null, { diff --git a/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee b/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee index c4b7d17d11..9a8cde3ff5 100644 --- a/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee @@ -11,6 +11,7 @@ describe 'ProjectRootDocManager', -> @callback = sinon.stub() @ProjectRootDocManager = SandboxedModule.require modulePath, requires: "./ProjectEntityHandler" : @ProjectEntityHandler = {} + "./ProjectEntityUpdateHandler" : @ProjectEntityUpdateHandler = {} describe "setRootDocAutomatically", -> describe "when there is a suitable root doc", -> @@ -30,7 +31,7 @@ describe 'ProjectRootDocManager', -> lines: ["Hello world"] @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) - @ProjectEntityHandler.setRootDoc = sinon.stub().callsArgWith(2) + @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) @ProjectRootDocManager.setRootDocAutomatically @project_id, done it "should check the docs of the project", -> @@ -38,7 +39,7 @@ describe 'ProjectRootDocManager', -> .should.equal true it "should set the root doc to the doc containing a documentclass", -> - @ProjectEntityHandler.setRootDoc.calledWith(@project_id, "doc-id-2") + @ProjectEntityUpdateHandler.setRootDoc.calledWith(@project_id, "doc-id-2") .should.equal true describe "when the root doc is an Rtex file", -> @@ -51,11 +52,11 @@ describe 'ProjectRootDocManager', -> _id: "doc-id-2" lines: ["\\documentclass{article}", "\\input{chapter1}"] @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) - @ProjectEntityHandler.setRootDoc = sinon.stub().callsArgWith(2) + @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) @ProjectRootDocManager.setRootDocAutomatically @project_id, @callback it "should set the root doc to the doc containing a documentclass", -> - @ProjectEntityHandler.setRootDoc.calledWith(@project_id, "doc-id-2") + @ProjectEntityUpdateHandler.setRootDoc.calledWith(@project_id, "doc-id-2") .should.equal true describe "when there is no suitable root doc", -> @@ -68,9 +69,9 @@ describe 'ProjectRootDocManager', -> _id: "doc-id-2" lines: ["%Example: \\documentclass{article}"] @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs) - @ProjectEntityHandler.setRootDoc = sinon.stub().callsArgWith(2) + @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) @ProjectRootDocManager.setRootDocAutomatically @project_id, done it "should not set the root doc to the doc containing a documentclass", -> - @ProjectEntityHandler.setRootDoc.called.should.equal false + @ProjectEntityUpdateHandler.setRootDoc.called.should.equal false diff --git a/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee b/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee index 5054a30f10..3d121e0bce 100644 --- a/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee +++ b/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee @@ -7,168 +7,92 @@ BufferedStream = require('bufferedstream') describe 'UpdateMerger :', -> beforeEach -> - @editorController = {} - @updateReciver = {} - @projectLocator = {} - @projectEntityHandler = {} - @fs = - unlink:sinon.stub().callsArgWith(1) - @FileTypeManager = {} - @LockManager = - runWithLock : sinon.spy((key, runner, callback) -> runner(callback)) @updateMerger = SandboxedModule.require modulePath, requires: - '../Editor/EditorController': @editorController - '../Project/ProjectLocator': @projectLocator - '../Project/ProjectEntityHandler': @projectEntityHandler - 'fs': @fs - '../Uploads/FileTypeManager':@FileTypeManager - 'settings-sharelatex':{path:{dumpPath:"dump_here"}} + 'fs': @fs = + unlink:sinon.stub().callsArgWith(1) 'logger-sharelatex': log: -> err: -> - "metrics-sharelatex": - Timer:-> - done:-> - "../../infrastructure/LockManager":@LockManager + '../Editor/EditorController': @EditorController = {} + '../Uploads/FileTypeManager':@FileTypeManager = {} + 'settings-sharelatex':{path:{dumpPath:"dump_here"}} @project_id = "project_id_here" @user_id = "mock-user-id" + + @docPath = "/folder/doc.tex" + @filePath = "/folder/file.png" + + @fsPath = "/tmp/file/path" + @source = "dropbox" - @update = new BufferedStream() - @update.headers = {} + @updateRequest = new BufferedStream() + @updateMerger.p.writeStreamToDisk = sinon.stub().yields(null, @fsPath) + @callback = sinon.stub() describe 'mergeUpdate', -> - beforeEach -> - @path = "/doc1" - @fsPath = "file/system/path.tex" - @updateMerger.p.writeStreamToDisk = sinon.stub().callsArgWith(2, null, @fsPath) - @FileTypeManager.isBinary = sinon.stub() - - describe "doc updates", () -> + describe "doc updates", -> beforeEach -> - @doc_id = "231312s" - @FileTypeManager.isBinary.callsArgWith(2, null, false) - @projectLocator.findElementByPath = sinon.stub().callsArgWith(2, null, _id: @doc_id) - @updateMerger.p.processDoc = sinon.stub().callsArgWith(6) - @filePath = "/folder/doc.tex" + @FileTypeManager.isBinary = sinon.stub().yields(null, false) + @updateMerger.p.processDoc = sinon.stub().yields() + @updateMerger.mergeUpdate @user_id, @project_id, @docPath, @updateRequest, @source, @callback - it 'should get the element id', (done)-> - @updateMerger.mergeUpdate @user_id, @project_id, @path, @update, @source, => - @projectLocator.findElementByPath.calledWith(@project_id, @path).should.equal true - done() - - it 'should take a project lock', (done)-> - @updateMerger.mergeUpdate @user_id, @project_id, @path, @update, @source, => - @LockManager.runWithLock.calledWith(@project_id).should.equal true - done() - - it 'should process update as doc', (done)-> - @updateMerger.mergeUpdate @user_id, @project_id, @filePath, @update, @source, => - @FileTypeManager.isBinary.calledWith(@filePath, @fsPath).should.equal true - @updateMerger.p.processDoc.calledWith(@project_id, @doc_id, @user_id, @fsPath, @filePath, @source).should.equal true - @fs.unlink.calledWith(@fsPath).should.equal true - done() - - describe "file updates", () -> - beforeEach -> - @file_id = "1231" - @projectLocator.findElementByPath = sinon.stub().callsArgWith(2, null, _id: @file_id) - @FileTypeManager.isBinary.callsArgWith(2, null, true) - @updateMerger.p.processFile = sinon.stub().callsArgWith(6) - @filePath = "/folder/file1.png" - - it 'should process update as file when it is not a doc', (done)-> - @updateMerger.mergeUpdate @user_id, @project_id, @filePath, @update, @source, => - @updateMerger.p.processFile.calledWith(@project_id, @file_id, @fsPath, @filePath, @source, @user_id).should.equal true - @FileTypeManager.isBinary.calledWith(@filePath, @fsPath).should.equal true - @fs.unlink.calledWith(@fsPath).should.equal true - done() - - describe 'deleteUpdate', (done)-> - beforeEach -> - @path = "folder/doc1" - @type = "mock-type" - @editorController.deleteEntityWithoutLock = -> - @entity_id = "entity_id_here" - @entity = _id:@entity_id - @projectLocator.findElementByPath = sinon.stub().callsArgWith(2, null, @entity, @type) - @editorController.deleteEntityWithoutLock = sinon.stub().callsArg(5) - - it 'should get the element id', (done)-> - @updateMerger.deleteUpdate @user_id, @project_id, @path, @source, => - @projectLocator.findElementByPath.calledWith(@project_id, @path).should.equal true - done() - - it 'should take a project lock', (done)-> - @updateMerger.deleteUpdate @user_id, @project_id, @path, @source, => - @LockManager.runWithLock.calledWith(@project_id).should.equal true - done() - - it 'should delete the entity in the editor controller with the correct type', (done)-> - @entity.lines = [] - @updateMerger.deleteUpdate @user_id, @project_id, @path, @source, => - @editorController.deleteEntityWithoutLock - .calledWith(@project_id, @entity_id, @type, @source, @user_id) + it 'should process update as doc', -> + @updateMerger.p.processDoc + .calledWith(@project_id, @user_id, @fsPath, @docPath, @source) .should.equal true - done() - describe 'private methods', () -> - describe 'processDoc', (done)-> + it 'removes the temp file from disk', -> + @fs.unlink.calledWith(@fsPath).should.equal true + + describe "file updates", -> + beforeEach -> + @FileTypeManager.isBinary = sinon.stub().yields(null, true) + @updateMerger.p.processFile = sinon.stub().yields() + @updateMerger.mergeUpdate @user_id, @project_id, @filePath, @updateRequest, @source, @callback + + it 'should process update as file', -> + @updateMerger.p.processFile + .calledWith(@project_id, @fsPath, @filePath, @source, @user_id) + .should.equal true + + it 'removes the temp file from disk', -> + @fs.unlink.calledWith(@fsPath).should.equal true + + describe 'deleteUpdate', -> + beforeEach -> + @EditorController.deleteEntityWithPath = sinon.stub().yields() + @updateMerger.deleteUpdate @user_id, @project_id, @docPath, @source, @callback + + it 'should delete the entity in the editor controller', -> + @EditorController.deleteEntityWithPath + .calledWith(@project_id, @docPath, @source, @user_id) + .should.equal true + + describe 'private methods', -> + describe 'processDoc', -> beforeEach -> - @doc_id = "312312klnkld" @docLines = "\\documentclass{article}\n\\usepackage[utf8]{inputenc}\n\n\\title{42}\n\\author{Jane Doe}\n\\date{June 2011}" - @splitDocLines = @docLines.split("\n") - @fs.readFile = sinon.stub().callsArgWith(2, null, @docLines) + @updateMerger.p.readFileIntoTextArray = sinon.stub().yields(null, @docLines) + @EditorController.upsertDocWithPath = sinon.stub().yields() - @editorController.setDoc = sinon.stub().callsArg(5) + @updateMerger.p.processDoc @project_id, @user_id, @fsPath, @docPath, @source, @callback - @update.write(@docLines) - @update.end() + it 'reads the temp file from disk', -> + @updateMerger.p.readFileIntoTextArray + .calledWith(@fsPath) + .should.equal true - it 'should set the doc text in the editor controller', (done)-> - @updateMerger.p.processDoc @project_id, @doc_id, @user_id, @update, "path", @source, => - @editorController.setDoc - .calledWith(@project_id, @doc_id, @user_id, @splitDocLines, @source) - .should.equal true - done() + it 'should upsert the doc in the editor controller', -> + @EditorController.upsertDocWithPath + .calledWith(@project_id, @docPath, @docLines, @source, @user_id) + .should.equal true - it 'should create a new doc when it doesnt exist', (done)-> - folder = {_id:"adslkjioj"} - docName = "main.tex" - path = "folder1/folder2/#{docName}" - @editorController.mkdirpWithoutLock = sinon.stub().callsArgWith(2, null, [folder], folder) - @editorController.addDocWithoutLock = sinon.stub().callsArg(6) - - @updateMerger.p.processDoc @project_id, undefined, @user_id, @update, path, @source, => - @editorController.mkdirpWithoutLock - .calledWith(@project_id) - .should.equal true - @editorController.addDocWithoutLock - .calledWith(@project_id, folder._id, docName, @splitDocLines, @source, @user_id) - .should.equal true - done() - - describe 'processFile', (done)-> + describe 'processFile', -> beforeEach -> - @file_id = "file_id_here" - @folder_id = "folder_id_here" - @path = "folder/file.png" - @folder = _id: @folder_id - @fileName = "file.png" - @fsPath = "fs/path.tex" - @editorController.addFileWithoutLock = sinon.stub().callsArg(6) - @editorController.replaceFileWithoutLock = sinon.stub().callsArg(5) - @editorController.deleteEntityWithoutLock = sinon.stub() - @editorController.mkdirpWithoutLock = sinon.stub().withArgs(@project_id).callsArgWith(2, null, [@folder], @folder) + @EditorController.upsertFileWithPath = sinon.stub().yields() + @updateMerger.p.processFile @project_id, @fsPath, @filePath, @source, @user_id, @callback - it 'should replace file if the file already exists', (done)-> - @updateMerger.p.processFile @project_id, @file_id, @fsPath, @path, @source, @user_id, => - @editorController.addFileWithoutLock.called.should.equal false - @editorController.replaceFileWithoutLock.calledWith(@project_id, @file_id, @fsPath, @source, @user_id).should.equal true - done() - - it 'should call add file if the file does not exist', (done)-> - @updateMerger.p.processFile @project_id, undefined, @fsPath, @path, @source, @user_id, => - @editorController.mkdirpWithoutLock.calledWith(@project_id, "folder/").should.equal true - @editorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @fileName, @fsPath, @source, @user_id).should.equal true - @editorController.replaceFileWithoutLock.called.should.equal false - done() + it 'should upsert the file in the editor controller', -> + @EditorController.upsertFileWithPath + .calledWith(@project_id, @filePath, @fsPath, @source, @user_id) + .should.equal true diff --git a/services/web/test/unit/coffee/Uploads/FileSystemImportManagerTests.coffee b/services/web/test/unit/coffee/Uploads/FileSystemImportManagerTests.coffee index b6241686a3..13db922dda 100644 --- a/services/web/test/unit/coffee/Uploads/FileSystemImportManagerTests.coffee +++ b/services/web/test/unit/coffee/Uploads/FileSystemImportManagerTests.coffee @@ -33,25 +33,25 @@ describe "FileSystemImportManager", -> describe "when path is symlink", -> beforeEach -> @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, false) - @EditorController.addDocWithoutLock = sinon.stub() + @EditorController.addDoc = sinon.stub() @FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback it "should not read the file from disk", -> @fs.readFile.called.should.equal false it "should not insert the doc", -> - @EditorController.addDocWithoutLock.called.should.equal false + @EditorController.addDoc.called.should.equal false describe "with replace set to false", -> beforeEach -> - @EditorController.addDocWithoutLock = sinon.stub().callsArg(6) + @EditorController.addDoc = sinon.stub().callsArg(6) @FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback it "should read the file from disk", -> @fs.readFile.calledWith(@path_on_disk, "utf8").should.equal true it "should insert the doc", -> - @EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id) + @EditorController.addDoc.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id) .should.equal true describe "with windows line ending", -> @@ -59,134 +59,60 @@ describe "FileSystemImportManager", -> @docContent = "one\r\ntwo\r\nthree" @docLines = ["one", "two", "three"] @fs.readFile = sinon.stub().callsArgWith(2, null, @docContent) - @EditorController.addDocWithoutLock = sinon.stub().callsArg(6) + @EditorController.addDoc = sinon.stub().callsArg(6) @FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback it "should strip the \\r characters before adding", -> - @EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id) + @EditorController.addDoc.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id) .should.equal true describe "with replace set to true", -> - describe "when the doc doesn't exist", -> - beforeEach -> - @folder = { - docs: [{ - _id: "doc-id-2" - name: "not-the-right-file.tex" - }] - } - @ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder) - @EditorController.addDocWithoutLock = sinon.stub().callsArg(6) - @FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback + beforeEach -> + @EditorController.upsertDoc = sinon.stub().yields() + @FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback - it "should look up the folder", -> - @ProjectLocator.findElement - .calledWith(project_id: @project_id, element_id: @folder_id, type: "folder") - .should.equal true - - it "should insert the doc", -> - @EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id) - .should.equal true - - describe "when the doc does exist", -> - beforeEach -> - @folder = { - docs: [{ - _id: @doc_id = "doc-id-1" - name: @name - }, { - _id: "doc-id-2" - name: "not-the-right-file.tex" - }] - } - @ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder) - @EditorController.setDoc = sinon.stub().callsArg(5) - @FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback - - it "should look up the folder", -> - @ProjectLocator.findElement - .calledWith(project_id: @project_id, element_id: @folder_id, type: "folder") - .should.equal true - - it "should set the doc with the new doc lines", -> - @EditorController.setDoc.calledWith(@project_id, @doc_id, @user_id, @docLines, "upload") - .should.equal true + it "should upsert the doc", -> + @EditorController.upsertDoc + .calledWith(@project_id, @folder_id, @name, @docLines, "upload", @user_id) + .should.equal true describe "addFile with replace set to false", -> beforeEach -> - @EditorController.addFileWithoutLock = sinon.stub().callsArg(6) + @EditorController.addFile = sinon.stub().callsArg(6) @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true) @FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback it "should add the file", -> - @EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload", @user_id) + @EditorController.addFile.calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload", @user_id) .should.equal true describe "addFile with symlink", -> beforeEach -> - @EditorController.addFileWithoutLock = sinon.stub() + @EditorController.addFile = sinon.stub() @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, false) - @EditorController.replaceFileWithoutLock = sinon.stub() + @EditorController.replaceFile = sinon.stub() @FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback it "should node add the file", -> - @EditorController.addFileWithoutLock.called.should.equal false - @EditorController.replaceFileWithoutLock.called.should.equal false + @EditorController.addFile.called.should.equal false + @EditorController.replaceFile.called.should.equal false describe "addFile with replace set to true", -> - describe "when the file doesn't exist", -> - beforeEach -> - @folder = { - fileRefs: [{ - _id: "file-id-2" - name: "not-the-right-file.tex" - }] - } - @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true) - @ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder) - @EditorController.addFileWithoutLock = sinon.stub().callsArg(6) - @FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback + beforeEach -> + @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true) + @EditorController.upsertFile = sinon.stub().yields() + @FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback - it "should look up the folder", -> - @ProjectLocator.findElement - .calledWith(project_id: @project_id, element_id: @folder_id, type: "folder") - .should.equal true - - it "should add the file", -> - @EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload", @user_id) - .should.equal true - - describe "when the file does exist", -> - beforeEach -> - @folder = { - fileRefs: [{ - _id: @file_id = "file-id-1" - name: @name - }, { - _id: "file-id-2" - name: "not-the-right-file.tex" - }] - } - @FileSystemImportManager._isSafeOnFileSystem = sinon.stub().callsArgWith(1, null, true) - @ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder) - @EditorController.replaceFileWithoutLock = sinon.stub().callsArg(5) - @FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback - - it "should look up the folder", -> - @ProjectLocator.findElement - .calledWith(project_id: @project_id, element_id: @folder_id, type: "folder") - .should.equal true - - it "should replace the file", -> - @EditorController.replaceFileWithoutLock - .calledWith(@project_id, @file_id, @path_on_disk, "upload", @user_id) - .should.equal true + it "should add the file", -> + @EditorController.upsertFile + .calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload", @user_id) + .should.equal true describe "addFolder", -> beforeEach -> @new_folder_id = "new-folder-id" - @EditorController.addFolderWithoutLock = sinon.stub().callsArgWith(4, null, _id: @new_folder_id) + @EditorController.addFolder = sinon.stub().callsArgWith(4, null, _id: @new_folder_id) @FileSystemImportManager.addFolderContents = sinon.stub().callsArg(5) describe "successfully", -> @@ -195,7 +121,7 @@ describe "FileSystemImportManager", -> @FileSystemImportManager.addFolder @user_id, @project_id, @folder_id, @name, @path_on_disk, @replace, @callback it "should add a folder to the project", -> - @EditorController.addFolderWithoutLock.calledWith(@project_id, @folder_id, @name, "upload") + @EditorController.addFolder.calledWith(@project_id, @folder_id, @name, "upload") .should.equal true it "should add the folders contents", -> @@ -208,7 +134,7 @@ describe "FileSystemImportManager", -> @FileSystemImportManager.addFolder @user_id, @project_id, @folder_id, @name, @path_on_disk, @replace, @callback it "should not add a folder to the project", -> - @EditorController.addFolderWithoutLock.called.should.equal false + @EditorController.addFolder.called.should.equal false @FileSystemImportManager.addFolderContents.called.should.equal false describe "addFolderContents", -> diff --git a/services/web/test/unit/coffee/Uploads/ProjectUploadControllerTests.coffee b/services/web/test/unit/coffee/Uploads/ProjectUploadControllerTests.coffee index 488250b647..4480f15536 100644 --- a/services/web/test/unit/coffee/Uploads/ProjectUploadControllerTests.coffee +++ b/services/web/test/unit/coffee/Uploads/ProjectUploadControllerTests.coffee @@ -17,8 +17,6 @@ describe "ProjectUploadController", -> done: sinon.stub() @AuthenticationController = getLoggedInUserId: sinon.stub().returns(@user_id) - @LockManager = - runWithLock : sinon.spy((key, runner, callback) -> runner(callback)) @ProjectUploadController = SandboxedModule.require modulePath, requires: "./ProjectUploadManager" : @ProjectUploadManager = {} @@ -26,7 +24,6 @@ describe "ProjectUploadController", -> "logger-sharelatex" : @logger = {log: sinon.stub(), error: sinon.stub(), err:->} "metrics-sharelatex": @metrics '../Authentication/AuthenticationController': @AuthenticationController - "../../infrastructure/LockManager": @LockManager "fs" : @fs = {} describe "uploadProject", -> @@ -129,9 +126,6 @@ describe "ProjectUploadController", -> @FileSystemImportManager.addEntity = sinon.stub().callsArgWith(6, null, @entity) @ProjectUploadController.uploadFile @req, @res - it "should take the lock", -> - @LockManager.runWithLock.calledWith(@project_id).should.equal true - it "should insert the file", -> @FileSystemImportManager.addEntity .calledWith(@user_id, @project_id, @folder_id, @name, @path)