Просмотр исходного кода

First pass at restore end point

James Allen 8 лет назад
Родитель
Сommit
beee86f1ce

+ 9 - 0
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

+ 29 - 0
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

+ 14 - 2
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
+			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)

+ 1 - 0
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

+ 9 - 5
services/web/app/views/project/editor/history.pug

@@ -138,15 +138,19 @@ div#history(ng-show="ui.view == 'history'")
 						}"
 					)
 					|  in <strong>{{history.diff.pathname}}</strong>
-				.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",

+ 51 - 0
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"

+ 14 - 0
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) ->