Quellcode durchsuchen

Merge branch 'master' of github.com:sharelatex/web-sharelatex

Shane Kilkelly vor 10 Jahren
Ursprung
Commit
ca43d27244

+ 1 - 1
services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee

@@ -12,7 +12,7 @@ module.exports =
 
 
 	addUserToGroup: (req, res)->
 	addUserToGroup: (req, res)->
 		adminUserId = req.session.user._id
 		adminUserId = req.session.user._id
-		newEmail = req.body.email
+		newEmail = req.body?.email?.toLowerCase()?.trim()
 		logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription"
 		logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription"
 		SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)->
 		SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)->
 			if err?
 			if err?

+ 16 - 0
services/web/app/coffee/Features/Uploads/ArchiveManager.coffee

@@ -1,6 +1,8 @@
 child   = require "child_process"
 child   = require "child_process"
 logger  = require "logger-sharelatex"
 logger  = require "logger-sharelatex"
 metrics = require "../../infrastructure/Metrics"
 metrics = require "../../infrastructure/Metrics"
+fs      = require "fs"
+Path    = require "path"
 
 
 module.exports = ArchiveManager =
 module.exports = ArchiveManager =
 	extractZipArchive: (source, destination, _callback = (err) ->) ->
 	extractZipArchive: (source, destination, _callback = (err) ->) ->
@@ -34,4 +36,18 @@ module.exports = ArchiveManager =
 				error = new Error(error)
 				error = new Error(error)
 				logger.error err:error, source: source, destination: destination, "error unzipping file"
 				logger.error err:error, source: source, destination: destination, "error unzipping file"
 			callback(error)
 			callback(error)
+	
+	findTopLevelDirectory: (directory, callback = (error, topLevelDir) ->) ->
+		fs.readdir directory, (error, files) ->
+			return callback(error) if error?
+			if files.length == 1
+				childPath = Path.join(directory, files[0])
+				fs.stat childPath, (error, stat) ->
+					return callback(error) if error?
+					if stat.isDirectory()
+						return callback(null, childPath)
+					else
+						return callback(null, directory)
+			else
+				return callback(null, directory)
 
 

+ 25 - 11
services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee

@@ -6,14 +6,28 @@ EditorController = require "../Editor/EditorController"
 ProjectLocator   = require "../Project/ProjectLocator"
 ProjectLocator   = require "../Project/ProjectLocator"
 
 
 module.exports = FileSystemImportManager =
 module.exports = FileSystemImportManager =
-	addDoc: (project_id, folder_id, name, path, replace, callback = (error, doc)-> )->
+	addDoc: (user_id, project_id, folder_id, name, path, replace, callback = (error, doc)-> )->
 		fs.readFile path, "utf8", (error, content = "") ->
 		fs.readFile path, "utf8", (error, content = "") ->
 			return callback(error) if error?
 			return callback(error) if error?
 			content = content.replace(/\r/g, "")
 			content = content.replace(/\r/g, "")
 			lines = content.split("\n")
 			lines = content.split("\n")
-			EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback
+			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", callback
+			else
+				EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback
 
 
-	addFile: (project_id, folder_id, name, path, replace, callback = (error, file)-> )->
+	addFile: (user_id, project_id, folder_id, name, path, replace, callback = (error, file)-> )->
 		if replace
 		if replace
 			ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
 			ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
 				return callback(error) if error?
 				return callback(error) if error?
@@ -30,14 +44,14 @@ module.exports = FileSystemImportManager =
 		else
 		else
 			EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback
 			EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback
 
 
-	addFolder: (project_id, folder_id, name, path, replace, callback = (error)-> ) ->
+	addFolder: (user_id, project_id, folder_id, name, path, replace, callback = (error)-> ) ->
 		EditorController.addFolderWithoutLock project_id, folder_id, name, "upload", (error, new_folder) =>
 		EditorController.addFolderWithoutLock project_id, folder_id, name, "upload", (error, new_folder) =>
 			return callback(error) if error?
 			return callback(error) if error?
-			@addFolderContents project_id, new_folder._id, path, replace, (error) ->
+			@addFolderContents user_id, project_id, new_folder._id, path, replace, (error) ->
 				return callback(error) if error?
 				return callback(error) if error?
 				callback null, new_folder
 				callback null, new_folder
 
 
-	addFolderContents: (project_id, parent_folder_id, folderPath, replace, callback = (error)-> ) ->
+	addFolderContents: (user_id, project_id, parent_folder_id, folderPath, replace, callback = (error)-> ) ->
 		fs.readdir folderPath, (error, entries = []) =>
 		fs.readdir folderPath, (error, entries = []) =>
 			return callback(error) if error?
 			return callback(error) if error?
 			jobs = _.map entries, (entry) =>
 			jobs = _.map entries, (entry) =>
@@ -45,21 +59,21 @@ module.exports = FileSystemImportManager =
 					FileTypeManager.shouldIgnore entry, (error, ignore) =>
 					FileTypeManager.shouldIgnore entry, (error, ignore) =>
 						return callback(error) if error?
 						return callback(error) if error?
 						if !ignore
 						if !ignore
-							@addEntity project_id, parent_folder_id, entry, "#{folderPath}/#{entry}", replace, callback
+							@addEntity user_id, project_id, parent_folder_id, entry, "#{folderPath}/#{entry}", replace, callback
 						else
 						else
 							callback()
 							callback()
 			async.parallelLimit jobs, 5, callback
 			async.parallelLimit jobs, 5, callback
 
 
-	addEntity: (project_id, folder_id, name, path, replace, callback = (error, entity)-> ) ->
+	addEntity: (user_id, project_id, folder_id, name, path, replace, callback = (error, entity)-> ) ->
 		FileTypeManager.isDirectory path, (error, isDirectory) =>
 		FileTypeManager.isDirectory path, (error, isDirectory) =>
 			return callback(error) if error?
 			return callback(error) if error?
 			if isDirectory
 			if isDirectory
-				@addFolder project_id, folder_id, name, path, replace, callback
+				@addFolder user_id, project_id, folder_id, name, path, replace, callback
 			else
 			else
 				FileTypeManager.isBinary name, path, (error, isBinary) =>
 				FileTypeManager.isBinary name, path, (error, isBinary) =>
 					return callback(error) if error?
 					return callback(error) if error?
 					if isBinary
 					if isBinary
-						@addFile project_id, folder_id, name, path, replace, callback
+						@addFile user_id, project_id, folder_id, name, path, replace, callback
 					else
 					else
-						@addDoc project_id, folder_id, name, path, replace, callback
+						@addDoc user_id, project_id, folder_id, name, path, replace, callback
 
 

+ 2 - 1
services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee

@@ -34,7 +34,8 @@ module.exports = ProjectUploadController =
 		if !name? or name.length == 0 or name.length > 150
 		if !name? or name.length == 0 or name.length > 150
 			logger.err project_id:project_id, name:name, "bad name when trying to upload file"
 			logger.err project_id:project_id, name:name, "bad name when trying to upload file"
 			return res.send success: false
 			return res.send success: false
-		FileSystemImportManager.addEntity project_id, folder_id, name, path, true, (error, entity) ->
+		user_id = req.session.user._id
+		FileSystemImportManager.addEntity user_id, project_id, folder_id, name, path, true, (error, entity) ->
 			fs.unlink path, ->
 			fs.unlink path, ->
 			timer.done()
 			timer.done()
 			if error?
 			if error?

+ 6 - 4
services/web/app/coffee/Features/Uploads/ProjectUploadManager.coffee

@@ -9,19 +9,21 @@ module.exports = ProjectUploadHandler =
 	createProjectFromZipArchive: (owner_id, name, zipPath, callback = (error, project) ->) ->
 	createProjectFromZipArchive: (owner_id, name, zipPath, callback = (error, project) ->) ->
 		ProjectCreationHandler.createBlankProject owner_id, name, (error, project) =>
 		ProjectCreationHandler.createBlankProject owner_id, name, (error, project) =>
 			return callback(error) if error?
 			return callback(error) if error?
-			@insertZipArchiveIntoFolder project._id, project.rootFolder[0]._id, zipPath, (error) ->
+			@insertZipArchiveIntoFolder owner_id, project._id, project.rootFolder[0]._id, zipPath, (error) ->
 				return callback(error) if error?
 				return callback(error) if error?
 				ProjectRootDocManager.setRootDocAutomatically project._id, (error) ->
 				ProjectRootDocManager.setRootDocAutomatically project._id, (error) ->
 					return callback(error) if error?
 					return callback(error) if error?
 					callback(error, project)
 					callback(error, project)
 
 
-	insertZipArchiveIntoFolder: (project_id, folder_id, path, callback = (error) ->) ->
+	insertZipArchiveIntoFolder: (owner_id, project_id, folder_id, path, callback = (error) ->) ->
 		destination = @_getDestinationDirectory path
 		destination = @_getDestinationDirectory path
 		ArchiveManager.extractZipArchive path, destination, (error) ->
 		ArchiveManager.extractZipArchive path, destination, (error) ->
 			return callback(error) if error?
 			return callback(error) if error?
-			FileSystemImportManager.addFolderContents project_id, folder_id, destination, false, (error) ->
+			ArchiveManager.findTopLevelDirectory destination, (error, topLevelDestination) ->
 				return callback(error) if error?
 				return callback(error) if error?
-				rimraf(destination, callback)
+				FileSystemImportManager.addFolderContents owner_id, project_id, folder_id, topLevelDestination, false, (error) ->
+					return callback(error) if error?
+					rimraf(destination, callback)
 
 
 	_getDestinationDirectory: (source) ->
 	_getDestinationDirectory: (source) ->
 		return path.join(path.dirname(source), "#{path.basename(source, ".zip")}-#{Date.now()}")
 		return path.join(path.dirname(source), "#{path.basename(source, ".zip")}-#{Date.now()}")

+ 15 - 0
services/web/app/views/project/editor/file-tree.jade

@@ -360,6 +360,17 @@ script(type="text/ng-template", id="uploadFileModalTemplate")
 		.alert.alert-warning.small.modal-alert(ng-if="tooManyFiles") #{translate("maximum_files_uploaded_together", {max:"{{max_files}}"})}
 		.alert.alert-warning.small.modal-alert(ng-if="tooManyFiles") #{translate("maximum_files_uploaded_together", {max:"{{max_files}}"})}
 		.alert.alert-warning.small.modal-alert(ng-if="rateLimitHit") #{translate("too_many_files_uploaded_throttled_short_period")}
 		.alert.alert-warning.small.modal-alert(ng-if="rateLimitHit") #{translate("too_many_files_uploaded_throttled_short_period")}
 		.alert.alert-warning.small.modal-alert(ng-if="notLoggedIn") #{translate("session_expired_redirecting_to_login", {seconds:"{{secondsToRedirect}}"})}
 		.alert.alert-warning.small.modal-alert(ng-if="notLoggedIn") #{translate("session_expired_redirecting_to_login", {seconds:"{{secondsToRedirect}}"})}
+		.alert.alert-warning.small.modal-alert(ng-if="conflicts.length > 0")
+			p.text-center
+				| The following files already exist in this project:
+			ul.text-center.list-unstyled.row-spaced-small
+				li(ng-repeat="conflict in conflicts"): strong {{ conflict }}
+			p.text-center.row-spaced-small
+				| Do you want to overwrite them?
+			p.text-center
+				a(href, ng-click="doUpload()").btn.btn-primary Overwrite
+				|  
+				a(href, ng-click="cancel()").btn.btn-default Cancel
 
 
 	.modal-body(
 	.modal-body(
 		fine-upload
 		fine-upload
@@ -370,10 +381,14 @@ script(type="text/ng-template", id="uploadFileModalTemplate")
 		drag-area-text="{{drag_files}}"
 		drag-area-text="{{drag_files}}"
 		hint-text="{{hint_press_and_hold_control_key}}"
 		hint-text="{{hint_press_and_hold_control_key}}"
 		multiple="true"
 		multiple="true"
+		auto-upload="false"
 		on-complete-callback="onComplete"
 		on-complete-callback="onComplete"
 		on-upload-callback="onUpload"
 		on-upload-callback="onUpload"
 		on-validate-batch="onValidateBatch"
 		on-validate-batch="onValidateBatch"
 		on-error-callback="onError"
 		on-error-callback="onError"
+		on-submit-callback="onSubmit"
+		on-cancel-callback="onCancel"
+		control="control"
 		params="{'folder_id': parent_folder_id}"
 		params="{'folder_id': parent_folder_id}"
 	)
 	)
 		span #{translate("upload_files")}
 		span #{translate("upload_files")}

+ 15 - 0
services/web/public/coffee/directives/fineUpload.coffee

@@ -16,7 +16,11 @@ define [
 				onUploadCallback: "="
 				onUploadCallback: "="
 				onValidateBatch: "="
 				onValidateBatch: "="
 				onErrorCallback: "="
 				onErrorCallback: "="
+				onSubmitCallback: "="
+				onCancelCallback: "="
+				autoUpload: "="
 				params: "="
 				params: "="
+				control: "="
 			}
 			}
 			link: (scope, element, attrs) ->
 			link: (scope, element, attrs) ->
 				multiple = scope.multiple or false
 				multiple = scope.multiple or false
@@ -37,12 +41,19 @@ define [
 				onUpload   = scope.onUploadCallback or () ->
 				onUpload   = scope.onUploadCallback or () ->
 				onError   = scope.onErrorCallback or () ->
 				onError   = scope.onErrorCallback or () ->
 				onValidateBatch = scope.onValidateBatch or () ->
 				onValidateBatch = scope.onValidateBatch or () ->
+				onSubmit = scope.onSubmitCallback or () ->
+				onCancel = scope.onCancelCallback or () ->
+				if !scope.autoUpload?
+					autoUpload = true
+				else
+					autoUpload = scope.autoUpload
 				params     = scope.params or {}
 				params     = scope.params or {}
 				params._csrf = window.csrfToken
 				params._csrf = window.csrfToken
 
 
 				q = new qq.FineUploader
 				q = new qq.FineUploader
 					element: element[0]
 					element: element[0]
 					multiple: multiple
 					multiple: multiple
+					autoUpload: autoUpload
 					disabledCancelForFormUploads: true
 					disabledCancelForFormUploads: true
 					validation: validation
 					validation: validation
 					maxConnections: maxConnections
 					maxConnections: maxConnections
@@ -56,6 +67,8 @@ define [
 						onUpload:   onUpload
 						onUpload:   onUpload
 						onValidateBatch: onValidateBatch
 						onValidateBatch: onValidateBatch
 						onError: onError
 						onError: onError
+						onSubmit: onSubmit
+						onCancel: onCancel
 					text: text
 					text: text
 					template: """
 					template: """
 						<div class="qq-uploader">
 						<div class="qq-uploader">
@@ -70,5 +83,7 @@ define [
 							<ul class="qq-upload-list"></ul>
 							<ul class="qq-upload-list"></ul>
 						</div>
 						</div>
 					"""
 					"""
+				window.q = q
+				scope.control?.q = q
 				return q
 				return q
 		}
 		}

+ 6 - 0
services/web/public/coffee/ide/file-tree/FileTreeManager.coffee

@@ -135,6 +135,12 @@ define [
 		multiSelectSelectedEntity: () ->
 		multiSelectSelectedEntity: () ->
 			@findSelectedEntity()?.multiSelected = true
 			@findSelectedEntity()?.multiSelected = true
 
 
+		existsInFolder: (folder_id, name) ->
+			folder = @findEntityById(folder_id)
+			return false if !folder?
+			entity = @_findEntityByPathInFolder(folder, name)
+			return entity?
+			
 		findSelectedEntity: () ->
 		findSelectedEntity: () ->
 			selected = null
 			selected = null
 			@forEachEntity (entity) ->
 			@forEachEntity (entity) ->

+ 30 - 6
services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee

@@ -106,7 +106,8 @@ define [
 			$scope.rateLimitHit = false
 			$scope.rateLimitHit = false
 			$scope.secondsToRedirect = 10
 			$scope.secondsToRedirect = 10
 			$scope.notLoggedIn = false
 			$scope.notLoggedIn = false
-
+			$scope.conflicts = []
+			$scope.control = {}
 
 
 			needToLogBackIn = ->
 			needToLogBackIn = ->
 				$scope.notLoggedIn = true
 				$scope.notLoggedIn = true
@@ -121,11 +122,6 @@ define [
 
 
 				decreseTimeout()
 				decreseTimeout()
 
 
-
-			uploadCount = 0
-			$scope.onUpload = () ->
-				uploadCount++
-
 			$scope.max_files = 40
 			$scope.max_files = 40
 			$scope.onComplete = (error, name, response) ->
 			$scope.onComplete = (error, name, response) ->
 				$timeout (() ->
 				$timeout (() ->
@@ -150,6 +146,34 @@ define [
 				else if reason.indexOf("403") != -1
 				else if reason.indexOf("403") != -1
 					needToLogBackIn()
 					needToLogBackIn()
 
 
+			_uploadTimer = null
+			uploadIfNoConflicts = () ->
+				if $scope.conflicts.length == 0
+					$scope.doUpload()
+
+			uploadCount = 0
+			$scope.onSubmit = (id, name) ->
+				uploadCount++
+				if ide.fileTreeManager.existsInFolder($scope.parent_folder_id, name)
+					$scope.conflicts.push name
+					$scope.$apply()
+				if !_uploadTimer?
+					_uploadTimer = setTimeout () ->
+						_uploadTimer = null
+						uploadIfNoConflicts()
+					, 0
+				return true
+			
+			$scope.onCancel = (id, name) ->
+				uploadCount--
+				index = $scope.conflicts.indexOf(name)
+				if index > -1
+					$scope.conflicts.splice(index, 1)
+				$scope.$apply()
+				uploadIfNoConflicts()
+
+			$scope.doUpload = () ->
+				$scope.control?.q?.uploadStoredFiles()
 
 
 			$scope.cancel = () ->
 			$scope.cancel = () ->
 				$modalInstance.dismiss('cancel')
 				$modalInstance.dismiss('cancel')

+ 4 - 3
services/web/public/stylesheets/app/editor/toolbar.less

@@ -95,15 +95,16 @@
 	&.toolbar-small {
 	&.toolbar-small {
 		height: 32px;
 		height: 32px;
 		> a, .toolbar-right > a {
 		> a, .toolbar-right > a {
-			padding: 4px 2px 2px;
+			padding: 2px 4px 1px 4px;
 			margin: 0;
 			margin: 0;
+			margin-top: 2px;
 		}
 		}
 		> a {
 		> a {
-			margin-left: 6px;
+			margin-left: 2px;
 		}
 		}
 		.toolbar-right > a {
 		.toolbar-right > a {
 			margin-left: 0;
 			margin-left: 0;
-			margin-right: 6px;
+			margin-right: 2px;
 		}
 		}
 	}
 	}
 
 

+ 2 - 2
services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupControllerTests.coffee

@@ -54,11 +54,11 @@ describe "SubscriptionGroupController", ->
 	describe "addUserToGroup", ->
 	describe "addUserToGroup", ->
 
 
 		it "should use the admin id for the logged in user and take the email address from the body", (done)->
 		it "should use the admin id for the logged in user and take the email address from the body", (done)->
-			newEmail = "31231"
+			newEmail = " boB@gmaiL.com "
 			@req.body = email: newEmail
 			@req.body = email: newEmail
 			res =
 			res =
 				json : (data)=>
 				json : (data)=>
-					@GroupHandler.addUserToGroup.calledWith(@adminUserId, newEmail).should.equal true
+					@GroupHandler.addUserToGroup.calledWith(@adminUserId, "bob@gmail.com").should.equal true
 					data.user.should.deep.equal @user
 					data.user.should.deep.equal @user
 					done()
 					done()
 			@Controller.addUserToGroup @req, res
 			@Controller.addUserToGroup @req, res

+ 54 - 0
services/web/test/UnitTests/coffee/Uploads/ArchiveManagerTests.coffee

@@ -22,6 +22,7 @@ describe "ArchiveManager", ->
 			"child_process": @child
 			"child_process": @child
 			"logger-sharelatex": @logger
 			"logger-sharelatex": @logger
 			"../../infrastructure/Metrics": @metrics
 			"../../infrastructure/Metrics": @metrics
+			"fs": @fs = {}
 	
 	
 	describe "extractZipArchive", ->
 	describe "extractZipArchive", ->
 		beforeEach ->
 		beforeEach ->
@@ -69,4 +70,57 @@ describe "ArchiveManager", ->
 
 
 			it "should log out the error", ->
 			it "should log out the error", ->
 				@logger.error.called.should.equal true
 				@logger.error.called.should.equal true
+	
+	describe "findTopLevelDirectory", ->
+		beforeEach ->
+			@fs.readdir = sinon.stub()
+			@fs.stat = sinon.stub()
+			@directory = "test/directory"
+
+		describe "with multiple files", ->
+			beforeEach ->
+				@fs.readdir.callsArgWith(1, null, ["multiple", "files"])
+				@ArchiveManager.findTopLevelDirectory(@directory, @callback)
+			
+			it "should find the files in the directory", ->
+				@fs.readdir
+					.calledWith(@directory)
+					.should.equal true
+			
+			it "should return the original directory", ->
+				@callback
+					.calledWith(null, @directory)
+					.should.equal true
+		
+		describe "with a single file (not folder)", ->
+			beforeEach ->
+				@fs.readdir.callsArgWith(1, null, ["foo.tex"])
+				@fs.stat.callsArgWith(1, null, { isDirectory: () -> false })
+				@ArchiveManager.findTopLevelDirectory(@directory, @callback)
+			
+			it "should check if the file is a directory", ->
+				@fs.stat
+					.calledWith(@directory + "/foo.tex")
+					.should.equal true
+			
+			it "should return the original directory", ->
+				@callback
+					.calledWith(null, @directory)
+					.should.equal true
+		
+		describe "with a single top-level folder", ->
+			beforeEach ->
+				@fs.readdir.callsArgWith(1, null, ["folder"])
+				@fs.stat.callsArgWith(1, null, { isDirectory: () -> true })
+				@ArchiveManager.findTopLevelDirectory(@directory, @callback)
+			
+			it "should check if the file is a directory", ->
+				@fs.stat
+					.calledWith(@directory + "/folder")
+					.should.equal true
+			
+			it "should return the child directory", ->
+				@callback
+					.calledWith(null, @directory + "/folder")
+					.should.equal true
 
 

+ 88 - 37
services/web/test/UnitTests/coffee/Uploads/FileSystemImportManagerTests.coffee

@@ -11,43 +11,94 @@ describe "FileSystemImportManager", ->
 		@name = "test-file.tex"
 		@name = "test-file.tex"
 		@path_on_disk = "/path/to/file/#{@name}"
 		@path_on_disk = "/path/to/file/#{@name}"
 		@replace = "replace-boolean-flag-mock"
 		@replace = "replace-boolean-flag-mock"
+		@user_id = "mock-user-123"
+		@callback = sinon.stub()
 		@FileSystemImportManager = SandboxedModule.require modulePath, requires:
 		@FileSystemImportManager = SandboxedModule.require modulePath, requires:
 			"fs" : @fs = {}
 			"fs" : @fs = {}
 			"../Editor/EditorController": @EditorController = {}
 			"../Editor/EditorController": @EditorController = {}
 			"./FileTypeManager": @FileTypeManager = {}
 			"./FileTypeManager": @FileTypeManager = {}
 			"../Project/ProjectLocator": @ProjectLocator = {}
 			"../Project/ProjectLocator": @ProjectLocator = {}
-
+	
 	describe "addDoc", ->
 	describe "addDoc", ->
 		beforeEach ->
 		beforeEach ->
 			@docContent = "one\ntwo\nthree"
 			@docContent = "one\ntwo\nthree"
 			@docLines = @docContent.split("\n")
 			@docLines = @docContent.split("\n")
 			@fs.readFile = sinon.stub().callsArgWith(2, null, @docContent)
 			@fs.readFile = sinon.stub().callsArgWith(2, null, @docContent)
-			@EditorController.addDocWithoutLock = sinon.stub().callsArg(5)
-			@FileSystemImportManager.addDoc @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
+		describe "with replace set to false", ->
+			beforeEach ->
+				@EditorController.addDocWithoutLock = sinon.stub().callsArg(5)
+				@FileSystemImportManager.addDoc @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback
 
 
-		it "should insert the doc", ->
-			@EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload")
-				.should.equal true
+			it "should read the file from disk", ->
+				@fs.readFile.calledWith(@path_on_disk, "utf8").should.equal true
 
 
-	describe "addDoc with windows line ending", ->
-		beforeEach ->
-			@docContent = "one\r\ntwo\r\nthree"
-			@docLines = ["one", "two", "three"]
-			@fs.readFile = sinon.stub().callsArgWith(2, null, @docContent)
-			@EditorController.addDocWithoutLock = sinon.stub().callsArg(5)
-			@FileSystemImportManager.addDoc @project_id, @folder_id, @name, @path_on_disk, false, @callback
+			it "should insert the doc", ->
+				@EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload")
+					.should.equal true
 
 
-		it "should strip the \\r characters before adding", ->
-			@EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload")
-				.should.equal true
+		describe "with windows line ending", ->
+			beforeEach ->
+				@docContent = "one\r\ntwo\r\nthree"
+				@docLines = ["one", "two", "three"]
+				@fs.readFile = sinon.stub().callsArgWith(2, null, @docContent)
+				@EditorController.addDocWithoutLock = sinon.stub().callsArg(5)
+				@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")
+					.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(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 insert the doc", ->
+					@EditorController.addDocWithoutLock.calledWith(@project_id, @folder_id, @name, @docLines, "upload")
+						.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
 
 
 	describe "addFile with replace set to false", ->
 	describe "addFile with replace set to false", ->
 		beforeEach ->
 		beforeEach ->
 			@EditorController.addFileWithoutLock = sinon.stub().callsArg(5)
 			@EditorController.addFileWithoutLock = sinon.stub().callsArg(5)
-			@FileSystemImportManager.addFile @project_id, @folder_id, @name, @path_on_disk, false, @callback
+			@FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, false, @callback
 
 
 		it "should add the file", ->
 		it "should add the file", ->
 			@EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload")
 			@EditorController.addFileWithoutLock.calledWith(@project_id, @folder_id, @name, @path_on_disk, "upload")
@@ -64,7 +115,7 @@ describe "FileSystemImportManager", ->
 				}
 				}
 				@ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder)
 				@ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder)
 				@EditorController.addFileWithoutLock = sinon.stub().callsArg(5)
 				@EditorController.addFileWithoutLock = sinon.stub().callsArg(5)
-				@FileSystemImportManager.addFile @project_id, @folder_id, @name, @path_on_disk, true, @callback
+				@FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback
 
 
 			it "should look up the folder", ->
 			it "should look up the folder", ->
 				@ProjectLocator.findElement
 				@ProjectLocator.findElement
@@ -88,7 +139,7 @@ describe "FileSystemImportManager", ->
 				}
 				}
 				@ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder)
 				@ProjectLocator.findElement = sinon.stub().callsArgWith(1, null, @folder)
 				@EditorController.replaceFile = sinon.stub().callsArg(4)
 				@EditorController.replaceFile = sinon.stub().callsArg(4)
-				@FileSystemImportManager.addFile @project_id, @folder_id, @name, @path_on_disk, true, @callback
+				@FileSystemImportManager.addFile @user_id, @project_id, @folder_id, @name, @path_on_disk, true, @callback
 
 
 			it "should look up the folder", ->
 			it "should look up the folder", ->
 				@ProjectLocator.findElement
 				@ProjectLocator.findElement
@@ -103,15 +154,15 @@ describe "FileSystemImportManager", ->
 		beforeEach ->
 		beforeEach ->
 			@new_folder_id = "new-folder-id"
 			@new_folder_id = "new-folder-id"
 			@EditorController.addFolderWithoutLock  = sinon.stub().callsArgWith(4, null, _id: @new_folder_id)
 			@EditorController.addFolderWithoutLock  = sinon.stub().callsArgWith(4, null, _id: @new_folder_id)
-			@FileSystemImportManager.addFolderContents = sinon.stub().callsArg(4)
-			@FileSystemImportManager.addFolder @project_id, @folder_id, @name, @path_on_disk, @replace, @callback
+			@FileSystemImportManager.addFolderContents = sinon.stub().callsArg(5)
+			@FileSystemImportManager.addFolder @user_id, @project_id, @folder_id, @name, @path_on_disk, @replace, @callback
 
 
 		it "should add a folder to the project", ->
 		it "should add a folder to the project", ->
 			@EditorController.addFolderWithoutLock.calledWith(@project_id, @folder_id, @name, "upload")
 			@EditorController.addFolderWithoutLock.calledWith(@project_id, @folder_id, @name, "upload")
 				.should.equal true
 				.should.equal true
 
 
 		it "should add the folders contents", ->
 		it "should add the folders contents", ->
-			@FileSystemImportManager.addFolderContents.calledWith(@project_id, @new_folder_id, @path_on_disk, @replace)
+			@FileSystemImportManager.addFolderContents.calledWith(@user_id, @project_id, @new_folder_id, @path_on_disk, @replace)
 				.should.equal true
 				.should.equal true
 
 
 	describe "addFolderContents", ->
 	describe "addFolderContents", ->
@@ -119,19 +170,19 @@ describe "FileSystemImportManager", ->
 			@folderEntries = ["path1", "path2", "path3"]
 			@folderEntries = ["path1", "path2", "path3"]
 			@ignoredEntries = [".DS_Store"]
 			@ignoredEntries = [".DS_Store"]
 			@fs.readdir = sinon.stub().callsArgWith(1, null, @folderEntries.concat @ignoredEntries)
 			@fs.readdir = sinon.stub().callsArgWith(1, null, @folderEntries.concat @ignoredEntries)
-			@FileSystemImportManager.addEntity = sinon.stub().callsArg(5)
+			@FileSystemImportManager.addEntity = sinon.stub().callsArg(6)
 			@FileTypeManager.shouldIgnore = (path, callback) =>
 			@FileTypeManager.shouldIgnore = (path, callback) =>
 				callback null, @ignoredEntries.indexOf(require("path").basename(path)) != -1
 				callback null, @ignoredEntries.indexOf(require("path").basename(path)) != -1
-			@FileSystemImportManager.addFolderContents @project_id, @folder_id, @path_on_disk, @replace, @callback
+			@FileSystemImportManager.addFolderContents @user_id, @project_id, @folder_id, @path_on_disk, @replace, @callback
 
 
 		it "should call addEntity for each file in the folder which is not ignored", ->
 		it "should call addEntity for each file in the folder which is not ignored", ->
 			for name in @folderEntries
 			for name in @folderEntries
-				@FileSystemImportManager.addEntity.calledWith(@project_id, @folder_id, name, "#{@path_on_disk}/#{name}", @replace)
+				@FileSystemImportManager.addEntity.calledWith(@user_id, @project_id, @folder_id, name, "#{@path_on_disk}/#{name}", @replace)
 					.should.equal true
 					.should.equal true
 
 
 		it "should not call addEntity for the ignored files", ->
 		it "should not call addEntity for the ignored files", ->
 			for name in @ignoredEntries
 			for name in @ignoredEntries
-				@FileSystemImportManager.addEntity.calledWith(@project_id, @folder_id, name, "#{@path_on_disk}/#{name}", @replace)
+				@FileSystemImportManager.addEntity.calledWith(@user_id, @project_id, @folder_id, name, "#{@path_on_disk}/#{name}", @replace)
 					.should.equal false
 					.should.equal false
 	
 	
 		it "should look in the correct directory", ->
 		it "should look in the correct directory", ->
@@ -141,33 +192,33 @@ describe "FileSystemImportManager", ->
 		describe "with directory", ->
 		describe "with directory", ->
 			beforeEach ->
 			beforeEach ->
 				@FileTypeManager.isDirectory = sinon.stub().callsArgWith(1, null, true)
 				@FileTypeManager.isDirectory = sinon.stub().callsArgWith(1, null, true)
-				@FileSystemImportManager.addFolder = sinon.stub().callsArg(5)
-				@FileSystemImportManager.addEntity @project_id, @folder_id, @name, @path_on_disk, @replace, @callback
+				@FileSystemImportManager.addFolder = sinon.stub().callsArg(6)
+				@FileSystemImportManager.addEntity @user_id, @project_id, @folder_id, @name, @path_on_disk, @replace, @callback
 
 
 			it "should call addFolder", ->
 			it "should call addFolder", ->
-				@FileSystemImportManager.addFolder.calledWith(@project_id, @folder_id, @name, @path_on_disk, @replace, @callback)
+				@FileSystemImportManager.addFolder.calledWith(@user_id, @project_id, @folder_id, @name, @path_on_disk, @replace)
 					.should.equal true
 					.should.equal true
 
 
 		describe "with binary file", ->
 		describe "with binary file", ->
 			beforeEach ->
 			beforeEach ->
 				@FileTypeManager.isDirectory = sinon.stub().callsArgWith(1, null, false)
 				@FileTypeManager.isDirectory = sinon.stub().callsArgWith(1, null, false)
 				@FileTypeManager.isBinary = sinon.stub().callsArgWith(2, null, true)
 				@FileTypeManager.isBinary = sinon.stub().callsArgWith(2, null, true)
-				@FileSystemImportManager.addFile = sinon.stub().callsArg(5)
-				@FileSystemImportManager.addEntity @project_id, @folder_id, @name, @path_on_disk, @replace, @callback
+				@FileSystemImportManager.addFile = sinon.stub().callsArg(6)
+				@FileSystemImportManager.addEntity @user_id, @project_id, @folder_id, @name, @path_on_disk, @replace, @callback
 
 
 			it "should call addFile", ->
 			it "should call addFile", ->
-				@FileSystemImportManager.addFile.calledWith(@project_id, @folder_id, @name, @path_on_disk, @replace, @callback)
+				@FileSystemImportManager.addFile.calledWith(@user_id, @project_id, @folder_id, @name, @path_on_disk, @replace, @callback)
 					.should.equal true
 					.should.equal true
 
 
 		describe "with text file", ->
 		describe "with text file", ->
 			beforeEach ->
 			beforeEach ->
 				@FileTypeManager.isDirectory = sinon.stub().callsArgWith(1, null, false)
 				@FileTypeManager.isDirectory = sinon.stub().callsArgWith(1, null, false)
 				@FileTypeManager.isBinary = sinon.stub().callsArgWith(2, null, false)
 				@FileTypeManager.isBinary = sinon.stub().callsArgWith(2, null, false)
-				@FileSystemImportManager.addDoc = sinon.stub().callsArg(5)
-				@FileSystemImportManager.addEntity @project_id, @folder_id, @name, @path_on_disk, @replace, @callback
+				@FileSystemImportManager.addDoc = sinon.stub().callsArg(6)
+				@FileSystemImportManager.addEntity @user_id, @project_id, @folder_id, @name, @path_on_disk, @replace, @callback
 
 
 			it "should call addFile", ->
 			it "should call addFile", ->
-				@FileSystemImportManager.addDoc.calledWith(@project_id, @folder_id, @name, @path_on_disk, @replace, @callback)
+				@FileSystemImportManager.addDoc.calledWith(@user_id, @project_id, @folder_id, @name, @path_on_disk, @replace, @callback)
 					.should.equal true
 					.should.equal true
 
 
 
 

+ 7 - 20
services/web/test/UnitTests/coffee/Uploads/ProjectUploadControllerTests.coffee

@@ -103,6 +103,9 @@ describe "ProjectUploadController", ->
 				qqfile:
 				qqfile:
 					path: @path
 					path: @path
 					originalname: @name
 					originalname: @name
+			@req.session =
+				user:
+					_id: @user_id
 			@req.params =
 			@req.params =
 				Project_id: @project_id
 				Project_id: @project_id
 			@req.query =
 			@req.query =
@@ -115,27 +118,12 @@ describe "ProjectUploadController", ->
 			beforeEach ->
 			beforeEach ->
 				@entity =
 				@entity =
 					_id : "1234"
 					_id : "1234"
-				@FileSystemImportManager.addEntity = sinon.stub().callsArgWith(5, null, @entity)
+				@FileSystemImportManager.addEntity = sinon.stub().callsArgWith(6, null, @entity)
 				@ProjectUploadController.uploadFile @req, @res
 				@ProjectUploadController.uploadFile @req, @res
 
 
-			it "should insert the file into the correct project", ->
+			it "should insert the file", ->
 				@FileSystemImportManager.addEntity
 				@FileSystemImportManager.addEntity
-					.calledWith(@project_id)
-					.should.equal true
-
-			it "should insert the file into the provided folder", ->
-				@FileSystemImportManager.addEntity
-					.calledWith(sinon.match.any, @folder_id)
-					.should.equal true
-
-			it "should insert the file with the correct name", ->
-				@FileSystemImportManager.addEntity
-					.calledWith(sinon.match.any, sinon.match.any, @name)
-					.should.equal true
-
-			it "should insert the file from the uploaded path", ->
-				@FileSystemImportManager.addEntity
-					.calledWith(sinon.match.any, sinon.match.any, sinon.match.any, @path)
+					.calledWith(@user_id, @project_id, @folder_id, @name, @path)
 					.should.equal true
 					.should.equal true
 
 
 			it "should return a successful response to the FileUploader client", ->
 			it "should return a successful response to the FileUploader client", ->
@@ -143,7 +131,6 @@ describe "ProjectUploadController", ->
 					success: true
 					success: true
 					entity_id: @entity._id
 					entity_id: @entity._id
 
 
-
 			it "should output a log line", ->
 			it "should output a log line", ->
 				@logger.log
 				@logger.log
 					.calledWith(sinon.match.any, "uploaded file")
 					.calledWith(sinon.match.any, "uploaded file")
@@ -158,7 +145,7 @@ describe "ProjectUploadController", ->
 		describe "when FileSystemImportManager.addEntity returns an error", ->
 		describe "when FileSystemImportManager.addEntity returns an error", ->
 			beforeEach ->
 			beforeEach ->
 				@FileSystemImportManager.addEntity = sinon.stub()
 				@FileSystemImportManager.addEntity = sinon.stub()
-					.callsArgWith(5, new Error("Sorry something went wrong"))
+					.callsArgWith(6, new Error("Sorry something went wrong"))
 				@ProjectUploadController.uploadFile @req, @res
 				@ProjectUploadController.uploadFile @req, @res
 
 
 			it "should return an unsuccessful response to the FileUploader client", ->
 			it "should return an unsuccessful response to the FileUploader client", ->

+ 10 - 5
services/web/test/UnitTests/coffee/Uploads/ProjectUploadManagerTests.coffee

@@ -8,6 +8,7 @@ describe "ProjectUploadManager", ->
 	beforeEach ->
 	beforeEach ->
 		@project_id = "project-id-123"
 		@project_id = "project-id-123"
 		@folder_id = "folder-id-123"
 		@folder_id = "folder-id-123"
+		@owner_id = "onwer-id-123"
 		@callback = sinon.stub()
 		@callback = sinon.stub()
 		@ProjectUploadManager = SandboxedModule.require modulePath, requires:
 		@ProjectUploadManager = SandboxedModule.require modulePath, requires:
 			"./FileSystemImportManager" : @FileSystemImportManager = {}
 			"./FileSystemImportManager" : @FileSystemImportManager = {}
@@ -26,7 +27,7 @@ describe "ProjectUploadManager", ->
 				_id: @project_id
 				_id: @project_id
 				rootFolder: [ _id: @root_folder_id ]
 				rootFolder: [ _id: @root_folder_id ]
 			@ProjectCreationHandler.createBlankProject = sinon.stub().callsArgWith(2, null, @project)
 			@ProjectCreationHandler.createBlankProject = sinon.stub().callsArgWith(2, null, @project)
-			@ProjectUploadManager.insertZipArchiveIntoFolder = sinon.stub().callsArg(3)
+			@ProjectUploadManager.insertZipArchiveIntoFolder = sinon.stub().callsArg(4)
 			@ProjectRootDocManager.setRootDocAutomatically = sinon.stub().callsArg(1)
 			@ProjectRootDocManager.setRootDocAutomatically = sinon.stub().callsArg(1)
 			@ProjectUploadManager.createProjectFromZipArchive @owner_id, @name, @source, @callback
 			@ProjectUploadManager.createProjectFromZipArchive @owner_id, @name, @source, @callback
 
 
@@ -45,7 +46,7 @@ describe "ProjectUploadManager", ->
 		it "should insert the zip file contents into the root folder", ->
 		it "should insert the zip file contents into the root folder", ->
 			@ProjectUploadManager
 			@ProjectUploadManager
 				.insertZipArchiveIntoFolder
 				.insertZipArchiveIntoFolder
-				.calledWith(@project_id, @root_folder_id, @source)
+				.calledWith(@owner_id, @project_id, @root_folder_id, @source)
 				.should.equal true
 				.should.equal true
 
 
 		it "should automatically set the root doc", ->
 		it "should automatically set the root doc", ->
@@ -63,9 +64,10 @@ describe "ProjectUploadManager", ->
 			@destination = "/path/to/zile/file-extracted"
 			@destination = "/path/to/zile/file-extracted"
 			@ProjectUploadManager._getDestinationDirectory = sinon.stub().returns @destination
 			@ProjectUploadManager._getDestinationDirectory = sinon.stub().returns @destination
 			@ArchiveManager.extractZipArchive = sinon.stub().callsArg(2)
 			@ArchiveManager.extractZipArchive = sinon.stub().callsArg(2)
-			@FileSystemImportManager.addFolderContents = sinon.stub().callsArg(4)
+			@ArchiveManager.findTopLevelDirectory = sinon.stub().callsArgWith(1, null, @topLevelDestination = "/path/to/zip/file-extracted/nested")
+			@FileSystemImportManager.addFolderContents = sinon.stub().callsArg(5)
 
 
-			@ProjectUploadManager.insertZipArchiveIntoFolder @project_id, @folder_id, @source, @callback
+			@ProjectUploadManager.insertZipArchiveIntoFolder @owner_id, @project_id, @folder_id, @source, @callback
 
 
 		it "should set up the directory to extract the archive to", ->
 		it "should set up the directory to extract the archive to", ->
 			@ProjectUploadManager._getDestinationDirectory.calledWith(@source).should.equal true
 			@ProjectUploadManager._getDestinationDirectory.calledWith(@source).should.equal true
@@ -73,8 +75,11 @@ describe "ProjectUploadManager", ->
 		it "should extract the archive", ->
 		it "should extract the archive", ->
 			@ArchiveManager.extractZipArchive.calledWith(@source, @destination).should.equal true
 			@ArchiveManager.extractZipArchive.calledWith(@source, @destination).should.equal true
 
 
+		it "should find the top level directory", ->
+			@ArchiveManager.findTopLevelDirectory.calledWith(@destination).should.equal true
+
 		it "should insert the extracted archive into the folder", ->
 		it "should insert the extracted archive into the folder", ->
-			@FileSystemImportManager.addFolderContents.calledWith(@project_id, @folder_id, @destination, false)
+			@FileSystemImportManager.addFolderContents.calledWith(@owner_id, @project_id, @folder_id, @topLevelDestination, false)
 				.should.equal true
 				.should.equal true
 
 
 		it "should return the callback", ->
 		it "should return the callback", ->