Преглед изворни кода

Merge pull request #1217 from sharelatex/ja-download-zip-version

Add option to download zip file of version in history

GitOrigin-RevId: 11ffb9a3305e1e5e0492fccf2be41be7beb67d68
James Allen пре 7 година
родитељ
комит
e603afe106

+ 34 - 0
services/web/app/coffee/Features/History/HistoryController.coffee

@@ -143,3 +143,37 @@ module.exports = HistoryController =
 				error = new Error("history api responded with non-success code: #{response.statusCode}")
 				error = new Error("history api responded with non-success code: #{response.statusCode}")
 				logger.error err: error, "project-history api responded with non-success code: #{response.statusCode}"
 				logger.error err: error, "project-history api responded with non-success code: #{response.statusCode}"
 				callback(error)
 				callback(error)
+
+	downloadZipOfVersion: (req, res, next) ->
+		{project_id, version} = req.params
+		logger.log {project_id, version}, "got request for zip file at version"
+		ProjectDetailsHandler.getDetails project_id, (err, project) ->
+			return next(err) if err?
+			v1_id = project.overleaf?.history?.id
+			if !v1_id?
+				logger.err {project_id, version}, 'got request for zip version of non-v1 history project'
+				return res.sendStatus(402)
+
+			url = "#{settings.apis.v1_history.url}/projects/#{v1_id}/version/#{version}/zip"
+			logger.log {project_id, v1_id, version, url}, "proxying to history api"
+			getReq = request(
+				url: url
+				auth:
+					user: settings.apis.v1_history.user
+					pass: settings.apis.v1_history.pass
+					sendImmediately: true
+			)
+			getReq.on 'response', (response) ->
+				# pipe also proxies the headers, but we want to customize these ones
+				delete response.headers['content-disposition']
+				delete response.headers['content-type']
+				res.status response.statusCode
+				res.setContentDisposition(
+					'attachment',
+					{filename: "#{project.name} (Version #{version}).zip"}
+				)
+				res.contentType('application/zip')
+				getReq.pipe(res)
+			getReq.on "error", (err) ->
+				logger.error {err, project_id, v1_id, version}, "history API error"
+				next(error)

+ 1 - 0
services/web/app/coffee/router.coffee

@@ -242,6 +242,7 @@ module.exports = class Router
 		webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
 		webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
 		webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc
 		webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc
 		webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2
 		webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2
+		webRouter.get  "/project/:project_id/version/:version/zip", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.downloadZipOfVersion
 		privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory
 		privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory
 
 
 		webRouter.get "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.getLabels
 		webRouter.get "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.getLabels

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

@@ -30,12 +30,13 @@
 			i.fa.fa-exchange
 			i.fa.fa-exchange
 			|  #{translate("compare_to_another_version")}
 			|  #{translate("compare_to_another_version")}
 
 
-		button.history-toolbar-btn-danger.pull-right(
-			tooltip-html="'<p>This feature is still in progress. Please use copy and paste to restore your files.</p><p>More restore options are coming soon.</p>'"
-			tooltip-placement="bottom"
+		a.history-toolbar-btn-danger.pull-right(
+			ng-hide="history.loadingFileTree || history.selection.updates.length == 0"
+			ng-href="/project/{{ project_id }}/version/{{ history.selection.updates[0].toV }}/zip"
+			target="_blank"
 		) 
 		) 
-			i.fa.fa-undo
-			| &nbsp;#{translate("restore_to_before_these_changes")}
+			i.fa.fa-download
+			| &nbsp;#{translate("download_project_at_this_version")}
 
 
 	.history-toolbar-entries-list(
 	.history-toolbar-entries-list(
 		ng-if="!history.error"
 		ng-if="!history.error"

+ 2 - 0
services/web/config/settings.defaults.coffee

@@ -171,6 +171,8 @@ module.exports = settings =
 			pass: v1Api.pass
 			pass: v1Api.pass
 		v1_history:
 		v1_history:
 			url: "http://#{process.env['V1_HISTORY_HOST'] or "localhost"}:3100/api"
 			url: "http://#{process.env['V1_HISTORY_HOST'] or "localhost"}:3100/api"
+			user: 'staging'
+			pass: 'password'
 
 
 	templates:
 	templates:
 		user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
 		user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"

+ 0 - 1
services/web/public/stylesheets/app/editor/history-v2.less

@@ -44,7 +44,6 @@
 		}
 		}
 		.history-toolbar-btn-danger {
 		.history-toolbar-btn-danger {
 			.btn-danger;
 			.btn-danger;
-			opacity: 0.65;
 		}
 		}
 	.history-toolbar-entries-list {
 	.history-toolbar-entries-list {
 		flex: 0 0 @changesListWidth;
 		flex: 0 0 @changesListWidth;

+ 47 - 0
services/web/test/acceptance/coffee/HistoryTests.coffee

@@ -0,0 +1,47 @@
+{expect} = require 'chai'
+
+{db, ObjectId} = require("../../../app/js/infrastructure/mongojs")
+MockV1HistoryApi = require './helpers/MockV1HistoryApi'
+User = require './helpers/User'
+
+describe 'History', ->
+	beforeEach (done) ->
+		@owner = new User()
+		@owner.login done
+
+	describe 'zip download of version', ->
+		it 'should stream the zip file of a version', (done) ->
+			@owner.createProject 'example-project', (error, @project_id) =>
+				return done(error) if error?
+				@v1_history_id = 42
+				db.projects.update {
+					_id: ObjectId(@project_id)
+				}, {
+					$set: {
+						'overleaf.history.id': @v1_history_id
+					}
+				}, (error) =>
+					return done(error) if error?
+					@owner.request "/project/#{@project_id}/version/42/zip", (error, response, body) =>
+						return done(error) if error?
+						expect(response.statusCode).to.equal 200
+						expect(response.headers['content-type']).to.equal 'application/zip'
+						expect(response.headers['content-disposition']).to.equal 'attachment; filename="example-project%20(Version%2042).zip"'
+						expect(body).to.equal "Mock zip for #{@v1_history_id} at version 42"
+						done()
+
+		it 'should return 402 for non-v2-history project', (done) ->
+			@owner.createProject 'non-v2-project', (error, @project_id) =>
+				return done(error) if error?
+				db.projects.update {
+					_id: ObjectId(@project_id)
+				}, {
+					$unset: {
+						'overleaf.history.id': true
+					}
+				}, (error) =>
+					return done(error) if error?
+					@owner.request "/project/#{@project_id}/version/42/zip", (error, response, body) =>
+						return done(error) if error?
+						expect(response.statusCode).to.equal 402
+						done()

+ 20 - 0
services/web/test/acceptance/coffee/helpers/MockV1HistoryApi.coffee

@@ -0,0 +1,20 @@
+_ = require 'lodash'
+express = require 'express'
+bodyParser = require "body-parser"
+app = express()
+{ObjectId} = require 'mongojs'
+
+module.exports = MockV1HistoryApi =
+	run: () ->
+		app.get "/api/projects/:project_id/version/:version/zip", (req, res, next) =>
+			res.header('content-disposition', 'attachment; name=project.zip')
+			res.header('content-type', 'application/octet-stream')
+			res.send "Mock zip for #{req.params.project_id} at version #{req.params.version}"
+
+		app.listen 3100, (error) ->
+			throw error if error?
+		.on "error", (error) ->
+			console.error "error starting MockV1HistoryApi:", error.message
+			process.exit(1)
+
+MockV1HistoryApi.run()