From 5abb745e05131a0d7ab5a88e15d7fe24bdb00ea4 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 22 Jun 2018 10:00:39 +0100 Subject: [PATCH 01/27] Copy linkedFileData when cloning a project --- .../Features/Project/ProjectEntityUpdateHandler.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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" From fadbd72837997eeb7fd5eb63fbf26d7f933d2493 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 22 Jun 2018 10:52:49 +0100 Subject: [PATCH 02/27] Add unit test for copying project with linked files --- .../ProjectEntityUpdateHandlerTests.coffee | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) 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() From 60ca298db3e179d52de5afb44577b5ee9e615e33 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 8 Jun 2018 09:35:52 +0100 Subject: [PATCH 03/27] WIP --- .../app/views/project/editor/file-tree.pug | 2 +- .../app/views/project/editor/left-menu.pug | 4 + .../controllers/FileTreeController.coffee | 85 ++++++++++++++++--- .../ide/pdf/controllers/PdfController.coffee | 1 + .../controllers/TestControlsController.coffee | 3 + 5 files changed, 80 insertions(+), 15 deletions(-) diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index d16258d1c9..18d313f5cb 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -345,7 +345,7 @@ script(type='text/ng-template', id='newDocModalTemplate') // Project Linked Files Modal script(type='text/ng-template', id='projectLinkedFileModalTemplate') .modal-header - h3 New file from Project + h3 New file from Project ({{ isOutputFiles }}) .modal-body div diff --git a/services/web/app/views/project/editor/left-menu.pug b/services/web/app/views/project/editor/left-menu.pug index d85a45723b..c1ff891844 100644 --- a/services/web/app/views/project/editor/left-menu.pug +++ b/services/web/app/views/project/editor/left-menu.pug @@ -73,6 +73,10 @@ aside#left-menu.full-size( a(href="#" ng-click="openProjectLinkedFileModal()") i.fa.fa-exclamation.fa-fw | Project-Linked-File Modal + li + a(href="#" ng-click="openProjectOutputLinkedFileModal()") + i.fa.fa-exclamation.fa-fw + | Project-Output-Linked-File Modal li a(href="#" ng-click="openLinkedFileModal()") i.fa.fa-exclamation.fa-fw 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..422e294129 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -53,6 +53,21 @@ define [ scope: $scope resolve: { parent_folder: () -> ide.fileTreeManager.getCurrentFolder() + isOutputFiles: () -> false + } + ) + + $scope.openProjectOutputLinkedFileModal = window.openProjectOutputLinkedFileModal = () -> + unless 'project_output_file' in window.data.enabledLinkedFileTypes + console.warn("Project linked output files are not enabled") + return + $modal.open( + templateUrl: "projectLinkedFileModalTemplate" + controller: "ProjectLinkedFileModalController" + scope: $scope + resolve: { + parent_folder: () -> ide.fileTreeManager.getCurrentFolder() + isOutputFiles: () -> true } ) @@ -215,25 +230,33 @@ define [ ] App.controller "ProjectLinkedFileModalController", [ - "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", - ($scope, ide, $modalInstance, $timeout, parent_folder) -> + "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", "isOutputFiles", + ($scope, ide, $modalInstance, $timeout, parent_folder, isOutputFiles) -> + $scope.isOutputFiles = isOutputFiles + console.log ">>>> LINKED", isOutputFiles + $scope.data = projects: null # or [] selectedProjectId: null projectEntities: null # or [] + projectOutputFiles: null # or [] selectedProjectEntity: null name: null $scope.state = inFlight: projects: false entities: false + compile: false create: false error: false $scope.$watch 'data.selectedProjectId', (newVal, oldVal) -> return if !newVal $scope.data.selectedProjectEntity = null - $scope.getProjectEntities($scope.data.selectedProjectId) + if isOutputFiles + $scope.compileProjectAndGetOutputFiles($scope.data.selectedProjectId) + else + $scope.getProjectEntities($scope.data.selectedProjectId) # auto-set filename based on selected file $scope.$watch 'data.selectedProjectEntity', (newVal, oldVal) -> @@ -248,7 +271,7 @@ define [ _reset = (opts) -> isError = opts.err == true inFlight = $scope.state.inFlight - inFlight.projects = inFlight.entities = inFlight.create = false + inFlight.projects = inFlight.entities = inFlight.compile = inFlight.create = false $scope.state.error = isError $scope.shouldEnableProjectSelect = () -> @@ -259,6 +282,10 @@ define [ { state, data } = $scope return !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProjectId + $scope.shouldEnableProjectOutputFileSelect = () -> + { state, data } = $scope + return !state.inFlight.projects && !state.inFlight.compile && data.projects && data.selectedProjectId + $scope.shouldEnableCreateButton = () -> state = $scope.state data = $scope.data @@ -266,8 +293,18 @@ define [ !state.inFlight.entities && data.projects && data.selectedProjectId && - data.projectEntities && - data.selectedProjectEntity && + ( + ( + !isOutputFiles && + data.projectEntities && + data.selectedProjectEntity + ) || + ( + isOutputFiles && + data.projectOutputFiles && + data.selectedProjectOutputFile + ) + ) && data.name $scope.getUserProjects = () -> @@ -295,23 +332,43 @@ define [ .catch (err) -> _reset(err: true) + window._C = $scope.compileProjectAndGetOutputFiles = (project_id) => + _setInFlight('compile') + $http.post("/project/${project_id}/compile", { + check: "silent", + draft: false, + incrementalCompilesEnabled: false + _csrf: window.csrfToken + }) + .then (data) -> + console.log ">> COMPILE", data + _reset(err: false) + .catch (err) -> + console.error(err) + _reset(err: true) + $scope.init = () -> $scope.getUserProjects() $timeout($scope.init, 0) $scope.create = () -> projectId = $scope.data.selectedProjectId - path = $scope.data.selectedProjectEntity name = $scope.data.name - if !name || !path || !projectId - _reset(err: true) - return + if isOutputFiles + provider = 'project_output_file' + payload = { + source_project_id: projectId, + source_output_file_path: $scope.data.selectedProjectOutputFile + } + 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, parent_folder, provider, payload) .then () -> _reset(err: false) $modalInstance.close() diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 77a94a564b..098b436e86 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -524,6 +524,7 @@ define [ { data } = response $scope.pdf.view = "pdf" $scope.pdf.compiling = false + console.log ">>", data parseCompileResponse(data) .catch (response) -> { data, status } = response 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..0dca64dc5b 100644 --- a/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee +++ b/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee @@ -7,6 +7,9 @@ define [ $scope.openProjectLinkedFileModal = () -> window.openProjectLinkedFileModal() + $scope.openProjectOutputLinkedFileModal = () -> + window.openProjectOutputLinkedFileModal() + $scope.openLinkedFileModal = () -> window.openLinkedFileModal() From ead245721be6cc4d0c8b6ab4446f0437e5e7f47a Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 8 Jun 2018 12:32:22 +0100 Subject: [PATCH 04/27] Mostly working selection of output files from another project --- .../app/views/project/editor/file-tree.pug | 20 ++++++++- .../controllers/FileTreeController.coffee | 43 ++++++++++++------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index 18d313f5cb..70fa5badc9 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -345,7 +345,7 @@ script(type='text/ng-template', id='newDocModalTemplate') // Project Linked Files Modal script(type='text/ng-template', id='projectLinkedFileModalTemplate') .modal-header - h3 New file from Project ({{ isOutputFiles }}) + h3 New file from Project ({{ isOutputFilesMode }}) .modal-body div @@ -369,7 +369,7 @@ script(type='text/ng-template', id='projectLinkedFileModalTemplate') ) {{ project.name }} br - .form-controls + .form-controls(ng-if="!isOutputFilesMode") label(for="project-entity-select") Select a File span(ng-show="state.inFlight.entities") |   @@ -384,6 +384,22 @@ script(type='text/ng-template', id='projectLinkedFileModalTemplate') ng-repeat="projectEntity in data.projectEntities" value="{{ projectEntity.path }}" ) {{ projectEntity.path.slice(1) }} + + .form-controls(ng-if="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 }} br .form-controls 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 422e294129..eec7d0627f 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -53,7 +53,7 @@ define [ scope: $scope resolve: { parent_folder: () -> ide.fileTreeManager.getCurrentFolder() - isOutputFiles: () -> false + isOutputFilesMode: () -> false } ) @@ -67,7 +67,7 @@ define [ scope: $scope resolve: { parent_folder: () -> ide.fileTreeManager.getCurrentFolder() - isOutputFiles: () -> true + isOutputFilesMode: () -> true } ) @@ -230,10 +230,10 @@ define [ ] App.controller "ProjectLinkedFileModalController", [ - "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", "isOutputFiles", - ($scope, ide, $modalInstance, $timeout, parent_folder, isOutputFiles) -> - $scope.isOutputFiles = isOutputFiles - console.log ">>>> LINKED", isOutputFiles + "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", "isOutputFilesMode", + ($scope, ide, $modalInstance, $timeout, parent_folder, isOutputFilesMode) -> + $scope.isOutputFilesMode = isOutputFilesMode + console.log ">>>> LINKED", isOutputFilesMode $scope.data = projects: null # or [] @@ -241,6 +241,7 @@ define [ projectEntities: null # or [] projectOutputFiles: null # or [] selectedProjectEntity: null + selectedProjectOutputFile: null name: null $scope.state = inFlight: @@ -253,7 +254,7 @@ define [ $scope.$watch 'data.selectedProjectId', (newVal, oldVal) -> return if !newVal $scope.data.selectedProjectEntity = null - if isOutputFiles + if isOutputFilesMode $scope.compileProjectAndGetOutputFiles($scope.data.selectedProjectId) else $scope.getProjectEntities($scope.data.selectedProjectId) @@ -265,6 +266,13 @@ define [ if fileName $scope.data.name = fileName + # auto-set filename based on selected file + $scope.$watch 'data.selectedProjectOutputFile', (newVal, oldVal) -> + return if !newVal + fileName = newVal.split('/').reverse()[0] + if fileName + $scope.data.name = fileName + _setInFlight = (type) -> $scope.state.inFlight[type] = true @@ -295,12 +303,12 @@ define [ data.selectedProjectId && ( ( - !isOutputFiles && + !isOutputFilesMode && data.projectEntities && data.selectedProjectEntity ) || ( - isOutputFiles && + isOutputFilesMode && data.projectOutputFiles && data.selectedProjectOutputFile ) @@ -332,17 +340,22 @@ define [ .catch (err) -> _reset(err: true) - window._C = $scope.compileProjectAndGetOutputFiles = (project_id) => + $scope.compileProjectAndGetOutputFiles = (project_id) => _setInFlight('compile') - $http.post("/project/${project_id}/compile", { + ide.$http.post("/project/#{project_id}/compile", { check: "silent", draft: false, incrementalCompilesEnabled: false _csrf: window.csrfToken }) - .then (data) -> - console.log ">> COMPILE", data - _reset(err: false) + .then (resp) -> + console.log ">> COMPILE", resp.data + if resp.data.status == 'success' + $scope.data.projectOutputFiles = resp.data.outputFiles + _reset(err: false) + else + $scope.data.projectOutputFiles = null + _reset(err: true) .catch (err) -> console.error(err) _reset(err: true) @@ -354,7 +367,7 @@ define [ $scope.create = () -> projectId = $scope.data.selectedProjectId name = $scope.data.name - if isOutputFiles + if isOutputFilesMode provider = 'project_output_file' payload = { source_project_id: projectId, From e916d967929cfa1b09d1965e1921534afbb83628 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 8 Jun 2018 15:33:02 +0100 Subject: [PATCH 05/27] WIP: basic backend for project-output-file agent --- .../coffee/Features/LinkedFiles/LinkedFilesController.coffee | 3 ++- services/web/app/views/project/editor/file-tree.pug | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee index 13de54e947..00c061778c 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee @@ -8,7 +8,8 @@ _ = require 'underscore' module.exports = LinkedFilesController = { Agents: { url: require('./UrlAgent'), - project_file: require('./ProjectFileAgent') + project_file: require('./ProjectFileAgent'), + project_output_file: require('./ProjectOutputFileAgent') } _getAgent: (provider) -> diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index 70fa5badc9..89e6db9f17 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -345,7 +345,9 @@ script(type='text/ng-template', id='newDocModalTemplate') // Project Linked Files Modal script(type='text/ng-template', id='projectLinkedFileModalTemplate') .modal-header - h3 New file from Project ({{ isOutputFilesMode }}) + h3 + span(ng-if="!isOutputFilesMode") New file from Project + span(ng-if="isOutputFilesMode") New file from Project output .modal-body div From d4beba24b62626bd411dea0c3734b953d3263a91 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 8 Jun 2018 16:06:47 +0100 Subject: [PATCH 06/27] Backend for project output file agent --- .../LinkedFiles/ProjectOutputFileAgent.coffee | 102 ++++++++++++++++++ .../app/views/project/editor/binary-file.pug | 20 +++- 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee 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..4850abd0e0 --- /dev/null +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee @@ -0,0 +1,102 @@ +FileWriter = require('../../infrastructure/FileWriter') +AuthorizationManager = require('../Authorization/AuthorizationManager') +ProjectGetter = require('../Project/ProjectGetter') +FileWriter = require('../../infrastructure/FileWriter') +Settings = require 'settings-sharelatex' +CompileManager = require '../Compile/CompileManager' +CompileController = require '../Compile/CompileController' +ClsiCookieManager = require '../Compile/ClsiCookieManager' +_ = require "underscore" +request = require "request" + + +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 + + +OutputFileFetchFailedError = (message) -> + error = new Error(message) + error.name = 'OutputFileFetchFailedError' + error.__proto__ = OutputFileFetchFailedError.prototype + return error +OutputFileFetchFailedError.prototype.__proto__ = Error.prototype + + +module.exports = ProjectOutputFileAgent = { + + sanitizeData: (data) -> + return { + source_project_id: data.source_project_id, + source_output_file_path: data.source_output_file_path + } + + canCreate: (data) -> true + + decorateLinkedFileData: (data, callback = (err, newData) ->) -> + callback = _.once(callback) + ProjectGetter.getProject data.source_project_id, {name: 1}, (err, project) -> + return callback(err) if err? + if !project? + return callback(new ProjectNotFoundError()) + callback(err, _.extend(data, {source_project_display_name: project.name})) + + checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) -> + callback = _.once(callback) + { source_project_id } = data + AuthorizationManager.canUserReadProject current_user_id, source_project_id, null, (err, canRead) -> + return callback(err) if err? + callback(null, canRead) + + _validate: (data) -> + data.source_project_id? && data.source_output_file_path? + + writeIncomingFileToDisk: (project_id, data, current_user_id, callback = (error, fsPath) ->) -> + callback = _.once(callback) + # TODO: + # - Compile project + # - Get output file content + # - Write to disk + # - callback with fs-path + if !ProjectOutputFileAgent._validate(data) + return callback(new BadDataError()) + { source_project_id, source_output_file_path } = data + CompileManager.compile source_project_id, null, {}, (err) -> + return callback(err) if err? + url = "#{Settings.apis.clsi.url}/project/#{source_project_id}/output/#{source_output_file_path}" + ClsiCookieManager.getCookieJar source_project_id, (err, jar)-> + return callback(err) if err? + oneMinute = 60 * 1000 + # the base request + options = { url: url, method: "GET", timeout: oneMinute, jar : jar } + readStream = request(options) + readStream.on "error", callback + readStream.on "response", (response) -> + if 200 <= response.statusCode < 300 + FileWriter.writeStreamToDisk project_id, readStream, callback + else + error = new OutputFileFetchFailedError("Output file fetch failed: #{url}") + error.statusCode = response.statusCode + callback(error) + + handleError: (error, req, res, next) -> + if error instanceof BadDataError + res.status(400).send("The submitted data is not valid") + 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 + next(error) +} diff --git a/services/web/app/views/project/editor/binary-file.pug b/services/web/app/views/project/editor/binary-file.pug index f4ccf8c9dc..bd0e4b9732 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 @@ -61,7 +63,23 @@ div.binary-file.full-size( | | 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") + | {{ openFile.linkedFileData.source_project_display_name }} + span(ng-if='openFile.linkedFileData.v1_source_doc_id') + | {{ openFile.linkedFileData.source_project_display_name }} + | : {{ 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" From 87474ce060470d2206c3bdc6cb0f3cff48e95391 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 8 Jun 2018 16:13:18 +0100 Subject: [PATCH 07/27] Remove commentary --- .../Features/LinkedFiles/ProjectOutputFileAgent.coffee | 5 ----- 1 file changed, 5 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee index 4850abd0e0..8af8b5bf63 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee @@ -64,11 +64,6 @@ module.exports = ProjectOutputFileAgent = { writeIncomingFileToDisk: (project_id, data, current_user_id, callback = (error, fsPath) ->) -> callback = _.once(callback) - # TODO: - # - Compile project - # - Get output file content - # - Write to disk - # - callback with fs-path if !ProjectOutputFileAgent._validate(data) return callback(new BadDataError()) { source_project_id, source_output_file_path } = data From 2da1d57948cea414efd1dd123a9a228f8ca44dc6 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 8 Jun 2018 16:22:34 +0100 Subject: [PATCH 08/27] Clean up logging --- .../coffee/ide/file-tree/controllers/FileTreeController.coffee | 2 -- .../web/public/coffee/ide/pdf/controllers/PdfController.coffee | 1 - 2 files changed, 3 deletions(-) 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 eec7d0627f..f929474952 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -233,7 +233,6 @@ define [ "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", "isOutputFilesMode", ($scope, ide, $modalInstance, $timeout, parent_folder, isOutputFilesMode) -> $scope.isOutputFilesMode = isOutputFilesMode - console.log ">>>> LINKED", isOutputFilesMode $scope.data = projects: null # or [] @@ -349,7 +348,6 @@ define [ _csrf: window.csrfToken }) .then (resp) -> - console.log ">> COMPILE", resp.data if resp.data.status == 'success' $scope.data.projectOutputFiles = resp.data.outputFiles _reset(err: false) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 098b436e86..77a94a564b 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -524,7 +524,6 @@ define [ { data } = response $scope.pdf.view = "pdf" $scope.pdf.compiling = false - console.log ">>", data parseCompileResponse(data) .catch (response) -> { data, status } = response From 5717496685caee9144e7dba5840626c0f8cd49d8 Mon Sep 17 00:00:00 2001 From: James Allen Date: Sun, 10 Jun 2018 15:44:00 +0100 Subject: [PATCH 09/27] Create unified new file modal with linked files --- .../app/views/project/editor/file-tree.pug | 246 +----------------- .../views/project/editor/new-file-modal.pug | 209 +++++++++++++++ .../controllers/FileTreeController.coffee | 222 ++++++++-------- .../stylesheets/app/editor/file-tree.less | 37 +++ .../stylesheets/components/fineupload.less | 12 +- 5 files changed, 363 insertions(+), 363 deletions(-) create mode 100644 services/web/app/views/project/editor/new-file-modal.pug diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index 89e6db9f17..d877421a9d 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -308,183 +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 - span(ng-if="!isOutputFilesMode") New file from Project - span(ng-if="isOutputFilesMode") New file from Project output - - .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(ng-if="!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(ng-if="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 }} - 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", @@ -496,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" @@ -510,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/new-file-modal.pug b/services/web/app/views/project/editor/new-file-modal.pug new file mode 100644 index 0000000000..48c8863e5b --- /dev/null +++ b/services/web/app/views/project/editor/new-file-modal.pug @@ -0,0 +1,209 @@ +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="!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="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 }} + + .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/public/coffee/ide/file-tree/controllers/FileTreeController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee index f929474952..6d92675b4e 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,52 +24,12 @@ define [ $scope.openUploadFileModal = () -> $modal.open( - templateUrl: "uploadFileModalTemplate" - controller: "UploadFileModalController" - scope: $scope + templateUrl: "newFileModalTemplate" + controller: "NewFileModalController" + size: 'lg' 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 - resolve: { - parent_folder: () -> ide.fileTreeManager.getCurrentFolder() - isOutputFilesMode: () -> false - } - ) - - $scope.openProjectOutputLinkedFileModal = window.openProjectOutputLinkedFileModal = () -> - unless 'project_output_file' in window.data.enabledLinkedFileTypes - console.warn("Project linked output files are not enabled") - return - $modal.open( - templateUrl: "projectLinkedFileModalTemplate" - controller: "ProjectLinkedFileModalController" - scope: $scope - resolve: { - parent_folder: () -> ide.fileTreeManager.getCurrentFolder() - isOutputFilesMode: () -> true + type: () -> 'upload' } ) @@ -82,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 @@ -133,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 @@ -146,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 @@ -177,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)-> @@ -225,14 +205,11 @@ define [ $scope.doUpload = () -> $scope.control?.q?.uploadStoredFiles() - $scope.cancel = () -> - $modalInstance.dismiss('cancel') ] App.controller "ProjectLinkedFileModalController", [ - "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", "isOutputFilesMode", - ($scope, ide, $modalInstance, $timeout, parent_folder, isOutputFilesMode) -> - $scope.isOutputFilesMode = isOutputFilesMode + "$scope", "ide", "$timeout", + ($scope, ide, $timeout) -> $scope.data = projects: null # or [] @@ -242,13 +219,11 @@ define [ selectedProjectEntity: null selectedProjectOutputFile: null name: null - $scope.state = - inFlight: - projects: false - entities: false - compile: false - create: false - error: false + $scope.state.inFlight = + projects: false + entities: false + compile: false + $scope.state.error = false $scope.$watch 'data.selectedProjectId', (newVal, oldVal) -> return if !newVal @@ -278,7 +253,8 @@ define [ _reset = (opts) -> isError = opts.err == true inFlight = $scope.state.inFlight - inFlight.projects = inFlight.entities = inFlight.compile = inFlight.create = false + inFlight.projects = inFlight.entities = inFlight.compile = false + $scope.state.inflight = false $scope.state.error = isError $scope.shouldEnableProjectSelect = () -> @@ -293,10 +269,11 @@ define [ { state, data } = $scope return !state.inFlight.projects && !state.inFlight.compile && data.projects && data.selectedProjectId - $scope.shouldEnableCreateButton = () -> + + validate = () -> state = $scope.state data = $scope.data - return !state.inFlight.projects && + $scope.state.valid = !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProjectId && @@ -313,6 +290,8 @@ define [ ) ) && data.name + $scope.$watch 'state', validate, true + $scope.$watch 'data', validate, true $scope.getUserProjects = () -> _setInFlight('projects') @@ -362,7 +341,7 @@ define [ $scope.getUserProjects() $timeout($scope.init, 0) - $scope.create = () -> + $scope.$on 'create', () -> projectId = $scope.data.selectedProjectId name = $scope.data.name if isOutputFilesMode @@ -379,34 +358,39 @@ define [ } _setInFlight('create') ide.fileTreeManager - .createLinkedFile(name, parent_folder, provider, payload) + .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 @@ -415,7 +399,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 @@ -423,15 +407,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/stylesheets/app/editor/file-tree.less b/services/web/public/stylesheets/app/editor/file-tree.less index 4e26e4751d..2cf73115d7 100644 --- a/services/web/public/stylesheets/app/editor/file-tree.less +++ b/services/web/public/stylesheets/app/editor/file-tree.less @@ -261,3 +261,40 @@ } } } + +.modal-new-file { + padding: 0; + table { + width: 100%; + td { + vertical-align: top; + } + } +} + .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; From 64ec90f34f76f608fce6e6c0a9c922dbf4491f98 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 11 Jun 2018 10:29:40 +0100 Subject: [PATCH 10/27] Move the isOutputFilesMode flag onto the scope --- services/web/app/views/project/editor/new-file-modal.pug | 4 ++-- .../ide/file-tree/controllers/FileTreeController.coffee | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/project/editor/new-file-modal.pug b/services/web/app/views/project/editor/new-file-modal.pug index 48c8863e5b..021f822eab 100644 --- a/services/web/app/views/project/editor/new-file-modal.pug +++ b/services/web/app/views/project/editor/new-file-modal.pug @@ -95,7 +95,7 @@ script(type='text/ng-template', id='newFileModalTemplate') value="{{ project._id }}" ) {{ project.name }} - .form-controls.row-spaced-small(ng-if="!isOutputFilesMode") + .form-controls.row-spaced-small(ng-if="!state.isOutputFilesMode") label(for="project-entity-select") Select a File span(ng-show="state.inFlight.entities") |   @@ -111,7 +111,7 @@ script(type='text/ng-template', id='newFileModalTemplate') value="{{ projectEntity.path }}" ) {{ projectEntity.path.slice(1) }} - .form-controls.row-spaced-small(ng-if="isOutputFilesMode") + .form-controls.row-spaced-small(ng-if="state.isOutputFilesMode") label(for="project-entity-select") Select an Output File span(ng-show="state.inFlight.compile") |   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 6d92675b4e..fd7b297de3 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -223,12 +223,13 @@ define [ 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 - if isOutputFilesMode + if $scope.state.isOutputFilesMode $scope.compileProjectAndGetOutputFiles($scope.data.selectedProjectId) else $scope.getProjectEntities($scope.data.selectedProjectId) @@ -279,12 +280,12 @@ define [ data.selectedProjectId && ( ( - !isOutputFilesMode && + !$scope.state.isOutputFilesMode && data.projectEntities && data.selectedProjectEntity ) || ( - isOutputFilesMode && + $scope.state.isOutputFilesMode && data.projectOutputFiles && data.selectedProjectOutputFile ) @@ -344,7 +345,7 @@ define [ $scope.$on 'create', () -> projectId = $scope.data.selectedProjectId name = $scope.data.name - if isOutputFilesMode + if $scope.state.isOutputFilesMode provider = 'project_output_file' payload = { source_project_id: projectId, From 6672a20c2bc1bb1985a7906be210fac736b09956 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 11 Jun 2018 11:27:57 +0100 Subject: [PATCH 11/27] Enable switching between source and output files --- .../web/app/views/project/editor/new-file-modal.pug | 7 +++++++ .../file-tree/controllers/FileTreeController.coffee | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/services/web/app/views/project/editor/new-file-modal.pug b/services/web/app/views/project/editor/new-file-modal.pug index 021f822eab..2b139a9d06 100644 --- a/services/web/app/views/project/editor/new-file-modal.pug +++ b/services/web/app/views/project/editor/new-file-modal.pug @@ -126,6 +126,13 @@ script(type='text/ng-template', id='newFileModalTemplate') ng-repeat="outputFile in data.projectOutputFiles" value="{{ outputFile.path }}" ) {{ outputFile.path }} + div + a( + href="#" + ng-click="toggleOutputFilesMode()" + ) + span(ng-show="state.isOutputFilesMode") Switch to source files + span(ng-show="!state.isOutputFilesMode") Switch to output files .form-controls.row-spaced-small label(for="name") File Name In This Project 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 fd7b297de3..da9c9d37e0 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -229,11 +229,20 @@ define [ $scope.$watch 'data.selectedProjectId', (newVal, oldVal) -> return if !newVal $scope.data.selectedProjectEntity = null + $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) -> return if !newVal @@ -258,6 +267,10 @@ define [ $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 From 7fc99a38f8a98b2a27292b728bf078065c261859 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 11 Jun 2018 11:53:30 +0100 Subject: [PATCH 12/27] Better styling on the output-files toggle --- services/web/app/views/project/editor/new-file-modal.pug | 7 ++++--- services/web/public/stylesheets/app/editor/file-tree.less | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/services/web/app/views/project/editor/new-file-modal.pug b/services/web/app/views/project/editor/new-file-modal.pug index 2b139a9d06..3dcd55bfb9 100644 --- a/services/web/app/views/project/editor/new-file-modal.pug +++ b/services/web/app/views/project/editor/new-file-modal.pug @@ -126,13 +126,14 @@ script(type='text/ng-template', id='newFileModalTemplate') ng-repeat="outputFile in data.projectOutputFiles" value="{{ outputFile.path }}" ) {{ outputFile.path }} - div + div.toggle-output-files-button + | or  a( href="#" ng-click="toggleOutputFilesMode()" ) - span(ng-show="state.isOutputFilesMode") Switch to source files - span(ng-show="!state.isOutputFilesMode") Switch to output files + 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 diff --git a/services/web/public/stylesheets/app/editor/file-tree.less b/services/web/public/stylesheets/app/editor/file-tree.less index 2cf73115d7..845cc74a93 100644 --- a/services/web/public/stylesheets/app/editor/file-tree.less +++ b/services/web/public/stylesheets/app/editor/file-tree.less @@ -270,6 +270,9 @@ vertical-align: top; } } + .toggle-output-files-button { + font-size: 80%; + } } .modal-new-file--list { background-color: @modal-footer-background-color; From 2cfc2b473803d46bffc0d939b7cfd61a08df4a73 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 13 Jun 2018 14:20:10 +0100 Subject: [PATCH 13/27] Only show output files which are images or pdfs --- .../ide/file-tree/controllers/FileTreeController.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 da9c9d37e0..71f3956118 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -342,7 +342,9 @@ define [ }) .then (resp) -> if resp.data.status == 'success' - $scope.data.projectOutputFiles = resp.data.outputFiles + filteredFiles = resp.data.outputFiles.filter (f) -> + f.path.match(/.*\.(pdf|png|jpeg|jpg|gif)/) + $scope.data.projectOutputFiles = filteredFiles _reset(err: false) else $scope.data.projectOutputFiles = null From 48a4f6c4c4aa75be0d01f4d505153ffc85142bcd Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 13 Jun 2018 14:49:16 +0100 Subject: [PATCH 14/27] Fix error handling for failing to get output file --- .../coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee index 8af8b5bf63..fff464945a 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee @@ -88,8 +88,8 @@ module.exports = ProjectOutputFileAgent = { handleError: (error, req, res, next) -> if error instanceof BadDataError res.status(400).send("The submitted data is not valid") - else if error instanceof SourceFileNotFoundError - res.status(404).send("Source file not found") + else if error instanceof OutputFileFetchFailedError + res.status(404).send("Could not get output file") else if error instanceof ProjectNotFoundError res.status(404).send("Project not found") else From 28257462aefe767f83838716284a95cf03ada47e Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 14 Jun 2018 10:03:29 +0100 Subject: [PATCH 15/27] Acceptance tests for project-output-file --- services/web/docker-compose.yml | 2 +- .../acceptance/coffee/LinkedFilesTests.coffee | 68 +++++++++++++++++++ .../coffee/helpers/MockClsiApi.coffee | 9 +++ 3 files changed, 78 insertions(+), 1 deletion(-) 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/test/acceptance/coffee/LinkedFilesTests.coffee b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee index 875e887d81..af7573a151 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() @@ -344,3 +346,69 @@ 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 output.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: "output.pdf", + }, (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: "output.pdf", + source_project_display_name: "output-test-two" + } + 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() diff --git a/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee b/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee index 2f9180960d..5b01b7a81b 100644 --- a/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee @@ -1,4 +1,5 @@ express = require("express") +bodyParser = require "body-parser" app = express() module.exports = MockClsiApi = @@ -30,6 +31,14 @@ module.exports = MockClsiApi = else res.sendStatus(404) + app.post "/project/:project_id/compile", (req, res, next) => + res.json { + outputFiles: [{path: 'output.pdf'}] + } + + app.get "/project/:project_id/output/:output_path", (req, res, next) => + res.status(200).send("hello") + app.listen 3013, (error) -> throw error if error? .on "error", (error) -> From a313184c712758df73871dad21506a6e6491fa30 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 14 Jun 2018 12:27:20 +0100 Subject: [PATCH 16/27] Handle linked-output-files from v1 imports --- .../LinkedFiles/ProjectFileAgent.coffee | 6 +- .../LinkedFiles/ProjectOutputFileAgent.coffee | 68 +++++++++++-------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee index 9ba8b5f1e7..78fc31ee03 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -60,6 +60,10 @@ SourceFileNotFoundError.prototype.__proto__ = Error.prototype module.exports = ProjectFileAgent = + V1ProjectNotFoundError: V1ProjectNotFoundError + + _v1ProjectNotFoundMessage: "Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file" + sanitizeData: (data) -> return _.pick( data, @@ -155,7 +159,7 @@ module.exports = ProjectFileAgent = 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") + res.status(409).send(ProjectFileAgent._v1ProjectNotFoundMessage) else next(error) next() diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee index fff464945a..5d81420cd8 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee @@ -6,6 +6,7 @@ Settings = require 'settings-sharelatex' CompileManager = require '../Compile/CompileManager' CompileController = require '../Compile/CompileController' ClsiCookieManager = require '../Compile/ClsiCookieManager' +ProjectFileAgent = require './ProjectFileAgent' _ = require "underscore" request = require "request" @@ -42,48 +43,53 @@ module.exports = ProjectOutputFileAgent = { source_output_file_path: data.source_output_file_path } - canCreate: (data) -> true + canCreate: ProjectFileAgent.canCreate - decorateLinkedFileData: (data, callback = (err, newData) ->) -> - callback = _.once(callback) - ProjectGetter.getProject data.source_project_id, {name: 1}, (err, project) -> - return callback(err) if err? - if !project? - return callback(new ProjectNotFoundError()) - callback(err, _.extend(data, {source_project_display_name: project.name})) + _getSourceProject: ProjectFileAgent._getSourceProject + + decorateLinkedFileData: ProjectFileAgent.decorateLinkedFileData + + _validate: (data) -> + return ( + (data.source_project_id? || data.v1_source_doc_id?) && + data.source_output_file_path? + ) checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) -> callback = _.once(callback) - { source_project_id } = data - AuthorizationManager.canUserReadProject current_user_id, source_project_id, null, (err, canRead) -> + if !ProjectOutputFileAgent._validate(data) + return callback(new BadDataError()) + @_getSourceProject data, (err, project) -> return callback(err) if err? - callback(null, canRead) - - _validate: (data) -> - data.source_project_id? && data.source_output_file_path? + 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 !ProjectOutputFileAgent._validate(data) return callback(new BadDataError()) - { source_project_id, source_output_file_path } = data - CompileManager.compile source_project_id, null, {}, (err) -> + { source_output_file_path } = data + @_getSourceProject data, (err, project) -> return callback(err) if err? - url = "#{Settings.apis.clsi.url}/project/#{source_project_id}/output/#{source_output_file_path}" - ClsiCookieManager.getCookieJar source_project_id, (err, jar)-> + source_project_id = project._id + CompileManager.compile source_project_id, null, {}, (err) -> return callback(err) if err? - oneMinute = 60 * 1000 - # the base request - options = { url: url, method: "GET", timeout: oneMinute, jar : jar } - readStream = request(options) - readStream.on "error", callback - readStream.on "response", (response) -> - if 200 <= response.statusCode < 300 - FileWriter.writeStreamToDisk project_id, readStream, callback - else - error = new OutputFileFetchFailedError("Output file fetch failed: #{url}") - error.statusCode = response.statusCode - callback(error) + url = "#{Settings.apis.clsi.url}/project/#{source_project_id}/output/#{source_output_file_path}" + ClsiCookieManager.getCookieJar source_project_id, (err, jar)-> + return callback(err) if err? + oneMinute = 60 * 1000 + # the base request + options = { url: url, method: "GET", timeout: oneMinute, jar : jar } + readStream = request(options) + readStream.on "error", callback + readStream.on "response", (response) -> + if 200 <= response.statusCode < 300 + FileWriter.writeStreamToDisk project_id, readStream, callback + else + error = new OutputFileFetchFailedError("Output file fetch failed: #{url}") + error.statusCode = response.statusCode + callback(error) handleError: (error, req, res, next) -> if error instanceof BadDataError @@ -92,6 +98,8 @@ module.exports = ProjectOutputFileAgent = { res.status(404).send("Could not get output file") else if error instanceof ProjectNotFoundError res.status(404).send("Project not found") + else if error instanceof ProjectFileAgent.V1ProjectNotFoundError + res.status(404).send(ProjectFileAgent._v1ProjectNotFoundMessage) else next(error) } From 2ade78783bf14b7477592f2c1c2d587989187dee Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 14 Jun 2018 14:25:44 +0100 Subject: [PATCH 17/27] Add acceptance test for refreshing output file from v1 project --- .../LinkedFiles/ProjectOutputFileAgent.coffee | 2 +- .../acceptance/coffee/LinkedFilesTests.coffee | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee index 5d81420cd8..5969b25c5b 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee @@ -99,7 +99,7 @@ module.exports = ProjectOutputFileAgent = { else if error instanceof ProjectNotFoundError res.status(404).send("Project not found") else if error instanceof ProjectFileAgent.V1ProjectNotFoundError - res.status(404).send(ProjectFileAgent._v1ProjectNotFoundMessage) + res.status(409).send(ProjectFileAgent._v1ProjectNotFoundMessage) else next(error) } diff --git a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee index af7573a151..173aefa154 100644 --- a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee +++ b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee @@ -412,3 +412,37 @@ describe "LinkedFiles", -> 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: "output.pdf" + }, + _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() From 708e809df677aae91b1e0858564cfe22354d491b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 15 Jun 2018 11:17:20 +0100 Subject: [PATCH 18/27] Use errors from ProjectFileAgent --- .../LinkedFiles/ProjectFileAgent.coffee | 8 ++++++-- .../LinkedFiles/ProjectOutputFileAgent.coffee | 20 ++----------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee index 78fc31ee03..1b9f964733 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -58,9 +58,12 @@ SourceFileNotFoundError = (message) -> SourceFileNotFoundError.prototype.__proto__ = Error.prototype -module.exports = ProjectFileAgent = +module.exports = ProjectFileAgent = { - V1ProjectNotFoundError: V1ProjectNotFoundError + V1ProjectNotFoundError + BadDataError + ProjectNotFoundError + V1ProjectNotFoundError _v1ProjectNotFoundMessage: "Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file" @@ -163,3 +166,4 @@ module.exports = ProjectFileAgent = else next(error) next() +} diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee index 5969b25c5b..d194032273 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee @@ -11,22 +11,6 @@ _ = require "underscore" request = require "request" -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 - - OutputFileFetchFailedError = (message) -> error = new Error(message) error.name = 'OutputFileFetchFailedError' @@ -92,11 +76,11 @@ module.exports = ProjectOutputFileAgent = { callback(error) handleError: (error, req, res, next) -> - if error instanceof BadDataError + if error instanceof ProjectFileAgent.BadDataError res.status(400).send("The submitted data is not valid") else if error instanceof OutputFileFetchFailedError res.status(404).send("Could not get output file") - else if error instanceof ProjectNotFoundError + else if error instanceof ProjectFileAgent.ProjectNotFoundError res.status(404).send("Project not found") else if error instanceof ProjectFileAgent.V1ProjectNotFoundError res.status(409).send(ProjectFileAgent._v1ProjectNotFoundMessage) From 67dcbff450c608f0473205c7910f2683952692ef Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 15 Jun 2018 15:34:37 +0100 Subject: [PATCH 19/27] Remove linked-files items from the left-menu test controls --- services/web/app/views/project/editor/left-menu.pug | 12 ------------ .../controllers/TestControlsController.coffee | 9 --------- 2 files changed, 21 deletions(-) diff --git a/services/web/app/views/project/editor/left-menu.pug b/services/web/app/views/project/editor/left-menu.pug index c1ff891844..2cedc0b37e 100644 --- a/services/web/app/views/project/editor/left-menu.pug +++ b/services/web/app/views/project/editor/left-menu.pug @@ -69,18 +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="openProjectOutputLinkedFileModal()") - i.fa.fa-exclamation.fa-fw - | Project-Output-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/public/coffee/ide/test-controls/controllers/TestControlsController.coffee b/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee index 0dca64dc5b..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,15 +4,6 @@ define [ ], (App) -> App.controller "TestControlsController", ($scope) -> - $scope.openProjectLinkedFileModal = () -> - window.openProjectLinkedFileModal() - - $scope.openProjectOutputLinkedFileModal = () -> - window.openProjectOutputLinkedFileModal() - - $scope.openLinkedFileModal = () -> - window.openLinkedFileModal() - $scope.richText = () -> current = window.location.toString() target = "#{current}#{if window.location.search then '&' else '?'}rt=true" From 54cdbd738cbc2f37ee2521732f3aeddb119b17e9 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 15 Jun 2018 16:04:42 +0100 Subject: [PATCH 20/27] If selecting 'output.pdf', set the filename to project-name.pdf --- .../file-tree/controllers/FileTreeController.coffee | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 71f3956118..cd3c35b866 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -253,9 +253,13 @@ define [ # auto-set filename based on selected file $scope.$watch 'data.selectedProjectOutputFile', (newVal, oldVal) -> return if !newVal - fileName = newVal.split('/').reverse()[0] - if fileName - $scope.data.name = fileName + 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 From d93eb448e3d987d2783db98b0c1c5892764802cd Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 15 Jun 2018 16:20:55 +0100 Subject: [PATCH 21/27] Move Clsi logic to ClsiManager --- .../app/coffee/Features/Compile/ClsiManager.coffee | 8 ++++++++ .../LinkedFiles/ProjectOutputFileAgent.coffee | 11 ++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index e5158cdb9b..232b416e66 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, output_file_path, callback=(err, readStream)->) -> + url = "#{Settings.apis.clsi.url}/project/#{project_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/ProjectOutputFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee index d194032273..0c84d3f7dc 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee @@ -4,8 +4,8 @@ ProjectGetter = require('../Project/ProjectGetter') FileWriter = require('../../infrastructure/FileWriter') Settings = require 'settings-sharelatex' CompileManager = require '../Compile/CompileManager' -CompileController = require '../Compile/CompileController' ClsiCookieManager = require '../Compile/ClsiCookieManager' +ClsiManager = require '../Compile/ClsiManager' ProjectFileAgent = require './ProjectFileAgent' _ = require "underscore" request = require "request" @@ -59,16 +59,13 @@ module.exports = ProjectOutputFileAgent = { source_project_id = project._id CompileManager.compile source_project_id, null, {}, (err) -> return callback(err) if err? - url = "#{Settings.apis.clsi.url}/project/#{source_project_id}/output/#{source_output_file_path}" - ClsiCookieManager.getCookieJar source_project_id, (err, jar)-> + ClsiManager.getOutputFileStream source_project_id, source_output_file_path, (err, readStream) -> return callback(err) if err? - oneMinute = 60 * 1000 - # the base request - options = { url: url, method: "GET", timeout: oneMinute, jar : jar } - readStream = request(options) + readStream.pause() readStream.on "error", callback readStream.on "response", (response) -> if 200 <= response.statusCode < 300 + readStream.resume() FileWriter.writeStreamToDisk project_id, readStream, callback else error = new OutputFileFetchFailedError("Output file fetch failed: #{url}") From 6058f3ef9bb5f7e5d9b1b4f6c1a438d0d459a8b6 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 20 Jun 2018 10:01:03 +0100 Subject: [PATCH 22/27] Refactor the LinkedFiles/Agent system, and track build_id for output files --- .../Features/Compile/ClsiManager.coffee | 4 +- .../LinkedFiles/LinkedFilesController.coffee | 60 ++---- .../LinkedFiles/LinkedFilesErrors.coffee | 122 +++++++++++ .../LinkedFiles/LinkedFilesHandler.coffee | 53 +++++ .../LinkedFiles/ProjectFileAgent.coffee | 191 ++++++++---------- .../LinkedFiles/ProjectOutputFileAgent.coffee | 173 +++++++++++----- .../Features/LinkedFiles/UrlAgent.coffee | 80 +++----- .../coffee/infrastructure/FileWriter.coffee | 5 +- .../controllers/FileTreeController.coffee | 6 +- .../acceptance/coffee/LinkedFilesTests.coffee | 4 +- .../coffee/helpers/MockClsiApi.coffee | 18 +- 11 files changed, 462 insertions(+), 254 deletions(-) create mode 100644 services/web/app/coffee/Features/LinkedFiles/LinkedFilesErrors.coffee create mode 100644 services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 232b416e66..d1dfe21dad 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -182,8 +182,8 @@ module.exports = ClsiManager = return callback(error) if error? callback(null, projectStateHash, docs) - getOutputFileStream: (project_id, output_file_path, callback=(err, readStream)->) -> - url = "#{Settings.apis.clsi.url}/project/#{project_id}/output/#{output_file_path}" + 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 } diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee index 00c061778c..2d42c2e19c 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee @@ -4,8 +4,11 @@ ProjectLocator = require '../Project/ProjectLocator' Settings = require 'settings-sharelatex' logger = require 'logger-sharelatex' _ = require 'underscore' +LinkedFilesErrors = require './LinkedFilesErrors' + module.exports = LinkedFilesController = { + Agents: { url: require('./UrlAgent'), project_file: require('./ProjectFileAgent'), @@ -38,16 +41,16 @@ 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 LinkedFilesErrors.handleError(err, req, res, next) if err? + res.json(new_file_id: newFileId) refreshLinkedFile: (req, res, next) -> {project_id, file_id} = req.params @@ -66,37 +69,14 @@ 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 - ) - _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 + Agent.refreshLinkedFile project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + (err, newFileId) -> + return LinkedFilesErrors.handleError(err, req, res, next) if err? + res.json(new_file_id: newFileId) } 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..69c66e11ea --- /dev/null +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesErrors.coffee @@ -0,0 +1,122 @@ +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, + + 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." + ) + + 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/LinkedFilesHandler.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee new file mode 100644 index 0000000000..4eb3391035 --- /dev/null +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee @@ -0,0 +1,53 @@ +LinkedFilesErrors = require './LinkedFilesErrors' +FileWriter = require '../../infrastructure/FileWriter' +EditorController = require '../Editor/EditorController' +_ = require 'underscore' + + +module.exports = LinkedFilesHandler = + + 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 1b9f964733..dab91a5162 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -1,75 +1,97 @@ -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' - - -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 - +LinkedFilesHandler = require './LinkedFilesHandler' +{ + BadDataError, + AccessDeniedError, + BadEntityTypeError, + SourceFileNotFoundError, + ProjectNotFoundError, + V1ProjectNotFoundError +} = require './LinkedFilesErrors' module.exports = ProjectFileAgent = { - V1ProjectNotFoundError - BadDataError - ProjectNotFoundError - V1ProjectNotFoundError + 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) - _v1ProjectNotFoundMessage: "Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file" + refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> + @_go project_id, linkedFileData, name, parent_folder_id, user_id, callback - sanitizeData: (data) -> + _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 + @_decorateLinkedFileData linkedFileData, (err, newLinkedFileData) => + return callback(err) if err? + if !@_validate(newLinkedFileData) + return callback(new BadDataError()) + callback(null, newLinkedFileData) + + _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()) + + _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) + + _sanitizeData: (data) -> return _.pick( data, + 'provider', 'source_project_id', 'v1_source_doc_id', 'source_entity_path' @@ -81,7 +103,7 @@ 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? @@ -102,13 +124,13 @@ module.exports = ProjectFileAgent = { else callback(new BadDataError('neither v1 nor v2 id present')) - decorateLinkedFileData: (data, callback = (err, newData) ->) -> + _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()) @@ -117,53 +139,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(ProjectFileAgent._v1ProjectNotFoundMessage) - else - next(error) - next() } diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee index 0c84d3f7dc..aff876f864 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee @@ -1,86 +1,157 @@ -FileWriter = require('../../infrastructure/FileWriter') AuthorizationManager = require('../Authorization/AuthorizationManager') ProjectGetter = require('../Project/ProjectGetter') -FileWriter = require('../../infrastructure/FileWriter') Settings = require 'settings-sharelatex' CompileManager = require '../Compile/CompileManager' -ClsiCookieManager = require '../Compile/ClsiCookieManager' ClsiManager = require '../Compile/ClsiManager' ProjectFileAgent = require './ProjectFileAgent' _ = require "underscore" -request = require "request" - - -OutputFileFetchFailedError = (message) -> - error = new Error(message) - error.name = 'OutputFileFetchFailedError' - error.__proto__ = OutputFileFetchFailedError.prototype - return error -OutputFileFetchFailedError.prototype.__proto__ = Error.prototype +LinkedFilesErrors = require './LinkedFilesErrors' +LinkedFilesHandler = require './LinkedFilesHandler' +logger = require 'logger-sharelatex' module.exports = ProjectOutputFileAgent = { - sanitizeData: (data) -> + _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 LinkedFilesErrors.AccessDeniedError()) if !allowed + @_decorateLinkedFileData linkedFileData, (err, newLinkedFileData) => + return callback(err) if err? + if !@_validate(newLinkedFileData) + return callback(new BadDataError()) + callback(null, newLinkedFileData) + + createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> + if !@_canCreate(linkedFileData) + return callback(new LinkedFilesErrors.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 LinkedFilesErrors.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 LinkedFilesErrors.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 + source_output_file_path: data.source_output_file_path, + build_id: data.build_id } - canCreate: ProjectFileAgent.canCreate + _canCreate: ProjectFileAgent._canCreate _getSourceProject: ProjectFileAgent._getSourceProject - decorateLinkedFileData: ProjectFileAgent.decorateLinkedFileData + _decorateLinkedFileData: ProjectFileAgent._decorateLinkedFileData _validate: (data) -> return ( (data.source_project_id? || data.v1_source_doc_id?) && - data.source_output_file_path? + data.source_output_file_path? && + data.build_id? ) - checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) -> + _checkAuth: (project_id, data, current_user_id, callback = (err, allowed)->) -> callback = _.once(callback) - if !ProjectOutputFileAgent._validate(data) - return callback(new BadDataError()) + if !@_validate(data) + return callback(new LinkedFilesErrors.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) + 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) ->) -> + _getFileStream: (linkedFileData, user_id, callback=(err, fileStream)->) -> callback = _.once(callback) - if !ProjectOutputFileAgent._validate(data) - return callback(new BadDataError()) - { source_output_file_path } = data - @_getSourceProject data, (err, project) -> + { source_output_file_path, build_id } = linkedFileData + @_getSourceProject linkedFileData, (err, project) -> return callback(err) if err? source_project_id = project._id - CompileManager.compile source_project_id, null, {}, (err) -> - return callback(err) if err? - ClsiManager.getOutputFileStream source_project_id, source_output_file_path, (err, readStream) -> + ClsiManager.getOutputFileStream source_project_id, + user_id, + build_id, + source_output_file_path, + (err, readStream) -> return callback(err) if err? readStream.pause() - readStream.on "error", callback - readStream.on "response", (response) -> - if 200 <= response.statusCode < 300 - readStream.resume() - FileWriter.writeStreamToDisk project_id, readStream, callback - else - error = new OutputFileFetchFailedError("Output file fetch failed: #{url}") - error.statusCode = response.statusCode - callback(error) + callback(null, readStream) - handleError: (error, req, res, next) -> - if error instanceof ProjectFileAgent.BadDataError - res.status(400).send("The submitted data is not valid") - else if error instanceof OutputFileFetchFailedError - res.status(404).send("Could not get output file") - else if error instanceof ProjectFileAgent.ProjectNotFoundError - res.status(404).send("Project not found") - else if error instanceof ProjectFileAgent.V1ProjectNotFoundError - res.status(409).send(ProjectFileAgent._v1ProjectNotFoundMessage) - else - next(error) + _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 LinkedFilesErrors.OutputFileFetchFailedError()) + outputFile = _.find( + outputFiles, + (o) => o.path == source_output_file_path + ) + if !outputFile? + return callback(new LinkedFilesErrors.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/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/public/coffee/ide/file-tree/controllers/FileTreeController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee index cd3c35b866..8573b3742d 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -218,6 +218,7 @@ define [ projectOutputFiles: null # or [] selectedProjectEntity: null selectedProjectOutputFile: null + buildId: null name: null $scope.state.inFlight = projects: false @@ -349,6 +350,8 @@ define [ 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 @@ -368,7 +371,8 @@ define [ provider = 'project_output_file' payload = { source_project_id: projectId, - source_output_file_path: $scope.data.selectedProjectOutputFile + source_output_file_path: $scope.data.selectedProjectOutputFile, + build_id: $scope.data.buildId } else provider = 'project_file' diff --git a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee index 173aefa154..c892848e08 100644 --- a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee +++ b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee @@ -151,7 +151,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", -> @@ -380,6 +380,7 @@ describe "LinkedFiles", -> data: source_project_id: @project_two_id, source_output_file_path: "output.pdf", + build_id: '1234-abcd' }, (error, response, body) => new_file_id = body.new_file_id @existing_file_id = new_file_id @@ -393,6 +394,7 @@ describe "LinkedFiles", -> source_project_id: @project_two_id, source_output_file_path: "output.pdf", source_project_display_name: "output-test-two" + build_id: '1234-abcd' } expect(firstFile.name).to.equal('test.pdf') done() diff --git a/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee b/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee index 5b01b7a81b..60424af2a8 100644 --- a/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee @@ -33,12 +33,26 @@ module.exports = MockClsiApi = app.post "/project/:project_id/compile", (req, res, next) => res.json { - outputFiles: [{path: 'output.pdf'}] + compile: + status: 'success' + outputFiles: [{path: 'output.pdf', build: 'abcd', url: 'http://example.com'}] + } + app.post "/project/:project_id/user/:user_id/compile", (req, res, next) => + res.json { + compile: + status: 'success' + outputFiles: [{path: 'output.pdf', build: 'abcd', url: 'http://example.com'}] } - app.get "/project/:project_id/output/:output_path", (req, res, next) => + app.get "/project/:project_id/status", (req, res, next) => + res.status(200).send() + + app.get "/project/:project_id/user/:user_id/build/:build_id/output/:output_path", (req, res, next) => res.status(200).send("hello") + app.all "*", (req, res, next) => + next() + app.listen 3013, (error) -> throw error if error? .on "error", (error) -> From c8012f296889c331586f4117295787f7b1cedf60 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 21 Jun 2018 11:40:16 +0100 Subject: [PATCH 23/27] Cleaner import of errors --- .../LinkedFiles/ProjectOutputFileAgent.coffee | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee index aff876f864..53d7652142 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee @@ -5,7 +5,12 @@ CompileManager = require '../Compile/CompileManager' ClsiManager = require '../Compile/ClsiManager' ProjectFileAgent = require './ProjectFileAgent' _ = require "underscore" -LinkedFilesErrors = require './LinkedFilesErrors' +{ + BadDataError, + AccessDeniedError, + BadEntityTypeError, + OutputFileFetchFailedError +} = require './LinkedFilesErrors' LinkedFilesHandler = require './LinkedFilesHandler' logger = require 'logger-sharelatex' @@ -15,7 +20,7 @@ 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 LinkedFilesErrors.AccessDeniedError()) if !allowed + return callback(new AccessDeniedError()) if !allowed @_decorateLinkedFileData linkedFileData, (err, newLinkedFileData) => return callback(err) if err? if !@_validate(newLinkedFileData) @@ -24,7 +29,7 @@ module.exports = ProjectOutputFileAgent = { createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> if !@_canCreate(linkedFileData) - return callback(new LinkedFilesErrors.AccessDeniedError()) + return callback(new AccessDeniedError()) linkedFileData = @_sanitizeData(linkedFileData) @_prepare project_id, linkedFileData, user_id, (err, linkedFileData) => return callback(err) if err? @@ -44,7 +49,7 @@ module.exports = ProjectOutputFileAgent = { return callback(err) if err? callback(null, file._id) # Created else - err = new LinkedFilesErrors.OutputFileFetchFailedError( + err = new OutputFileFetchFailedError( "Output file fetch failed: #{linkedFileData.build_id}, #{linkedFileData.source_output_file_path}" ) err.statusCode = response.statusCode @@ -70,7 +75,7 @@ module.exports = ProjectOutputFileAgent = { return callback(err) if err? callback(null, file._id) # Created else - err = new LinkedFilesErrors.OutputFileFetchFailedError( + err = new OutputFileFetchFailedError( "Output file fetch failed: #{linkedFileData.build_id}, #{linkedFileData.source_output_file_path}" ) err.statusCode = response.statusCode @@ -101,7 +106,7 @@ module.exports = ProjectOutputFileAgent = { _checkAuth: (project_id, data, current_user_id, callback = (err, allowed)->) -> callback = _.once(callback) if !@_validate(data) - return callback(new LinkedFilesErrors.BadDataError()) + return callback(new BadDataError()) @_getSourceProject data, (err, project) -> return callback(err) if err? AuthorizationManager.canUserReadProject current_user_id, @@ -138,13 +143,13 @@ module.exports = ProjectOutputFileAgent = { (err, status, outputFiles) -> return callback(err) if err? if status != 'success' - return callback(new LinkedFilesErrors.OutputFileFetchFailedError()) + return callback(new OutputFileFetchFailedError()) outputFile = _.find( outputFiles, (o) => o.path == source_output_file_path ) if !outputFile? - return callback(new LinkedFilesErrors.OutputFileFetchFailedError()) + return callback(new OutputFileFetchFailedError()) build_id = outputFile.build ClsiManager.getOutputFileStream source_project_id, user_id, From dfb4898be52d030b32b88db5216bf46dfeaa2c00 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 21 Jun 2018 11:40:36 +0100 Subject: [PATCH 24/27] Move `getFileById` to the `LinkedFilesHandler` module --- .../Features/LinkedFiles/LinkedFilesController.coffee | 11 +---------- .../Features/LinkedFiles/LinkedFilesHandler.coffee | 10 ++++++++++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee index 2d42c2e19c..ba624e7814 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee @@ -22,15 +22,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 @@ -57,7 +48,7 @@ module.exports = LinkedFilesController = { 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 diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee index 4eb3391035..76693cb525 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee @@ -1,11 +1,21 @@ LinkedFilesErrors = require './LinkedFilesErrors' FileWriter = require '../../infrastructure/FileWriter' EditorController = require '../Editor/EditorController' +ProjectLocator = require '../Project/ProjectLocator' _ = require 'underscore' 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) + importFromStream: ( project_id, readStream, From ebe828aa625c69a563542cde436928c29d5bd64d Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 21 Jun 2018 15:27:32 +0100 Subject: [PATCH 25/27] Refactor, and remove the `source_project_display_name` prop from linkedFileData --- .../LinkedFiles/LinkedFilesController.coffee | 1 + .../LinkedFiles/LinkedFilesHandler.coffee | 25 ++++++++++++++- .../LinkedFiles/ProjectFileAgent.coffee | 32 +++---------------- .../LinkedFiles/ProjectOutputFileAgent.coffee | 12 +++---- .../app/views/project/editor/binary-file.pug | 8 ++--- .../acceptance/coffee/LinkedFilesTests.coffee | 5 ++- 6 files changed, 39 insertions(+), 44 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee index ba624e7814..9e70f6e200 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee @@ -5,6 +5,7 @@ Settings = require 'settings-sharelatex' logger = require 'logger-sharelatex' _ = require 'underscore' LinkedFilesErrors = require './LinkedFilesErrors' +LinkedFilesHandler = require './LinkedFilesHandler' module.exports = LinkedFilesController = { diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee index 76693cb525..6262f0a5ab 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesHandler.coffee @@ -1,8 +1,14 @@ -LinkedFilesErrors = require './LinkedFilesErrors' 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 = @@ -16,6 +22,23 @@ module.exports = LinkedFilesHandler = 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, diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee index dab91a5162..77196ef1d4 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -1,7 +1,6 @@ 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') _ = require "underscore" @@ -30,11 +29,9 @@ module.exports = ProjectFileAgent = { @_checkAuth project_id, linkedFileData, user_id, (err, allowed) => return callback(err) if err? return callback(new AccessDeniedError()) if !allowed - @_decorateLinkedFileData linkedFileData, (err, newLinkedFileData) => - return callback(err) if err? - if !@_validate(newLinkedFileData) - return callback(new BadDataError()) - callback(null, newLinkedFileData) + if !@_validate(linkedFileData) + return callback(new BadDataError()) + callback(null, linkedFileData) _go: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> linkedFileData = @_sanitizeData(linkedFileData) @@ -107,28 +104,7 @@ module.exports = ProjectFileAgent = { # 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')) - - _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})) + _getSourceProject: LinkedFilesHandler.getSourceProject _checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) -> callback = _.once(callback) diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee index 53d7652142..5be0866ee2 100644 --- a/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee @@ -21,11 +21,9 @@ module.exports = ProjectOutputFileAgent = { @_checkAuth project_id, linkedFileData, user_id, (err, allowed) => return callback(err) if err? return callback(new AccessDeniedError()) if !allowed - @_decorateLinkedFileData linkedFileData, (err, newLinkedFileData) => - return callback(err) if err? - if !@_validate(newLinkedFileData) - return callback(new BadDataError()) - callback(null, newLinkedFileData) + if !@_validate(linkedFileData) + return callback(new BadDataError()) + callback(null, linkedFileData) createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) -> if !@_canCreate(linkedFileData) @@ -92,9 +90,7 @@ module.exports = ProjectOutputFileAgent = { _canCreate: ProjectFileAgent._canCreate - _getSourceProject: ProjectFileAgent._getSourceProject - - _decorateLinkedFileData: ProjectFileAgent._decorateLinkedFileData + _getSourceProject: LinkedFilesHandler.getSourceProject _validate: (data) -> return ( diff --git a/services/web/app/views/project/editor/binary-file.pug b/services/web/app/views/project/editor/binary-file.pug index bd0e4b9732..e7cd1e32ed 100644 --- a/services/web/app/views/project/editor/binary-file.pug +++ b/services/web/app/views/project/editor/binary-file.pug @@ -56,9 +56,9 @@ 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 }} @@ -71,9 +71,9 @@ 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_output_file_path }}, | | at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }} diff --git a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee index c892848e08..72fbf5502e 100644 --- a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee +++ b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee @@ -118,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() @@ -393,7 +392,6 @@ describe "LinkedFiles", -> provider: 'project_output_file', source_project_id: @project_two_id, source_output_file_path: "output.pdf", - source_project_display_name: "output-test-two" build_id: '1234-abcd' } expect(firstFile.name).to.equal('test.pdf') @@ -430,7 +428,8 @@ describe "LinkedFiles", -> linkedFileData: { provider: "project_output_file", v1_source_doc_id: 9999999, # We won't find this id in the database - source_output_file_path: "output.pdf" + source_output_file_path: "output.pdf", + build_id: '123' }, _id: "abcdef", rev: 0, From 096d3f28a1402686d6f429d0591a0fee17baa34b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 22 Jun 2018 11:14:04 +0100 Subject: [PATCH 26/27] Move the `handleError` function into the `LinkedFilesController` --- .../LinkedFiles/LinkedFilesController.coffee | 56 +++++++++++++++++-- .../LinkedFiles/LinkedFilesErrors.coffee | 38 ------------- 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee index 9e70f6e200..34f77fde20 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee @@ -4,8 +4,19 @@ ProjectLocator = require '../Project/ProjectLocator' Settings = require 'settings-sharelatex' logger = require 'logger-sharelatex' _ = require 'underscore' -LinkedFilesErrors = require './LinkedFilesErrors' LinkedFilesHandler = require './LinkedFilesHandler' +{ + + UrlFetchFailedError, + InvalidUrlError, + OutputFileFetchFailedError, + AccessDeniedError, + BadEntityTypeError, + BadDataError, + ProjectNotFoundError, + V1ProjectNotFoundError, + SourceFileNotFoundError, +} = require './LinkedFilesErrors' module.exports = LinkedFilesController = { @@ -41,7 +52,7 @@ module.exports = LinkedFilesController = { parent_folder_id, user_id, (err, newFileId) -> - return LinkedFilesErrors.handleError(err, req, res, next) if err? + return LinkedFilesController.handleError(err, req, res, next) if err? res.json(new_file_id: newFileId) refreshLinkedFile: (req, res, next) -> @@ -68,7 +79,44 @@ module.exports = LinkedFilesController = { parent_folder_id, user_id, (err, newFileId) -> - return LinkedFilesErrors.handleError(err, req, res, next) if err? + 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." + ) + + 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 index 69c66e11ea..8cf671702e 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesErrors.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesErrors.coffee @@ -81,42 +81,4 @@ module.exports = { ProjectNotFoundError, V1ProjectNotFoundError, SourceFileNotFoundError, - - 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." - ) - - else if error instanceof InvalidUrlError - res.status(422).send( - "Your URL is not valid. Please check it and try again." - ) - - else - next(error) } From a8222f2e31fcebbbafa8ca3037507b4608639b9c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 22 Jun 2018 13:20:13 +0100 Subject: [PATCH 27/27] Fix tests after sync with master --- .../acceptance/coffee/LinkedFilesTests.coffee | 8 +++--- .../coffee/helpers/MockClsiApi.coffee | 26 +++++-------------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee index 72fbf5502e..0c369097cd 100644 --- a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee +++ b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee @@ -369,7 +369,7 @@ describe "LinkedFiles", -> cb(error) ], done - it 'should import the output.pdf file from the source project', (done) -> + it 'should import the project.pdf file from the source project', (done) -> @owner.request.post { url: "/project/#{@project_one_id}/linked_file", json: @@ -378,7 +378,7 @@ describe "LinkedFiles", -> provider: 'project_output_file', data: source_project_id: @project_two_id, - source_output_file_path: "output.pdf", + source_output_file_path: "project.pdf", build_id: '1234-abcd' }, (error, response, body) => new_file_id = body.new_file_id @@ -391,7 +391,7 @@ describe "LinkedFiles", -> expect(firstFile.linkedFileData).to.deep.equal { provider: 'project_output_file', source_project_id: @project_two_id, - source_output_file_path: "output.pdf", + source_output_file_path: "project.pdf", build_id: '1234-abcd' } expect(firstFile.name).to.equal('test.pdf') @@ -428,7 +428,7 @@ describe "LinkedFiles", -> linkedFileData: { provider: "project_output_file", v1_source_doc_id: 9999999, # We won't find this id in the database - source_output_file_path: "output.pdf", + source_output_file_path: "project.pdf", build_id: '123' }, _id: "abcdef", diff --git a/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee b/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee index 60424af2a8..0711ac613f 100644 --- a/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockClsiApi.coffee @@ -4,7 +4,8 @@ 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' @@ -22,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' @@ -31,27 +35,11 @@ module.exports = MockClsiApi = else res.sendStatus(404) - app.post "/project/:project_id/compile", (req, res, next) => - res.json { - compile: - status: 'success' - outputFiles: [{path: 'output.pdf', build: 'abcd', url: 'http://example.com'}] - } - app.post "/project/:project_id/user/:user_id/compile", (req, res, next) => - res.json { - compile: - status: 'success' - outputFiles: [{path: 'output.pdf', build: 'abcd', url: 'http://example.com'}] - } - - app.get "/project/:project_id/status", (req, res, next) => - res.status(200).send() - app.get "/project/:project_id/user/:user_id/build/:build_id/output/:output_path", (req, res, next) => res.status(200).send("hello") - app.all "*", (req, res, next) => - next() + app.get "/project/:project_id/status", (req, res, next) => + res.status(200).send() app.listen 3013, (error) -> throw error if error?