diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index e5158cdb9b..d1dfe21dad 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -182,6 +182,14 @@ module.exports = ClsiManager = return callback(error) if error? callback(null, projectStateHash, docs) + getOutputFileStream: (project_id, user_id, build_id, output_file_path, callback=(err, readStream)->) -> + url = "#{Settings.apis.clsi.url}/project/#{project_id}/user/#{user_id}/build/#{build_id}/output/#{output_file_path}" + ClsiCookieManager.getCookieJar project_id, (err, jar)-> + return callback(err) if err? + options = { url: url, method: "GET", timeout: 60 * 1000, jar : jar } + readStream = request(options) + callback(null, readStream) + _buildRequestFromDocupdater: (project_id, options, project, projectStateHash, docUpdaterDocs, callback = (error, request) ->) -> ProjectEntityHandler.getAllDocPathsFromProject project, (error, docPath) -> return callback(error) if error? diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee index 13de54e947..34f77fde20 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee @@ -4,11 +4,27 @@ ProjectLocator = require '../Project/ProjectLocator' Settings = require 'settings-sharelatex' logger = require 'logger-sharelatex' _ = require 'underscore' +LinkedFilesHandler = require './LinkedFilesHandler' +{ + + UrlFetchFailedError, + InvalidUrlError, + OutputFileFetchFailedError, + AccessDeniedError, + BadEntityTypeError, + BadDataError, + ProjectNotFoundError, + V1ProjectNotFoundError, + SourceFileNotFoundError, +} = require './LinkedFilesErrors' + module.exports = LinkedFilesController = { + Agents: { url: require('./UrlAgent'), - project_file: require('./ProjectFileAgent') + project_file: require('./ProjectFileAgent'), + project_output_file: require('./ProjectOutputFileAgent') } _getAgent: (provider) -> @@ -18,15 +34,6 @@ module.exports = LinkedFilesController = { return null LinkedFilesController.Agents[provider] - _getFileById: (project_id, file_id, callback=(err, file)->) -> - ProjectLocator.findElement { - project_id, - element_id: file_id, - type: 'file' - }, (err, file, path, parentFolder) -> - return callback(err) if err? - callback(null, file, path, parentFolder) - createLinkedFile: (req, res, next) -> {project_id} = req.params {name, provider, data, parent_folder_id} = req.body @@ -37,23 +44,23 @@ module.exports = LinkedFilesController = { if !Agent? return res.sendStatus(400) - linkedFileData = Agent.sanitizeData(data) - linkedFileData.provider = provider + data.provider = provider - if !Agent.canCreate(linkedFileData) - return res.status(403).send('Cannot create linked file') - - LinkedFilesController._doImport( - req, res, next, Agent, project_id, user_id, - parent_folder_id, name, linkedFileData - ) + Agent.createLinkedFile project_id, + data, + name, + parent_folder_id, + user_id, + (err, newFileId) -> + return LinkedFilesController.handleError(err, req, res, next) if err? + res.json(new_file_id: newFileId) refreshLinkedFile: (req, res, next) -> {project_id, file_id} = req.params user_id = AuthenticationController.getLoggedInUserId(req) logger.log {project_id, file_id, user_id}, 'refresh linked file request' - LinkedFilesController._getFileById project_id, file_id, (err, file, path, parentFolder) -> + LinkedFilesHandler.getFileById project_id, file_id, (err, file, path, parentFolder) -> return next(err) if err? return res.sendStatus(404) if !file? name = file.name @@ -65,37 +72,51 @@ module.exports = LinkedFilesController = { Agent = LinkedFilesController._getAgent(provider) if !Agent? return res.sendStatus(400) - LinkedFilesController._doImport( - req, res, next, Agent, project_id, user_id, - parent_folder_id, name, linkedFileData + + Agent.refreshLinkedFile project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + (err, newFileId) -> + return LinkedFilesController.handleError(err, req, res, next) if err? + res.json(new_file_id: newFileId) + + handleError: (error, req, res, next) -> + if error instanceof BadDataError + res.status(400).send("The submitted data is not valid") + + else if error instanceof AccessDeniedError + res.status(403).send("You do not have access to this project") + + else if error instanceof BadDataError + res.status(400).send("The submitted data is not valid") + + else if error instanceof BadEntityTypeError + res.status(400).send("The file is the wrong type") + + else if error instanceof SourceFileNotFoundError + res.status(404).send("Source file not found") + + else if error instanceof ProjectNotFoundError + res.status(404).send("Project not found") + + else if error instanceof V1ProjectNotFoundError + res.status(409).send("Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file") + + else if error instanceof OutputFileFetchFailedError + res.status(404).send("Could not get output file") + + else if error instanceof UrlFetchFailedError + res.status(422).send( + "Your URL could not be reached (#{error.statusCode} status code). Please check it and try again." ) - _doImport: (req, res, next, Agent, project_id, user_id, parent_folder_id, name, linkedFileData) -> - Agent.checkAuth project_id, linkedFileData, user_id, (err, allowed) -> - return Agent.handleError(err, req, res, next) if err? - return res.sendStatus(403) if !allowed - Agent.decorateLinkedFileData linkedFileData, (err, newLinkedFileData) -> - return Agent.handleError(err) if err? - linkedFileData = newLinkedFileData - Agent.writeIncomingFileToDisk project_id, - linkedFileData, - user_id, - (error, fsPath) -> - if error? - logger.error( - {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, - 'error writing linked file to disk' - ) - return Agent.handleError(error, req, res, next) - EditorController.upsertFile project_id, - parent_folder_id, - name, - fsPath, - linkedFileData, - "upload", - user_id, - (error, file) -> - return next(error) if error? - res.json(new_file_id: file._id) # created + else if error instanceof InvalidUrlError + res.status(422).send( + "Your URL is not valid. Please check it and try again." + ) - } + else + next(error) +} diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesErrors.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesErrors.coffee new file mode 100644 index 0000000000..8cf671702e --- /dev/null +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesErrors.coffee @@ -0,0 +1,84 @@ +UrlFetchFailedError = (message) -> + error = new Error(message) + error.name = 'UrlFetchFailedError' + error.__proto__ = UrlFetchFailedError.prototype + return error +UrlFetchFailedError.prototype.__proto__ = Error.prototype + + +InvalidUrlError = (message) -> + error = new Error(message) + error.name = 'InvalidUrlError' + error.__proto__ = InvalidUrlError.prototype + return error +InvalidUrlError.prototype.__proto__ = Error.prototype + + +OutputFileFetchFailedError = (message) -> + error = new Error(message) + error.name = 'OutputFileFetchFailedError' + error.__proto__ = OutputFileFetchFailedError.prototype + return error +OutputFileFetchFailedError.prototype.__proto__ = Error.prototype + + +AccessDeniedError = (message) -> + error = new Error(message) + error.name = 'AccessDenied' + error.__proto__ = AccessDeniedError.prototype + return error +AccessDeniedError.prototype.__proto__ = Error.prototype + + +BadEntityTypeError = (message) -> + error = new Error(message) + error.name = 'BadEntityType' + error.__proto__ = BadEntityTypeError.prototype + return error +BadEntityTypeError.prototype.__proto__ = Error.prototype + + +BadDataError = (message) -> + error = new Error(message) + error.name = 'BadData' + error.__proto__ = BadDataError.prototype + return error +BadDataError.prototype.__proto__ = Error.prototype + + +ProjectNotFoundError = (message) -> + error = new Error(message) + error.name = 'ProjectNotFound' + error.__proto__ = ProjectNotFoundError.prototype + return error +ProjectNotFoundError.prototype.__proto__ = Error.prototype + + +V1ProjectNotFoundError = (message) -> + error = new Error(message) + error.name = 'V1ProjectNotFound' + error.__proto__ = V1ProjectNotFoundError.prototype + return error +V1ProjectNotFoundError.prototype.__proto__ = Error.prototype + + +SourceFileNotFoundError = (message) -> + error = new Error(message) + error.name = 'SourceFileNotFound' + error.__proto__ = SourceFileNotFoundError.prototype + return error +SourceFileNotFoundError.prototype.__proto__ = Error.prototype + + +module.exports = { + + UrlFetchFailedError, + InvalidUrlError, + OutputFileFetchFailedError, + AccessDeniedError, + BadEntityTypeError, + BadDataError, + ProjectNotFoundError, + V1ProjectNotFoundError, + SourceFileNotFoundError, +} diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee new file mode 100644 index 0000000000..6262f0a5ab --- /dev/null +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee @@ -0,0 +1,86 @@ +FileWriter = require '../../infrastructure/FileWriter' +EditorController = require '../Editor/EditorController' +ProjectLocator = require '../Project/ProjectLocator' +Project = require("../../models/Project").Project +ProjectGetter = require("../Project/ProjectGetter") +_ = require 'underscore' +{ + ProjectNotFoundError, + V1ProjectNotFoundError, + BadDataError +} = require './LinkedFilesErrors' + + +module.exports = LinkedFilesHandler = + + getFileById: (project_id, file_id, callback=(err, file)->) -> + ProjectLocator.findElement { + project_id, + element_id: file_id, + type: 'file' + }, (err, file, path, parentFolder) -> + return callback(err) if err? + callback(null, file, path, parentFolder) + + getSourceProject: (data, callback=(err, project)->) -> + projection = {_id: 1, name: 1} + if data.v1_source_doc_id? + Project.findOne {'overleaf.id': data.v1_source_doc_id}, projection, (err, project) -> + return callback(err) if err? + if !project? + return callback(new V1ProjectNotFoundError()) + callback(null, project) + else if data.source_project_id? + ProjectGetter.getProject data.source_project_id, projection, (err, project) -> + return callback(err) if err? + if !project? + return callback(new ProjectNotFoundError()) + callback(null, project) + else + callback(new BadDataError('neither v1 nor v2 id present')) + + importFromStream: ( + project_id, + readStream, + linkedFileData, + name, + parent_folder_id, + user_id, + callback=(err, file)-> + ) -> + callback = _.once(callback) + FileWriter.writeStreamToDisk project_id, readStream, (err, fsPath) -> + return callback(err) if err? + EditorController.upsertFile project_id, + parent_folder_id, + name, + fsPath, + linkedFileData, + "upload", + user_id, + (err, file) => + return callback(err) if err? + callback(null, file) + + importContent: ( + project_id, + content, + linkedFileData, + name, + parent_folder_id, + user_id, + callback=(err, file)-> + ) -> + callback = _.once(callback) + FileWriter.writeContentToDisk project_id, content, (err, fsPath) -> + return callback(err) if err? + EditorController.upsertFile project_id, + parent_folder_id, + name, + fsPath, + linkedFileData, + "upload", + user_id, + (err, file) => + return callback(err) if err? + callback(null, file) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee index 9ba8b5f1e7..77196ef1d4 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -1,68 +1,94 @@ -FileWriter = require('../../infrastructure/FileWriter') AuthorizationManager = require('../Authorization/AuthorizationManager') ProjectLocator = require('../Project/ProjectLocator') ProjectGetter = require('../Project/ProjectGetter') -Project = require("../../models/Project").Project DocstoreManager = require('../Docstore/DocstoreManager') FileStoreHandler = require('../FileStore/FileStoreHandler') -FileWriter = require('../../infrastructure/FileWriter') _ = require "underscore" Settings = require 'settings-sharelatex' +LinkedFilesHandler = require './LinkedFilesHandler' +{ + BadDataError, + AccessDeniedError, + BadEntityTypeError, + SourceFileNotFoundError, + ProjectNotFoundError, + V1ProjectNotFoundError +} = require './LinkedFilesErrors' +module.exports = ProjectFileAgent = { -AccessDeniedError = (message) -> - error = new Error(message) - error.name = 'AccessDenied' - error.__proto__ = AccessDeniedError.prototype - return error -AccessDeniedError.prototype.__proto__ = Error.prototype + createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> + if !@_canCreate(linkedFileData) + return callback(new AccessDeniedError()) + @_go(project_id, linkedFileData, name, parent_folder_id, user_id, callback) + refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> + @_go project_id, linkedFileData, name, parent_folder_id, user_id, callback -BadEntityTypeError = (message) -> - error = new Error(message) - error.name = 'BadEntityType' - error.__proto__ = BadEntityTypeError.prototype - return error -BadEntityTypeError.prototype.__proto__ = Error.prototype + _prepare: (project_id, linkedFileData, user_id, callback=(err, linkedFileData)->) -> + @_checkAuth project_id, linkedFileData, user_id, (err, allowed) => + return callback(err) if err? + return callback(new AccessDeniedError()) if !allowed + if !@_validate(linkedFileData) + return callback(new BadDataError()) + callback(null, linkedFileData) + _go: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> + linkedFileData = @_sanitizeData(linkedFileData) + @_prepare project_id, linkedFileData, user_id, (err, linkedFileData) => + return callback(err) if err? + if !@_validate(linkedFileData) + return callback(new BadDataError()) + @_getEntity linkedFileData, user_id, (err, source_project, entity, type) => + return callback(err) if err? + if type == 'doc' + DocstoreManager.getDoc source_project._id, entity._id, (err, lines) -> + return callback(err) if err? + LinkedFilesHandler.importContent project_id, + lines.join('\n'), + linkedFileData, + name, + parent_folder_id, + user_id, + (err, file) -> + return callback(err) if err? + callback(null, file._id) # Created + else if type == 'file' + FileStoreHandler.getFileStream source_project._id, entity._id, null, (err, fileStream) -> + return callback(err) if err? + LinkedFilesHandler.importFromStream project_id, + fileStream, + linkedFileData, + name, + parent_folder_id, + user_id, + (err, file) -> + return callback(err) if err? + callback(null, file._id) # Created + else + callback(new BadEntityTypeError()) -BadDataError = (message) -> - error = new Error(message) - error.name = 'BadData' - error.__proto__ = BadDataError.prototype - return error -BadDataError.prototype.__proto__ = Error.prototype + _getEntity: + (linkedFileData, current_user_id, callback = (err, entity, type) ->) -> + callback = _.once(callback) + { source_entity_path } = linkedFileData + @_getSourceProject linkedFileData, (err, project) -> + return callback(err) if err? + source_project_id = project._id + ProjectLocator.findElementByPath { + project_id: source_project_id, + path: source_entity_path + }, (err, entity, type) -> + if err? + if err.toString().match(/^not found.*/) + err = new SourceFileNotFoundError() + return callback(err) + callback(null, project, entity, type) - -ProjectNotFoundError = (message) -> - error = new Error(message) - error.name = 'ProjectNotFound' - error.__proto__ = ProjectNotFoundError.prototype - return error -ProjectNotFoundError.prototype.__proto__ = Error.prototype - - -V1ProjectNotFoundError = (message) -> - error = new Error(message) - error.name = 'V1ProjectNotFound' - error.__proto__ = V1ProjectNotFoundError.prototype - return error -V1ProjectNotFoundError.prototype.__proto__ = Error.prototype - - -SourceFileNotFoundError = (message) -> - error = new Error(message) - error.name = 'SourceFileNotFound' - error.__proto__ = SourceFileNotFoundError.prototype - return error -SourceFileNotFoundError.prototype.__proto__ = Error.prototype - - -module.exports = ProjectFileAgent = - - sanitizeData: (data) -> + _sanitizeData: (data) -> return _.pick( data, + 'provider', 'source_project_id', 'v1_source_doc_id', 'source_entity_path' @@ -74,34 +100,13 @@ module.exports = ProjectFileAgent = data.source_entity_path? ) - canCreate: (data) -> + _canCreate: (data) -> # Don't allow creation of linked-files with v1 doc ids !data.v1_source_doc_id? - _getSourceProject: (data, callback=(err, project)->) -> - projection = {_id: 1, name: 1} - if data.v1_source_doc_id? - Project.findOne {'overleaf.id': data.v1_source_doc_id}, projection, (err, project) -> - return callback(err) if err? - if !project? - return callback(new V1ProjectNotFoundError()) - callback(null, project) - else if data.source_project_id? - ProjectGetter.getProject data.source_project_id, projection, (err, project) -> - return callback(err) if err? - if !project? - return callback(new ProjectNotFoundError()) - callback(null, project) - else - callback(new BadDataError('neither v1 nor v2 id present')) + _getSourceProject: LinkedFilesHandler.getSourceProject - decorateLinkedFileData: (data, callback = (err, newData) ->) -> - callback = _.once(callback) - @_getSourceProject data, (err, project) -> - return callback(err) if err? - callback(err, _.extend(data, {source_project_display_name: project.name})) - - checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) -> + _checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) -> callback = _.once(callback) if !ProjectFileAgent._validate(data) return callback(new BadDataError()) @@ -110,52 +115,4 @@ module.exports = ProjectFileAgent = AuthorizationManager.canUserReadProject current_user_id, project._id, null, (err, canRead) -> return callback(err) if err? callback(null, canRead) - - writeIncomingFileToDisk: - (project_id, data, current_user_id, callback = (error, fsPath) ->) -> - callback = _.once(callback) - if !ProjectFileAgent._validate(data) - return callback(new BadDataError()) - { source_entity_path } = data - @_getSourceProject data, (err, project) -> - return callback(err) if err? - source_project_id = project._id - ProjectLocator.findElementByPath { - project_id: source_project_id, - path: source_entity_path - }, (err, entity, type) -> - if err? - if err.toString().match(/^not found.*/) - err = new SourceFileNotFoundError() - return callback(err) - ProjectFileAgent._writeEntityToDisk source_project_id, entity._id, type, callback - - _writeEntityToDisk: (project_id, entity_id, type, callback=(err, location)->) -> - callback = _.once(callback) - if type == 'doc' - DocstoreManager.getDoc project_id, entity_id, (err, lines) -> - return callback(err) if err? - FileWriter.writeLinesToDisk entity_id, lines, callback - else if type == 'file' - FileStoreHandler.getFileStream project_id, entity_id, null, (err, fileStream) -> - return callback(err) if err? - FileWriter.writeStreamToDisk entity_id, fileStream, callback - else - callback(new BadEntityTypeError()) - - handleError: (error, req, res, next) -> - if error instanceof AccessDeniedError - res.status(403).send("You do not have access to this project") - else if error instanceof BadDataError - res.status(400).send("The submitted data is not valid") - else if error instanceof BadEntityTypeError - res.status(400).send("The file is the wrong type") - else if error instanceof SourceFileNotFoundError - res.status(404).send("Source file not found") - else if error instanceof ProjectNotFoundError - res.status(404).send("Project not found") - else if error instanceof V1ProjectNotFoundError - res.status(409).send("Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file") - else - next(error) - next() +} diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee new file mode 100644 index 0000000000..5be0866ee2 --- /dev/null +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee @@ -0,0 +1,158 @@ +AuthorizationManager = require('../Authorization/AuthorizationManager') +ProjectGetter = require('../Project/ProjectGetter') +Settings = require 'settings-sharelatex' +CompileManager = require '../Compile/CompileManager' +ClsiManager = require '../Compile/ClsiManager' +ProjectFileAgent = require './ProjectFileAgent' +_ = require "underscore" +{ + BadDataError, + AccessDeniedError, + BadEntityTypeError, + OutputFileFetchFailedError +} = require './LinkedFilesErrors' +LinkedFilesHandler = require './LinkedFilesHandler' +logger = require 'logger-sharelatex' + + +module.exports = ProjectOutputFileAgent = { + + _prepare: (project_id, linkedFileData, user_id, callback=(err, linkedFileData)->) -> + @_checkAuth project_id, linkedFileData, user_id, (err, allowed) => + return callback(err) if err? + return callback(new AccessDeniedError()) if !allowed + if !@_validate(linkedFileData) + return callback(new BadDataError()) + callback(null, linkedFileData) + + createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> + if !@_canCreate(linkedFileData) + return callback(new AccessDeniedError()) + linkedFileData = @_sanitizeData(linkedFileData) + @_prepare project_id, linkedFileData, user_id, (err, linkedFileData) => + return callback(err) if err? + @_getFileStream linkedFileData, user_id, (err, readStream) => + return callback(err) if err? + readStream.on "error", callback + readStream.on "response", (response) => + if 200 <= response.statusCode < 300 + readStream.resume() + LinkedFilesHandler.importFromStream project_id, + readStream, + linkedFileData, + name, + parent_folder_id, + user_id, + (err, file) -> + return callback(err) if err? + callback(null, file._id) # Created + else + err = new OutputFileFetchFailedError( + "Output file fetch failed: #{linkedFileData.build_id}, #{linkedFileData.source_output_file_path}" + ) + err.statusCode = response.statusCode + callback(err) + + refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> + @_prepare project_id, linkedFileData, user_id, (err, linkedFileData) => + return callback(err) if err? + @_compileAndGetFileStream linkedFileData, user_id, (err, readStream, new_build_id) => + return callback(err) if err? + readStream.on "error", callback + readStream.on "response", (response) => + if 200 <= response.statusCode < 300 + readStream.resume() + linkedFileData.build_id = new_build_id + LinkedFilesHandler.importFromStream project_id, + readStream, + linkedFileData, + name, + parent_folder_id, + user_id, + (err, file) -> + return callback(err) if err? + callback(null, file._id) # Created + else + err = new OutputFileFetchFailedError( + "Output file fetch failed: #{linkedFileData.build_id}, #{linkedFileData.source_output_file_path}" + ) + err.statusCode = response.statusCode + callback(err) + + + _sanitizeData: (data) -> + return { + provider: data.provider, + source_project_id: data.source_project_id, + source_output_file_path: data.source_output_file_path, + build_id: data.build_id + } + + _canCreate: ProjectFileAgent._canCreate + + _getSourceProject: LinkedFilesHandler.getSourceProject + + _validate: (data) -> + return ( + (data.source_project_id? || data.v1_source_doc_id?) && + data.source_output_file_path? && + data.build_id? + ) + + _checkAuth: (project_id, data, current_user_id, callback = (err, allowed)->) -> + callback = _.once(callback) + if !@_validate(data) + return callback(new BadDataError()) + @_getSourceProject data, (err, project) -> + return callback(err) if err? + AuthorizationManager.canUserReadProject current_user_id, + project._id, + null, + (err, canRead) -> + return callback(err) if err? + callback(null, canRead) + + _getFileStream: (linkedFileData, user_id, callback=(err, fileStream)->) -> + callback = _.once(callback) + { source_output_file_path, build_id } = linkedFileData + @_getSourceProject linkedFileData, (err, project) -> + return callback(err) if err? + source_project_id = project._id + ClsiManager.getOutputFileStream source_project_id, + user_id, + build_id, + source_output_file_path, + (err, readStream) -> + return callback(err) if err? + readStream.pause() + callback(null, readStream) + + _compileAndGetFileStream: (linkedFileData, user_id, callback=(err, stream, build_id)->) -> + callback = _.once(callback) + { source_output_file_path } = linkedFileData + @_getSourceProject linkedFileData, (err, project) -> + return callback(err) if err? + source_project_id = project._id + CompileManager.compile source_project_id, + user_id, + {}, + (err, status, outputFiles) -> + return callback(err) if err? + if status != 'success' + return callback(new OutputFileFetchFailedError()) + outputFile = _.find( + outputFiles, + (o) => o.path == source_output_file_path + ) + if !outputFile? + return callback(new OutputFileFetchFailedError()) + build_id = outputFile.build + ClsiManager.getOutputFileStream source_project_id, + user_id, + build_id, + source_output_file_path, + (err, readStream) -> + return callback(err) if err? + readStream.pause() + callback(null, readStream, build_id) +} diff --git a/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee index d3748cf8d2..b5a6e49020 100644 --- a/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee @@ -1,67 +1,53 @@ request = require 'request' -FileWriter = require('../../infrastructure/FileWriter') _ = require "underscore" urlValidator = require 'valid-url' Settings = require 'settings-sharelatex' +{ InvalidUrlError, UrlFetchFailedError } = require './LinkedFilesErrors' +LinkedFilesHandler = require './LinkedFilesHandler' -UrlFetchFailedError = (message) -> - error = new Error(message) - error.name = 'UrlFetchFailedError' - error.__proto__ = UrlFetchFailedError.prototype - return error -UrlFetchFailedError.prototype.__proto__ = Error.prototype - -InvalidUrlError = (message) -> - error = new Error(message) - error.name = 'InvalidUrlError' - error.__proto__ = InvalidUrlError.prototype - return error -InvalidUrlError.prototype.__proto__ = Error.prototype module.exports = UrlAgent = { - UrlFetchFailedError: UrlFetchFailedError - InvalidUrlError: InvalidUrlError - sanitizeData: (data) -> + createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> + linkedFileData = @._sanitizeData(linkedFileData) + @_getUrlStream project_id, linkedFileData, user_id, (err, readStream) -> + return callback(err) if err? + readStream.on "error", callback + readStream.on "response", (response) -> + if 200 <= response.statusCode < 300 + readStream.resume() + LinkedFilesHandler.importFromStream project_id, + readStream, + linkedFileData, + name, + parent_folder_id, + user_id, + (err, file) -> + return callback(err) if err? + callback(null, file._id) # Created + else + error = new UrlFetchFailedError("url fetch failed: #{linkedFileData.url}") + error.statusCode = response.statusCode + callback(error) + + refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> + @createLinkedFile project_id, linkedFileData, name, parent_folder_id, user_id, callback + + _sanitizeData: (data) -> return { + provider: data.provider url: @._prependHttpIfNeeded(data.url) } - canCreate: (data) -> true - - decorateLinkedFileData: (data, callback = (err, newData) ->) -> - return callback(null, data) - - checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) -> - callback(null, true) - - writeIncomingFileToDisk: (project_id, data, current_user_id, callback = (error, fsPath) ->) -> + _getUrlStream: (project_id, data, current_user_id, callback = (error, fsPath) ->) -> callback = _.once(callback) url = data.url if !urlValidator.isWebUri(url) return callback(new InvalidUrlError("invalid url: #{url}")) - url = UrlAgent._wrapWithProxy(url) + url = @_wrapWithProxy(url) readStream = request.get(url) - readStream.on "error", callback - readStream.on "response", (response) -> - if 200 <= response.statusCode < 300 - FileWriter.writeStreamToDisk project_id, readStream, callback - else - error = new UrlFetchFailedError("url fetch failed: #{url}") - error.statusCode = response.statusCode - callback(error) - - handleError: (error, req, res, next) -> - if error instanceof UrlFetchFailedError - res.status(422).send( - "Your URL could not be reached (#{error.statusCode} status code). Please check it and try again." - ) - else if error instanceof InvalidUrlError - res.status(422).send( - "Your URL is not valid. Please check it and try again." - ) - else - next(error) + readStream.pause() + callback(null, readStream) _prependHttpIfNeeded: (url) -> if !url.match('://') diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index 22034200f5..b7be80cb47 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -55,7 +55,10 @@ module.exports = ProjectEntityUpdateHandler = self = 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) + fileProperties = name : SafePath.clean(origonalFileRef.name) + if origonalFileRef.linkedFileData? + fileProperties.linkedFileData = origonalFileRef.linkedFileData + fileRef = new File(fileProperties) 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" diff --git a/services/web/app/coffee/infrastructure/FileWriter.coffee b/services/web/app/coffee/infrastructure/FileWriter.coffee index 27b1f16921..4e37273338 100644 --- a/services/web/app/coffee/infrastructure/FileWriter.coffee +++ b/services/web/app/coffee/infrastructure/FileWriter.coffee @@ -15,11 +15,14 @@ module.exports = FileWriter = callback(null) writeLinesToDisk: (identifier, lines, callback = (error, fsPath)->) -> + FileWriter.writeContentToDisk(identifier, lines.join('\n'), callback) + + writeContentToDisk: (identifier, content, callback = (error, fsPath)->) -> callback = _.once(callback) fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}" FileWriter._ensureDumpFolderExists (error) -> return callback(error) if error? - fs.writeFile fsPath, lines.join('\n'), (error) -> + fs.writeFile fsPath, content, (error) -> return callback(error) if error? callback(null, fsPath) diff --git a/services/web/app/views/project/editor/binary-file.pug b/services/web/app/views/project/editor/binary-file.pug index f4ccf8c9dc..e7cd1e32ed 100644 --- a/services/web/app/views/project/editor/binary-file.pug +++ b/services/web/app/views/project/editor/binary-file.pug @@ -38,6 +38,7 @@ div.binary-file.full-size( ) #{translate("no_preview_available")} div.binary-file-footer + // Linked Files: URL div(ng-if="openFile.linkedFileData.provider == 'url'") p i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon @@ -47,6 +48,7 @@ div.binary-file.full-size( | | at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }} + // Linked Files: Project File div(ng-if="openFile.linkedFileData.provider == 'project_file'") p i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon @@ -54,14 +56,30 @@ div.binary-file.full-size( | a(ng-if='!openFile.linkedFileData.v1_source_doc_id' ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank") - | {{ openFile.linkedFileData.source_project_display_name }} + | Another project span(ng-if='openFile.linkedFileData.v1_source_doc_id') - | {{ openFile.linkedFileData.source_project_display_name }} + | Another project | /{{ openFile.linkedFileData.source_entity_path.slice(1) }}, | | at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }} - span(ng-if="openFile.linkedFileData.provider == 'url' || openFile.linkedFileData.provider == 'project_file'") + // Linked Files: Project Output File + div(ng-if="openFile.linkedFileData.provider == 'project_output_file'") + p + i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon + | Imported from the output of + | + a(ng-if='!openFile.linkedFileData.v1_source_doc_id' + ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank") + | Another project + span(ng-if='openFile.linkedFileData.v1_source_doc_id') + | Another project + | : {{ openFile.linkedFileData.source_output_file_path }}, + | + | at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }} + + // Bottom Controls + span(ng-if="openFile.linkedFileData.provider") button.btn.btn-success( href, ng-click="refreshFile(openFile)", ng-disabled="refreshing" diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index d16258d1c9..d877421a9d 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -308,165 +308,10 @@ script(type='text/ng-template', id='entityListItemTemplate') ng-repeat="child in entity.children | orderBy:[orderByFoldersFirst, 'name']" ) -script(type='text/ng-template', id='newDocModalTemplate') - .modal-header - h3 #{translate("new_file")} - .modal-body - form(novalidate, name="newDocForm") - div.alert.alert-danger(ng-if="error") - div(ng-switch="error") - span(ng-switch-when="already exists") #{translate("file_already_exists")} - span(ng-switch-default) {{error}} - input.form-control( - type="text", - placeholder="File Name", - required, - ng-model="inputs.name", - on-enter="create()", - select-name-on="open", - valid-file, - name="name" - ) - .text-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile") - | #{translate('files_cannot_include_invalid_characters')} - .modal-footer - button.btn.btn-default( - ng-disabled="state.inflight" - ng-click="cancel()" - ) #{translate("cancel")} - button.btn.btn-primary( - ng-disabled="newDocForm.$invalid || state.inflight" - ng-click="create()" - ) - span(ng-hide="state.inflight") #{translate("create")} - span(ng-show="state.inflight") #{translate("creating")}... - - -// Project Linked Files Modal -script(type='text/ng-template', id='projectLinkedFileModalTemplate') - .modal-header - h3 New file from Project - - .modal-body - div - div.alert.alert-danger(ng-if="state.error") Error, something went wrong! - div - form - .form-controls - label(for="project-select") Select a Project - span(ng-show="state.inFlight.projects") - |   - i.fa.fa-spinner.fa-spin - select.form-control( - name="project-select" - ng-model="data.selectedProjectId" - ng-disabled="!shouldEnableProjectSelect()" - ) - option(value="" disabled selected) - Please Select a Project - option( - ng-repeat="project in data.projects" - value="{{ project._id }}" - ) {{ project.name }} - - br - .form-controls - label(for="project-entity-select") Select a File - span(ng-show="state.inFlight.entities") - |   - i.fa.fa-spinner.fa-spin - select.form-control( - name="project-entity-select" - ng-model="data.selectedProjectEntity" - ng-disabled="!shouldEnableProjectEntitySelect()" - ) - option(value="" disabled selected) - Please Select a File - option( - ng-repeat="projectEntity in data.projectEntities" - value="{{ projectEntity.path }}" - ) {{ projectEntity.path.slice(1) }} - br - - .form-controls - label(for="name") File Name In This Project - input.form-control( - type="text" - placeholder="example.tex" - required - ng-model="data.name" - name="name" - ) - br - - .modal-footer - span(ng-show="state.inFlight.create") - i.fa.fa-spinner.fa-spin - |   - button.btn.btn-default( - ng-disabled="state.inflight" - ng-click="cancel()" - ) #{translate("cancel")} - button.btn.btn-primary( - ng-disabled="!shouldEnableCreateButton()" - ng-click="create()" - ) - span(ng-hide="state.inflight") #{translate("create")} - span(ng-show="state.inflight") #{translate("creating")}... - - -script(type='text/ng-template', id='linkedFileModalTemplate') - .modal-header - h3 New file from URL - .modal-body - form(novalidate, name="newLinkedFileForm") - div.alert.alert-danger(ng-if="error") - div(ng-switch="error") - span(ng-switch-when="already exists") #{translate("file_already_exists")} - span(ng-switch-default) {{error}} - label(for="url") URL to fetch the file from - input.form-control( - type="text", - placeholder="www.example.com/my_file", - required, - ng-model="inputs.url", - focus-on="open", - on-enter="create()", - name="url" - ) - .row-spaced - label(for="name") File name in this project - input.form-control( - type="text", - placeholder="my_file", - required, - ng-model="inputs.name", - ng-change="nameChangedByUser = true" - valid-file, - on-enter="create()", - name="name" - ) - .text-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile") - | #{translate('files_cannot_include_invalid_characters')} - .modal-footer - button.btn.btn-default( - ng-disabled="state.inflight" - ng-click="cancel()" - ) #{translate("cancel")} - button.btn.btn-primary( - ng-disabled="newLinkedFileForm.$invalid || state.inflight" - ng-click="create()" - ) - span(ng-hide="state.inflight") #{translate("create")} - span(ng-show="state.inflight") #{translate("creating")}... - - script(type='text/ng-template', id='newFolderModalTemplate') .modal-header h3 #{translate("new_folder")} .modal-body - div.alert.alert-danger(ng-if="error") - div(ng-switch="error") - span(ng-switch-when="already exists") #{translate("file_already_exists")} - span(ng-switch-default) {{error}} form(novalidate, name="newFolderForm") input.form-control( type="text", @@ -478,8 +323,12 @@ script(type='text/ng-template', id='newFolderModalTemplate') valid-file, name="name" ) - .text-danger.row-spaced-small(ng-show="newFolderForm.name.$error.validFile") - | #{translate('files_cannot_include_invalid_characters')} + div.alert.alert-danger.row-spaced-small(ng-show="newFolderForm.name.$error.validFile") + | #{translate('files_cannot_include_invalid_characters')} + div.alert.alert-danger.row-spaced-small(ng-if="error") + div(ng-switch="error") + span(ng-switch-when="already exists") #{translate("file_already_exists")} + span(ng-switch-default) {{error}} .modal-footer button.btn.btn-default( ng-disabled="state.inflight" @@ -492,70 +341,7 @@ script(type='text/ng-template', id='newFolderModalTemplate') span(ng-hide="state.inflight") #{translate("create")} span(ng-show="state.inflight") #{translate("creating")}... - -script(type="text/template", id="qq-file-uploader-template") - div.qq-uploader-selector - div(qq-hide-dropzone="").qq-upload-drop-area-selector.qq-upload-drop-area - span.qq-upload-drop-area-text-selector #{translate('drop_files_here_to_upload')} - div.qq-upload-button-selector.btn.btn-primary.btn-lg - div #{translate('upload')} - span.or.btn-lg #{translate('or')} - span.drag-here.btn-lg #{translate('drag_here')} - span.qq-drop-processing-selector - span #{translate('processing')} - span.qq-drop-processing-spinner-selector - ul.qq-upload-list-selector - li - div.qq-progress-bar-container-selector - div( - role="progressbar" - aria-valuenow="0" - aria-valuemin="0" - aria-valuemax="100" - class="qq-progress-bar-selector qq-progress-bar" - ) - span.qq-upload-file-selector.qq-upload-file - span.qq-upload-size-selector.qq-upload-size - a(type="button").qq-btn.qq-upload-cancel-selector.qq-upload-cancel #{translate('cancel')} - button(type="button").qq-btn.qq-upload-retry-selector.qq-upload-retry #{translate('retry')} - span(role="status").qq-upload-status-text-selector.qq-upload-status-text - -script(type="text/ng-template", id="uploadFileModalTemplate") - .modal-header - h3 #{translate("upload_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="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( - fine-upload - endpoint="/project/{{ project_id }}/upload" - template-id="qq-file-uploader-template" - multiple="true" - auto-upload="false" - on-complete-callback="onComplete" - on-upload-callback="onUpload" - on-validate-batch="onValidateBatch" - on-error-callback="onError" - on-submit-callback="onSubmit" - on-cancel-callback="onCancel" - control="control" - params="{'folder_id': parent_folder_id}" - ) - .modal-footer - button.btn.btn-default(ng-click="cancel()") #{translate("cancel")} - +include ./new-file-modal script(type='text/ng-template', id='deleteEntityModalTemplate') .modal-header diff --git a/services/web/app/views/project/editor/left-menu.pug b/services/web/app/views/project/editor/left-menu.pug index d85a45723b..2cedc0b37e 100644 --- a/services/web/app/views/project/editor/left-menu.pug +++ b/services/web/app/views/project/editor/left-menu.pug @@ -69,14 +69,6 @@ aside#left-menu.full-size( a(href="#" ng-click="richText()") i.fa.fa-exclamation.fa-fw | Rich Text - li - a(href="#" ng-click="openProjectLinkedFileModal()") - i.fa.fa-exclamation.fa-fw - | Project-Linked-File Modal - li - a(href="#" ng-click="openLinkedFileModal()") - i.fa.fa-exclamation.fa-fw - | URL-Linked-File Modal h4(ng-show="!anonymous") #{translate("settings")} diff --git a/services/web/app/views/project/editor/new-file-modal.pug b/services/web/app/views/project/editor/new-file-modal.pug new file mode 100644 index 0000000000..3dcd55bfb9 --- /dev/null +++ b/services/web/app/views/project/editor/new-file-modal.pug @@ -0,0 +1,217 @@ +script(type='text/ng-template', id='newFileModalTemplate') + .modal-header + h3 Add Files + .modal-body.modal-new-file + table + tr + td.modal-new-file--list + ul.list-unstyled + li(ng-class="type == 'doc' ? 'active' : null") + a(href, ng-click="type = 'doc'") + i.fa.fa-fw.fa-file + | + | New File + li(ng-class="type == 'upload' ? 'active' : null") + a(href, ng-click="type = 'upload'") + i.fa.fa-fw.fa-upload + | + | Upload + li(ng-class="type == 'project' ? 'active' : null") + a(href, ng-click="type = 'project'") + i.fa.fa-fw.fa-folder-open + | + | From Another Project + li(ng-class="type == 'url' ? 'active' : null") + a(href, ng-click="type = 'url'") + i.fa.fa-fw.fa-globe + | + | From External URL + td(class="modal-new-file--body modal-new-file--body-{{type}}") + div(ng-if="type == 'doc'", ng-controller="NewDocModalController") + form(novalidate, name="newDocForm") + label(for="name") File Name + input.form-control( + type="text", + placeholder="File Name", + required, + ng-model="inputs.name", + on-enter="create()", + select-name-on="open", + valid-file, + name="name" + ) + div.alert.alert-danger.row-spaced-small(ng-if="error") + div(ng-switch="error") + span(ng-switch-when="already exists") #{translate("file_already_exists")} + span(ng-switch-default) {{error}} + div.alert.alert-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile") + | #{translate('files_cannot_include_invalid_characters')} + div(ng-if="type == 'upload'", ng-controller="UploadFileModalController") + .alert.alert-warning.small.modal(ng-if="tooManyFiles") #{translate("maximum_files_uploaded_together", {max:"{{max_files}}"})} + .alert.alert-warning.small.modal(ng-if="rateLimitHit") #{translate("too_many_files_uploaded_throttled_short_period")} + .alert.alert-warning.small.modal(ng-if="notLoggedIn") #{translate("session_expired_redirecting_to_login", {seconds:"{{secondsToRedirect}}"})} + .alert.alert-warning.small.modal(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 + div( + fine-upload + endpoint="/project/{{ project_id }}/upload" + template-id="qq-file-uploader-template" + multiple="true" + auto-upload="false" + on-complete-callback="onComplete" + on-upload-callback="onUpload" + on-validate-batch="onValidateBatch" + on-error-callback="onError" + on-submit-callback="onSubmit" + on-cancel-callback="onCancel" + control="control" + params="{'folder_id': parent_folder_id}" + ) + div(ng-if="type == 'project'", ng-controller="ProjectLinkedFileModalController") + div + form + .form-controls + label(for="project-select") Select a Project + span(ng-show="state.inFlight.projects") + |   + i.fa.fa-spinner.fa-spin + select.form-control( + name="project-select" + ng-model="data.selectedProjectId" + ng-disabled="!shouldEnableProjectSelect()" + ) + option(value="" disabled selected) - Please Select a Project + option( + ng-repeat="project in data.projects" + value="{{ project._id }}" + ) {{ project.name }} + + .form-controls.row-spaced-small(ng-if="!state.isOutputFilesMode") + label(for="project-entity-select") Select a File + span(ng-show="state.inFlight.entities") + |   + i.fa.fa-spinner.fa-spin + select.form-control( + name="project-entity-select" + ng-model="data.selectedProjectEntity" + ng-disabled="!shouldEnableProjectEntitySelect()" + ) + option(value="" disabled selected) - Please Select a File + option( + ng-repeat="projectEntity in data.projectEntities" + value="{{ projectEntity.path }}" + ) {{ projectEntity.path.slice(1) }} + + .form-controls.row-spaced-small(ng-if="state.isOutputFilesMode") + label(for="project-entity-select") Select an Output File + span(ng-show="state.inFlight.compile") + |   + i.fa.fa-spinner.fa-spin + select.form-control( + name="project-output-file-select" + ng-model="data.selectedProjectOutputFile" + ng-disabled="!shouldEnableProjectOutputFileSelect()" + ) + option(value="" disabled selected) - Please Select an Output File + option( + ng-repeat="outputFile in data.projectOutputFiles" + value="{{ outputFile.path }}" + ) {{ outputFile.path }} + div.toggle-output-files-button + | or  + a( + href="#" + ng-click="toggleOutputFilesMode()" + ) + span(ng-show="state.isOutputFilesMode") select from source files + span(ng-show="!state.isOutputFilesMode") select from output files + + .form-controls.row-spaced-small + label(for="name") File Name In This Project + input.form-control( + type="text" + placeholder="example.tex" + required + ng-model="data.name" + name="name" + ) + div.alert.alert-danger.row-spaced-small(ng-if="state.error") Error, something went wrong! + div(ng-if="type == 'url'", ng-controller="UrlLinkedFileModalController") + form(novalidate, name="newLinkedFileForm") + label(for="url") URL to fetch the file from + input.form-control( + type="text", + placeholder="www.example.com/my_file", + required, + ng-model="inputs.url", + focus-on="open", + on-enter="create()", + name="url" + ) + .row-spaced.small + label(for="name") File name in this project + input.form-control( + type="text", + placeholder="my_file", + required, + ng-model="inputs.name", + ng-change="nameChangedByUser = true" + valid-file, + on-enter="create()", + name="name" + ) + .text-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile") + | #{translate('files_cannot_include_invalid_characters')} + div.alert.alert-danger.row-spaced-small(ng-if="error") + div(ng-switch="error") + span(ng-switch-when="already exists") #{translate("file_already_exists")} + span(ng-switch-default) {{error}} + .modal-footer + button.btn.btn-default( + ng-disabled="state.inflight" + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-primary( + ng-disabled="state.inflight || !state.valid" + ng-click="create()" + ng-hide="type == 'upload'" + ) + span(ng-hide="state.inflight") #{translate("create")} + span(ng-show="state.inflight") #{translate("creating")}... + +script(type="text/template", id="qq-file-uploader-template") + div.qq-uploader-selector + div(qq-hide-dropzone="").qq-upload-drop-area-selector.qq-upload-drop-area + span.qq-upload-drop-area-text-selector #{translate('drop_files_here_to_upload')} + div Drag here + div.row-spaced-small.small #{translate('or')} + div.row-spaced-small + div.qq-upload-button-selector.btn.btn-primary + | Select from your computer + span.qq-drop-processing-selector + span #{translate('processing')} + span.qq-drop-processing-spinner-selector + ul.qq-upload-list-selector + li + div.qq-progress-bar-container-selector + div( + role="progressbar" + aria-valuenow="0" + aria-valuemin="0" + aria-valuemax="100" + class="qq-progress-bar-selector qq-progress-bar" + ) + span.qq-upload-file-selector.qq-upload-file + span.qq-upload-size-selector.qq-upload-size + a(type="button").qq-btn.qq-upload-cancel-selector.qq-upload-cancel #{translate('cancel')} + button(type="button").qq-btn.qq-upload-retry-selector.qq-upload-retry #{translate('retry')} + span(role="status").qq-upload-status-text-selector.qq-upload-status-text diff --git a/services/web/docker-compose.yml b/services/web/docker-compose.yml index 9596fe5126..f2966da141 100644 --- a/services/web/docker-compose.yml +++ b/services/web/docker-compose.yml @@ -17,7 +17,7 @@ services: PROJECT_HISTORY_ENABLED: 'true' ENABLED_LINKED_FILE_TYPES: 'url' LINKED_URL_PROXY: 'http://localhost:6543' - ENABLED_LINKED_FILE_TYPES: 'url,project_file' + ENABLED_LINKED_FILE_TYPES: 'url,project_file,project_output_file' SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee depends_on: - redis diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee index 010e00476f..8573b3742d 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -4,10 +4,12 @@ define [ App.controller "FileTreeController", ["$scope", "$modal", "ide", "$rootScope", ($scope, $modal, ide, $rootScope) -> $scope.openNewDocModal = () -> $modal.open( - templateUrl: "newDocModalTemplate" - controller: "NewDocModalController" + templateUrl: "newFileModalTemplate" + controller: "NewFileModalController" + size: 'lg' resolve: { parent_folder: () -> ide.fileTreeManager.getCurrentFolder() + type: () -> 'doc' } ) @@ -22,37 +24,12 @@ define [ $scope.openUploadFileModal = () -> $modal.open( - templateUrl: "uploadFileModalTemplate" - controller: "UploadFileModalController" - scope: $scope - resolve: { - parent_folder: () -> ide.fileTreeManager.getCurrentFolder() - } - ) - - $scope.openLinkedFileModal = window.openLinkedFileModal = () -> - unless 'url' in window.data.enabledLinkedFileTypes - console.warn("Url linked files are not enabled") - return - $modal.open( - templateUrl: "linkedFileModalTemplate" - controller: "LinkedFileModalController" - scope: $scope - resolve: { - parent_folder: () -> ide.fileTreeManager.getCurrentFolder() - } - ) - - $scope.openProjectLinkedFileModal = window.openProjectLinkedFileModal = () -> - unless 'project_file' in window.data.enabledLinkedFileTypes - console.warn("Project linked files are not enabled") - return - $modal.open( - templateUrl: "projectLinkedFileModalTemplate" - controller: "ProjectLinkedFileModalController" - scope: $scope + templateUrl: "newFileModalTemplate" + controller: "NewFileModalController" + size: 'lg' resolve: { parent_folder: () -> ide.fileTreeManager.getCurrentFolder() + type: () -> 'upload' } ) @@ -67,42 +44,10 @@ define [ $scope.$broadcast "delete:selected" ] - App.controller "NewDocModalController", [ - "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", - ($scope, ide, $modalInstance, $timeout, parent_folder) -> - $scope.inputs = - name: "name.tex" - $scope.state = - inflight: false - - $modalInstance.opened.then () -> - $timeout () -> - $scope.$broadcast "open" - , 200 - - $scope.create = () -> - name = $scope.inputs.name - if !name? or name.length == 0 - return - $scope.state.inflight = true - ide.fileTreeManager - .createDoc(name, parent_folder) - .then () -> - $scope.state.inflight = false - $modalInstance.close() - .catch (response)-> - { data } = response - $scope.error = data - $scope.state.inflight = false - - $scope.cancel = () -> - $modalInstance.dismiss('cancel') - ] - App.controller "NewFolderModalController", [ "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", ($scope, ide, $modalInstance, $timeout, parent_folder) -> - $scope.inputs = + $scope.inputs = name: "name" $scope.state = inflight: false @@ -118,10 +63,10 @@ define [ return $scope.state.inflight = true ide.fileTreeManager - .createFolder(name, parent_folder) + .createFolder(name, $scope.parent_folder) .then () -> $scope.state.inflight = false - $modalInstance.close() + $modalInstance.dismiss('done') .catch (response)-> { data } = response $scope.error = data @@ -131,10 +76,60 @@ define [ $modalInstance.dismiss('cancel') ] + App.controller "NewFileModalController", [ + "$scope", "type", "parent_folder", "$modalInstance" + ($scope, type, parent_folder, $modalInstance) -> + $scope.type = type + $scope.parent_folder = parent_folder + $scope.state = { + inflight: false + valid: true + } + $scope.cancel = () -> + $modalInstance.dismiss('cancel') + $scope.create = () -> + $scope.$broadcast 'create' + $scope.$on 'done', () -> + $modalInstance.dismiss('done') + ] + + App.controller "NewDocModalController", [ + "$scope", "ide", "$timeout" + ($scope, ide, $timeout) -> + $scope.inputs = + name: "name.tex" + + validate = () -> + name = $scope.inputs.name + $scope.state.valid = (name? and name.length > 0) + $scope.$watch 'inputs.name', validate + + $timeout () -> + $scope.$broadcast "open" + , 200 + + $scope.$on 'create', () -> + name = $scope.inputs.name + if !name? or name.length == 0 + return + $scope.state.inflight = true + ide.fileTreeManager + .createDoc(name, $scope.parent_folder) + .then () -> + $scope.state.inflight = false + $scope.$emit 'done' + .catch (response)-> + { data } = response + $scope.error = data + $scope.state.inflight = false + + ] + App.controller "UploadFileModalController", [ - "$scope", "$rootScope", "ide", "$modalInstance", "$timeout", "parent_folder", "$window" - ($scope, $rootScope, ide, $modalInstance, $timeout, parent_folder, $window) -> - $scope.parent_folder_id = parent_folder?.id + "$scope", "$rootScope", "ide", "$timeout", "$window" + ($scope, $rootScope, ide, $timeout, $window) -> + $scope.parent_folder_id = $scope.parent_folder?.id + $scope.project_id = ide.project_id $scope.tooManyFiles = false $scope.rateLimitHit = false $scope.secondsToRedirect = 10 @@ -162,7 +157,7 @@ define [ if response.success $rootScope.$broadcast 'file:upload:complete', response if uploadCount == 0 and response? and response.success - $modalInstance.close("done") + $scope.$emit 'done' ), 250 $scope.onValidateBatch = (files)-> @@ -210,30 +205,44 @@ define [ $scope.doUpload = () -> $scope.control?.q?.uploadStoredFiles() - $scope.cancel = () -> - $modalInstance.dismiss('cancel') ] App.controller "ProjectLinkedFileModalController", [ - "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", - ($scope, ide, $modalInstance, $timeout, parent_folder) -> + "$scope", "ide", "$timeout", + ($scope, ide, $timeout) -> + $scope.data = projects: null # or [] selectedProjectId: null projectEntities: null # or [] + projectOutputFiles: null # or [] selectedProjectEntity: null + selectedProjectOutputFile: null + buildId: null name: null - $scope.state = - inFlight: - projects: false - entities: false - create: false - error: false + $scope.state.inFlight = + projects: false + entities: false + compile: false + $scope.state.isOutputFilesMode = false + $scope.state.error = false $scope.$watch 'data.selectedProjectId', (newVal, oldVal) -> return if !newVal $scope.data.selectedProjectEntity = null - $scope.getProjectEntities($scope.data.selectedProjectId) + $scope.data.selectedProjectOutputFile = null + if $scope.state.isOutputFilesMode + $scope.compileProjectAndGetOutputFiles($scope.data.selectedProjectId) + else + $scope.getProjectEntities($scope.data.selectedProjectId) + + $scope.$watch 'state.isOutputFilesMode', (newVal, oldVal) -> + return if !newVal and !oldVal + $scope.data.selectedProjectOutputFile = null + if newVal == true + $scope.compileProjectAndGetOutputFiles($scope.data.selectedProjectId) + else + $scope.getProjectEntities($scope.data.selectedProjectId) # auto-set filename based on selected file $scope.$watch 'data.selectedProjectEntity', (newVal, oldVal) -> @@ -242,15 +251,31 @@ define [ if fileName $scope.data.name = fileName + # auto-set filename based on selected file + $scope.$watch 'data.selectedProjectOutputFile', (newVal, oldVal) -> + return if !newVal + if newVal == 'output.pdf' + project = _.find($scope.data.projects, (p) -> p._id == $scope.data.selectedProjectId) + $scope.data.name = if project?.name? then "#{project.name}.pdf" else 'output.pdf' + else + fileName = newVal.split('/').reverse()[0] + if fileName + $scope.data.name = fileName + _setInFlight = (type) -> $scope.state.inFlight[type] = true _reset = (opts) -> isError = opts.err == true inFlight = $scope.state.inFlight - inFlight.projects = inFlight.entities = inFlight.create = false + inFlight.projects = inFlight.entities = inFlight.compile = false + $scope.state.inflight = false $scope.state.error = isError + $scope.toggleOutputFilesMode = () -> + return if !$scope.data.selectedProjectId + $scope.state.isOutputFilesMode = !$scope.state.isOutputFilesMode + $scope.shouldEnableProjectSelect = () -> { state, data } = $scope return !state.inFlight.projects && data.projects @@ -259,16 +284,33 @@ define [ { state, data } = $scope return !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProjectId - $scope.shouldEnableCreateButton = () -> + $scope.shouldEnableProjectOutputFileSelect = () -> + { state, data } = $scope + return !state.inFlight.projects && !state.inFlight.compile && data.projects && data.selectedProjectId + + + validate = () -> state = $scope.state data = $scope.data - return !state.inFlight.projects && + $scope.state.valid = !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProjectId && - data.projectEntities && - data.selectedProjectEntity && + ( + ( + !$scope.state.isOutputFilesMode && + data.projectEntities && + data.selectedProjectEntity + ) || + ( + $scope.state.isOutputFilesMode && + data.projectOutputFiles && + data.selectedProjectOutputFile + ) + ) && data.name + $scope.$watch 'state', validate, true + $scope.$watch 'data', validate, true $scope.getUserProjects = () -> _setInFlight('projects') @@ -295,50 +337,84 @@ define [ .catch (err) -> _reset(err: true) + $scope.compileProjectAndGetOutputFiles = (project_id) => + _setInFlight('compile') + ide.$http.post("/project/#{project_id}/compile", { + check: "silent", + draft: false, + incrementalCompilesEnabled: false + _csrf: window.csrfToken + }) + .then (resp) -> + if resp.data.status == 'success' + filteredFiles = resp.data.outputFiles.filter (f) -> + f.path.match(/.*\.(pdf|png|jpeg|jpg|gif)/) + $scope.data.projectOutputFiles = filteredFiles + $scope.data.buildId = filteredFiles?[0]?.build + console.log ">> build_id", $scope.data.buildId + _reset(err: false) + else + $scope.data.projectOutputFiles = null + _reset(err: true) + .catch (err) -> + console.error(err) + _reset(err: true) + $scope.init = () -> $scope.getUserProjects() $timeout($scope.init, 0) - $scope.create = () -> + $scope.$on 'create', () -> projectId = $scope.data.selectedProjectId - path = $scope.data.selectedProjectEntity name = $scope.data.name - if !name || !path || !projectId - _reset(err: true) - return + if $scope.state.isOutputFilesMode + provider = 'project_output_file' + payload = { + source_project_id: projectId, + source_output_file_path: $scope.data.selectedProjectOutputFile, + build_id: $scope.data.buildId + } + else + provider = 'project_file' + payload = { + source_project_id: projectId, + source_entity_path: $scope.data.selectedProjectEntity + } _setInFlight('create') ide.fileTreeManager - .createLinkedFile(name, parent_folder, 'project_file', { - source_project_id: projectId, - source_entity_path: path - }) + .createLinkedFile(name, $scope.parent_folder, provider, payload) .then () -> _reset(err: false) - $modalInstance.close() + $scope.$emit 'done' .catch (response)-> { data } = response _reset(err: true) - $scope.cancel = () -> - $modalInstance.dismiss('cancel') ] - # TODO: rename all this to UrlLinkedFilModalController - App.controller "LinkedFileModalController", [ - "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", - ($scope, ide, $modalInstance, $timeout, parent_folder) -> + App.controller "UrlLinkedFileModalController", [ + "$scope", "ide", "$timeout" + ($scope, ide, $timeout) -> $scope.inputs = name: "" url: "" $scope.nameChangedByUser = false - $scope.state = - inflight: false - $modalInstance.opened.then () -> - $timeout () -> - $scope.$broadcast "open" - , 200 + $timeout () -> + $scope.$broadcast "open" + , 200 + + validate = () -> + {name, url} = $scope.inputs + if !name? or name.length == 0 + $scope.state.valid = false + else if !url? or url.length == 0 + $scope.state.valid = false + else + $scope.state.valid = true + $scope.$watch 'inputs.name', validate + $scope.$watch 'inputs.url', validate $scope.$watch "inputs.url", (url) -> if url? and url != "" and !$scope.nameChangedByUser @@ -347,7 +423,7 @@ define [ if parts.length > 1 # Wait for at one / $scope.inputs.name = parts[0] - $scope.create = () -> + $scope.$on 'create', () -> {name, url} = $scope.inputs if !name? or name.length == 0 return @@ -355,15 +431,13 @@ define [ return $scope.state.inflight = true ide.fileTreeManager - .createLinkedFile(name, parent_folder, 'url', {url}) + .createLinkedFile(name, $scope.parent_folder, 'url', {url}) .then () -> $scope.state.inflight = false - $modalInstance.close() + $scope.$emit 'done' .catch (response)-> { data } = response $scope.error = data $scope.state.inflight = false - $scope.cancel = () -> - $modalInstance.dismiss('cancel') ] diff --git a/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee b/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee index ae7db45905..881d03d3cb 100644 --- a/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee +++ b/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee @@ -4,12 +4,6 @@ define [ ], (App) -> App.controller "TestControlsController", ($scope) -> - $scope.openProjectLinkedFileModal = () -> - window.openProjectLinkedFileModal() - - $scope.openLinkedFileModal = () -> - window.openLinkedFileModal() - $scope.richText = () -> current = window.location.toString() target = "#{current}#{if window.location.search then '&' else '?'}rt=true" diff --git a/services/web/public/stylesheets/app/editor/file-tree.less b/services/web/public/stylesheets/app/editor/file-tree.less index 4e26e4751d..845cc74a93 100644 --- a/services/web/public/stylesheets/app/editor/file-tree.less +++ b/services/web/public/stylesheets/app/editor/file-tree.less @@ -261,3 +261,43 @@ } } } + +.modal-new-file { + padding: 0; + table { + width: 100%; + td { + vertical-align: top; + } + } + .toggle-output-files-button { + font-size: 80%; + } +} + .modal-new-file--list { + background-color: @modal-footer-background-color; + width: 220px; + ul { + li { + padding: (@line-height-computed / 4); + a { + color: @text-color; + } + } + li.active { + background-color: white; + a { + color: @link-color; + } + } + } + } + + .modal-new-file--body { + padding: 20px; + padding-top: (@line-height-computed / 4); + } + + .modal-new-file--body-upload { + padding-top: 20px; + } diff --git a/services/web/public/stylesheets/components/fineupload.less b/services/web/public/stylesheets/components/fineupload.less index ee1e96164f..bb037b4087 100644 --- a/services/web/public/stylesheets/components/fineupload.less +++ b/services/web/public/stylesheets/components/fineupload.less @@ -10,13 +10,17 @@ } .qq-uploader-selector { text-align: center; - .drag-here { - border: 1px dashed #666; - vertical-align: middle; - } + border: 1px dashed #666; + border-radius: 6px; + vertical-align: middle; .help { margin-top: 6px; } + min-height: 200px; + padding: 20px; + display: flex; + flex-direction: column; + justify-content: center; } /*.qq-upload-button-selector { display: block; diff --git a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee index 875e887d81..0c369097cd 100644 --- a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee +++ b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee @@ -8,6 +8,8 @@ MockFileStoreApi = require './helpers/MockFileStoreApi' request = require "./helpers/request" User = require "./helpers/User" +MockClsiApi = require "./helpers/MockClsiApi" + express = require("express") LinkedUrlProxy = express() @@ -116,7 +118,6 @@ describe "LinkedFiles", -> provider: 'project_file', source_project_id: @project_two_id, source_entity_path: "/#{@source_doc_name}", - source_project_display_name: "plf-test-two" } expect(firstFile.name).to.equal('test-link.txt') done() @@ -149,7 +150,7 @@ describe "LinkedFiles", -> source_entity_path: "/#{@source_doc_name}", }, (error, response, body) => expect(response.statusCode).to.equal 403 - expect(body).to.equal 'Cannot create linked file' + expect(body).to.equal 'You do not have access to this project' done() describe "with a linked project_file from a v1 project that has not been imported", -> @@ -344,3 +345,105 @@ describe "LinkedFiles", -> # TODO: Add test for asking for host that return ENOTFOUND # (This will probably end up handled by the proxy) + + describe "creating a linked output file", -> + before (done) -> + async.series [ + (cb) => + @owner.createProject 'output-test-one', {template: 'blank'}, (error, project_id) => + @project_one_id = project_id + cb(error) + (cb) => + @owner.getProject @project_one_id, (error, project) => + @project_one = project + @project_one_root_folder_id = project.rootFolder[0]._id.toString() + cb(error) + (cb) => + @owner.createProject 'output-test-two', {template: 'blank'}, (error, project_id) => + @project_two_id = project_id + cb(error) + (cb) => + @owner.getProject @project_two_id, (error, project) => + @project_two = project + @project_two_root_folder_id = project.rootFolder[0]._id.toString() + cb(error) + ], done + + it 'should import the project.pdf file from the source project', (done) -> + @owner.request.post { + url: "/project/#{@project_one_id}/linked_file", + json: + name: 'test.pdf', + parent_folder_id: @project_one_root_folder_id, + provider: 'project_output_file', + data: + source_project_id: @project_two_id, + source_output_file_path: "project.pdf", + build_id: '1234-abcd' + }, (error, response, body) => + new_file_id = body.new_file_id + @existing_file_id = new_file_id + expect(new_file_id).to.exist + @owner.getProject @project_one_id, (error, project) => + return done(error) if error? + firstFile = project.rootFolder[0].fileRefs[0] + expect(firstFile._id.toString()).to.equal(new_file_id.toString()) + expect(firstFile.linkedFileData).to.deep.equal { + provider: 'project_output_file', + source_project_id: @project_two_id, + source_output_file_path: "project.pdf", + build_id: '1234-abcd' + } + expect(firstFile.name).to.equal('test.pdf') + done() + + it 'should refresh the file', (done) -> + @owner.request.post { + url: "/project/#{@project_one_id}/linked_file/#{@existing_file_id}/refresh", + json: true + }, (error, response, body) => + new_file_id = body.new_file_id + expect(new_file_id).to.exist + expect(new_file_id).to.not.equal @existing_file_id + @refreshed_file_id = new_file_id + @owner.getProject @project_one_id, (error, project) => + return done(error) if error? + firstFile = project.rootFolder[0].fileRefs[0] + expect(firstFile._id.toString()).to.equal(new_file_id.toString()) + expect(firstFile.name).to.equal('test.pdf') + done() + + describe "with a linked project_output_file from a v1 project that has not been imported", -> + before (done) -> + async.series [ + (cb) => + @owner.createProject 'output-v1-test-one', {template: 'blank'}, (error, project_id) => + @project_one_id = project_id + cb(error) + (cb) => + @owner.getProject @project_one_id, (error, project) => + @project_one = project + @project_one_root_folder_id = project.rootFolder[0]._id.toString() + @project_one.rootFolder[0].fileRefs.push { + linkedFileData: { + provider: "project_output_file", + v1_source_doc_id: 9999999, # We won't find this id in the database + source_output_file_path: "project.pdf", + build_id: '123' + }, + _id: "abcdef", + rev: 0, + created: new Date(), + name: "whatever.pdf" + } + @owner.saveProject @project_one, cb + ], done + + it 'should refuse to refresh', (done) -> + @owner.request.post { + url: "/project/#{@project_one_id}/linked_file/abcdef/refresh", + json: true + }, (error, response, body) => + expect(response.statusCode).to.equal 409 + expect(body).to.equal "Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file" + done() diff --git a/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee b/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee index 2f9180960d..0711ac613f 100644 --- a/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee @@ -1,9 +1,11 @@ express = require("express") +bodyParser = require "body-parser" app = express() module.exports = MockClsiApi = run: () -> - app.post "/project/:project_id/compile", (req, res, next) => + + compile = (req, res, next) => res.status(200).send { compile: status: 'success' @@ -21,6 +23,9 @@ module.exports = MockClsiApi = ] } + app.post "/project/:project_id/compile", compile + app.post "/project/:project_id/user/:user_id/compile", compile + app.get "/project/:project_id/build/:build_id/output/*", (req, res, next) -> filename = req.params[0] if filename == 'project.pdf' @@ -30,6 +35,12 @@ module.exports = MockClsiApi = else res.sendStatus(404) + app.get "/project/:project_id/user/:user_id/build/:build_id/output/:output_path", (req, res, next) => + res.status(200).send("hello") + + app.get "/project/:project_id/status", (req, res, next) => + res.status(200).send() + app.listen 3013, (error) -> throw error if error? .on "error", (error) -> diff --git a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee index 3a5b7a58bd..3de4546e6d 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityUpdateHandlerTests.coffee @@ -44,6 +44,8 @@ describe 'ProjectEntityUpdateHandler', -> else @._id = file_id @rev = 0 + if options.linkedFileData? + @linkedFileData = options.linkedFileData @docName = "doc-name" @docLines = ['1234','abc'] @@ -121,6 +123,35 @@ describe 'ProjectEntityUpdateHandler', -> .calledWithMatch(project_id, projectHistoryId, userId, changesMatcher) .should.equal true + describe 'copyFileFromExistingProjectWithProject, with linkedFileData', -> + + beforeEach -> + @oldProject_id = "123kljadas" + @oldFileRef = { + _id:"oldFileRef", + name:@fileName, + linkedFileData: @linkedFileData + } + @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, with the linkedFileData', -> + @ProjectEntityMongoUpdateHandler._putElement + .calledWithMatch( + @project, + folder_id, + { _id: file_id, name: @fileName, linkedFileData: @linkedFileData}, + "file" + ) + .should.equal true + describe 'updateDocLines', -> beforeEach -> @path = "/somewhere/something.tex" @@ -285,7 +316,7 @@ describe 'ProjectEntityUpdateHandler', -> beforeEach -> @path = "/path/to/file" - @newFile = {_id: file_id, rev: 0, name: @fileName} + @newFile = {_id: file_id, rev: 0, name: @fileName, linkedFileData: @linkedFileData} @TpdsUpdateSender.addFile = sinon.stub().yields() @ProjectEntityMongoUpdateHandler.addFile = sinon.stub().yields(null, {path: fileSystem: @path}, @project) @ProjectEntityUpdateHandler.addFile project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId, @callback @@ -330,7 +361,7 @@ describe 'ProjectEntityUpdateHandler', -> @newFileUrl = "new-file-url" @FileStoreHandler.uploadFileFromDisk = sinon.stub().yields(null, @newFileUrl) - @newFile = _id: new_file_id, name: "dummy-upload-filename", rev: 0 + @newFile = _id: new_file_id, name: "dummy-upload-filename", rev: 0, linkedFileData: @linkedFileData @oldFile = _id: file_id @path = "/path/to/file" @ProjectEntityMongoUpdateHandler._insertDeletedFileReference = sinon.stub().yields()