overleaf/services/web/app/coffee/Features/History/HistoryController.coffee
Ersun Warncke c303af5d4d Merge pull request #1354 from sharelatex/ew-async-zip-download
use async project history zip download api

GitOrigin-RevId: 847e22eb3bf89e2e9e3c86bcc9ae92f8c90b25b1
2019-01-07 17:38:33 +00:00

209 lines
7.3 KiB
CoffeeScript

_ = require "lodash"
async = require "async"
logger = require "logger-sharelatex"
request = require "request"
settings = require "settings-sharelatex"
AuthenticationController = require "../Authentication/AuthenticationController"
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) ->) ->
project_id = req.params?.Project_id
# find out which type of history service this project uses
ProjectDetailsHandler.getDetails project_id, (err, project) ->
return next(err) if err?
history = project.overleaf?.history
if history?.id? and history?.display
req.useProjectHistory = true
else
req.useProjectHistory = false
next()
ensureProjectHistoryEnabled: (req, res, next = (error) ->) ->
if req.useProjectHistory?
next()
else
logger.log {project_id}, "project history not enabled"
res.sendStatus(404)
proxyToHistoryApi: (req, res, next = (error) ->) ->
user_id = AuthenticationController.getLoggedInUserId req
url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url
logger.log url: url, "proxying to history api"
getReq = request(
url: url
method: req.method
headers:
"X-User-Id": user_id
)
getReq.pipe(res)
getReq.on "error", (error) ->
logger.error url: url, err: error, "history API error"
next(error)
proxyToHistoryApiAndInjectUserDetails: (req, res, next = (error) ->) ->
user_id = AuthenticationController.getLoggedInUserId req
url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url
logger.log url: url, "proxying to history api"
HistoryController._makeRequest {
url: url
method: req.method
json: true
headers:
"X-User-Id": user_id
}, (error, body) ->
return next(error) if error?
HistoryManager.injectUserDetails body, (error, data) ->
return next(error) if error?
res.json data
buildHistoryServiceUrl: (useProjectHistory) ->
# choose a history service, either document-level (trackchanges)
# or project-level (project_history)
if useProjectHistory
return settings.apis.project_history.url
else
return settings.apis.trackchanges.url
resyncProjectHistory: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
ProjectEntityUpdateHandler.resyncProjectHistory project_id, (error) ->
return res.sendStatus(404) if error instanceof Errors.ProjectHistoryDisabledError
return next(error) if error?
res.sendStatus 204
restoreFileFromV2: (req, res, next) ->
{project_id} = req.params
{version, pathname} = req.body
user_id = AuthenticationController.getLoggedInUserId req
logger.log {project_id, version, pathname}, "restoring file from v2"
RestoreManager.restoreFileFromV2 user_id, project_id, version, pathname, (error, entity) ->
return next(error) if error?
res.json {
type: entity.type,
id: entity._id
}
restoreDocFromDeletedDoc: (req, res, next) ->
{project_id, doc_id} = req.params
{name} = req.body
user_id = AuthenticationController.getLoggedInUserId(req)
if !name?
return res.sendStatus 400 # Malformed request
logger.log {project_id, doc_id, user_id}, "restoring doc from v1 deleted doc"
RestoreManager.restoreDocFromDeletedDoc user_id, project_id, doc_id, name, (err, doc) =>
return next(error) if error?
res.json {
doc_id: doc._id
}
getLabels: (req, res, next) ->
project_id = req.params.Project_id
user_id = AuthenticationController.getLoggedInUserId(req)
HistoryController._makeRequest {
method: "GET"
url: "#{settings.apis.project_history.url}/project/#{project_id}/labels"
json: true
}, (error, labels) ->
return next(error) if error?
res.json labels
createLabel: (req, res, next) ->
project_id = req.params.Project_id
{comment, version} = req.body
user_id = AuthenticationController.getLoggedInUserId(req)
HistoryController._makeRequest {
method: "POST"
url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels"
json: {comment, version}
}, (error, label) ->
return next(error) if error?
res.json label
deleteLabel: (req, res, next) ->
project_id = req.params.Project_id
label_id = req.params.label_id
user_id = AuthenticationController.getLoggedInUserId(req)
HistoryController._makeRequest {
method: "DELETE"
url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels/#{label_id}"
}, (error) ->
return next(error) if error?
res.sendStatus 204
_makeRequest: (options, callback) ->
request options, (error, response, body) ->
return callback(error) if error?
if 200 <= response.statusCode < 300
callback(null, body)
else
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}"
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)
HistoryController._pipeHistoryZipToResponse v1_id, version, "#{project.name} (Version #{version})", res, next
_pipeHistoryZipToResponse: (v1_project_id, version, name, res, next) ->
# increase timeout to 6 minutes
res.setTimeout(6 * 60 * 1000)
url = "#{settings.apis.v1_history.url}/projects/#{v1_project_id}/version/#{version}/zip"
logger.log {v1_project_id, version, url}, "getting s3 url from history api"
options =
auth:
user: settings.apis.v1_history.user
pass: settings.apis.v1_history.pass
json: true
method: 'post'
url: url
request options, (err, response, body) ->
if err
logger.error {err, v1_project_id, version}, "history API error"
return next(err)
retryAttempt = 0
retryDelay = 2000
# retry for about 6 minutes starting with short delay
async.retry 40,
(callback) ->
setTimeout(() ->
# increase delay by 1 second up to 10
retryDelay += 1000 if retryDelay < 10000
retryAttempt++
getReq = request(
url: body.zipUrl
sendImmediately: true
)
getReq.on 'response', (response) ->
return callback new Error "invalid response" unless response.statusCode == 200
# 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: "#{name}.zip"}
)
res.contentType('application/zip')
getReq.pipe(res)
callback()
getReq.on "error", (err) ->
logger.error {err, v1_project_id, version, retryAttempt}, "history s3 download error"
callback(err)
, retryDelay)
(err) ->
if err
logger.error {err, v1_project_id, version, retryAttempt}, "history s3 download failed"
next(err)