| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- _ = 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)
|