First pass at restore end point

This commit is contained in:
James Allen 2018-03-08 17:24:54 +00:00
parent 8ff1492962
commit beee86f1ce
7 changed files with 127 additions and 7 deletions

View file

@ -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

View file

@ -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

View file

@ -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()}"
@ -21,3 +22,14 @@ module.exports =
writeStream.on "finish", ->
logger.log {identifier, fsPath}, "[writeStreamToDisk] write stream finished"
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)

View file

@ -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

View file

@ -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",

View file

@ -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"

View file

@ -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) ->