From beee86f1ce17946402aee1b518e2498281a92aa7 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 8 Mar 2018 17:24:54 +0000 Subject: [PATCH] First pass at restore end point --- .../Features/History/HistoryController.coffee | 9 ++++ .../Features/History/RestoreManager.coffee | 29 +++++++++++ .../coffee/infrastructure/FileWriter.coffee | 16 +++++- services/web/app/coffee/router.coffee | 1 + .../web/app/views/project/editor/history.pug | 14 +++-- .../coffee/RestoringFilesTest.coffee | 51 +++++++++++++++++++ .../helpers/MockProjectHistoryApi.coffee | 14 +++++ 7 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 services/web/app/coffee/Features/History/RestoreManager.coffee create mode 100644 services/web/test/acceptance/coffee/RestoringFilesTest.coffee diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee index e90aec9479..9563db2a24 100644 --- a/services/web/app/coffee/Features/History/HistoryController.coffee +++ b/services/web/app/coffee/Features/History/HistoryController.coffee @@ -6,6 +6,7 @@ Errors = require "../Errors/Errors" HistoryManager = require "./HistoryManager" ProjectDetailsHandler = require "../Project/ProjectDetailsHandler" ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler" +RestoreManager = require "./RestoreManager" module.exports = HistoryController = selectHistoryApi: (req, res, next = (error) ->) -> @@ -71,3 +72,11 @@ module.exports = HistoryController = return res.sendStatus(404) if error instanceof Errors.ProjectHistoryDisabledError return next(error) if error? res.sendStatus 204 + + restoreFile: (req, res, next) -> + {project_id} = req.params + {version, pathname} = req.body + user_id = AuthenticationController.getLoggedInUserId req + RestoreManager.restoreFile user_id, project_id, version, pathname, (error) -> + return next(error) if error? + res.send 204 diff --git a/services/web/app/coffee/Features/History/RestoreManager.coffee b/services/web/app/coffee/Features/History/RestoreManager.coffee new file mode 100644 index 0000000000..0eccb7e390 --- /dev/null +++ b/services/web/app/coffee/Features/History/RestoreManager.coffee @@ -0,0 +1,29 @@ +Settings = require 'settings-sharelatex' +Path = require 'path' +FileWriter = require '../../infrastructure/FileWriter' +FileSystemImportManager = require '../Uploads/FileSystemImportManager' +ProjectLocator = require '../Project/ProjectLocator' + +module.exports = RestoreManager = + restoreFile: (user_id, project_id, version, pathname, callback = (error) ->) -> + RestoreManager._writeFileVersionToDisk project_id, version, pathname, (error, fsPath) -> + return callback(error) if error? + basename = Path.basename(pathname) + dirname = Path.dirname(pathname) + if dirname == '.' # no directory + dirname = '' + ProjectLocator.findElementByPath {project_id, path: dirname}, (error, element, type) -> + return callback(error) if error? + # We're going to try to recover the file into the folder it was in previously, + # but this is historical, so the folder may not exist anymore. Fallback to the + # root folder if not (parent_folder_id == null will default to this) + if type == 'folder' and element? + parent_folder_id = element._id + else + parent_folder_id = null + # TODO if we get a name conflict error from here, then retry with a timestamp appended + FileSystemImportManager.addEntity user_id, project_id, parent_folder_id, basename, fsPath, false, callback + + _writeFileVersionToDisk: (project_id, version, pathname, callback = (error, fsPath) ->) -> + url = "#{Settings.apis.project_history.url}/project/#{project_id}/version/#{version}/#{pathname}" + FileWriter.writeUrlToDisk project_id, url, callback \ No newline at end of file diff --git a/services/web/app/coffee/infrastructure/FileWriter.coffee b/services/web/app/coffee/infrastructure/FileWriter.coffee index b19dc83336..1e91f47ebb 100644 --- a/services/web/app/coffee/infrastructure/FileWriter.coffee +++ b/services/web/app/coffee/infrastructure/FileWriter.coffee @@ -3,8 +3,9 @@ logger = require 'logger-sharelatex' uuid = require 'uuid' _ = require 'underscore' Settings = require 'settings-sharelatex' +request = require 'request' -module.exports = +module.exports = FileWriter = writeStreamToDisk: (identifier, stream, callback = (error, fsPath) ->) -> callback = _.once(callback) fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}" @@ -20,4 +21,15 @@ module.exports = callback(err) writeStream.on "finish", -> logger.log {identifier, fsPath}, "[writeStreamToDisk] write stream finished" - callback null, fsPath \ No newline at end of file + callback null, fsPath + + writeUrlToDisk: (identifier, url, callback = (error, fsPath) ->) -> + callback = _.once(callback) + stream = request.get(url) + stream.on 'response', (response) -> + if 200 <= response.statusCode < 300 + FileWriter.writeStreamToDisk identifier, stream, callback + else + err = new Error("bad response from url: #{response.statusCode}") + logger.err {err, identifier, url}, err.message + callback(err) \ No newline at end of file diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 949967c7c3..b051f69292 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -201,6 +201,7 @@ module.exports = class Router webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi webRouter.get "/project/:Project_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi + webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFile privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug index 7b4436af5d..6558aabbbc 100644 --- a/services/web/app/views/project/editor/history.pug +++ b/services/web/app/views/project/editor/history.pug @@ -138,15 +138,19 @@ div#history(ng-show="ui.view == 'history'") }" ) | in {{history.diff.pathname}} - .toolbar-right + .toolbar-right(ng-if="!history.isV2") a.btn.btn-danger.btn-sm( href, - ng-if="!history.isV2" ng-click="openRestoreDiffModal()" ) #{translate("restore_to_before_these_changes")} - .deleted-warning( - ng-show="history.selection.docs[history.selection.pathname].deleted" - ) This file was deleted + .toolbar-right(ng-if="history.isV2") + button.btn.btn-danger.btn-sm( + href, + ng-if="history.selection.docs[history.selection.pathname].deleted" + ng-click="" + ) + i.fa.fa-fw.fa-step-backward + | Restore this deleted file .diff-editor.hide-ace-cursor( ace-editor="history", theme="settings.theme", diff --git a/services/web/test/acceptance/coffee/RestoringFilesTest.coffee b/services/web/test/acceptance/coffee/RestoringFilesTest.coffee new file mode 100644 index 0000000000..8225e7ce47 --- /dev/null +++ b/services/web/test/acceptance/coffee/RestoringFilesTest.coffee @@ -0,0 +1,51 @@ +async = require "async" +expect = require("chai").expect + +ProjectGetter = require "../../../app/js/Features/Project/ProjectGetter.js" + +User = require "./helpers/User" +MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi" +MockDocstoreApi = require "./helpers/MockDocstoreApi" + +describe "RestoringFiles", -> + before (done) -> + @owner = new User() + @owner.login (error) => + throw error if error? + @owner.createProject "example-project", {template: "example"}, (error, @project_id) => + throw error if error? + done() + + describe "restoring a text file", -> + beforeEach (done) -> + MockProjectHistoryApi.addOldFile(@project_id, 42, "foo.tex", "hello world, this is foo.tex!") + @owner.request { + method: "POST", + url: "/project/#{@project_id}/restore_file", + json: + pathname: "foo.tex" + version: 42 + }, (error, response, body) -> + throw error if error? + expect(response.statusCode).to.equal 204 + done() + + it "should have created a doc", -> + @owner.getProject @project_id, (error, project) => + throw error if error? + doc = _.find project.rootFolder[0].docs, (doc) -> + doc.name == 'foo.tex' + doc = MockDocstoreApi.docs[@project_id][doc._id] + expect(doc.lines).to.deep.equal [ + "hello world, this is foo.tex!" + ] + done() + + describe "restoring a binary file", -> + it "should have created a file" + + describe "restoring to a directory that no longer exists", -> + it "should have created the file in the root folder" + + describe "restoring to a filename that already exists", -> + it "should have created the file with a timestamp appended" diff --git a/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee b/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee index 9027e22468..0bf4c15887 100644 --- a/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockProjectHistoryApi.coffee @@ -4,10 +4,24 @@ app = express() module.exports = MockProjectHistoryApi = docs: {} + oldFiles: {} + + addOldFile: (project_id, version, pathname, content) -> + @oldFiles["#{project_id}:#{version}:#{pathname}"] = content + run: () -> app.post "/project", (req, res, next) => res.json project: id: 1 + app.get "/project/:project_id/version/:version/:pathname", (req, res, next) => + {project_id, version, pathname} = req.params + key = "#{project_id}:#{version}:#{pathname}" + console.log key, @oldFiles, @oldFiles[key] + if @oldFiles[key]? + res.send @oldFiles[key] + else + res.send 404 + app.listen 3054, (error) -> throw error if error? .on "error", (error) ->