HistoryController.coffee 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. _ = require "lodash"
  2. async = require "async"
  3. logger = require "logger-sharelatex"
  4. request = require "request"
  5. settings = require "settings-sharelatex"
  6. AuthenticationController = require "../Authentication/AuthenticationController"
  7. Errors = require "../Errors/Errors"
  8. HistoryManager = require "./HistoryManager"
  9. ProjectDetailsHandler = require "../Project/ProjectDetailsHandler"
  10. ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler"
  11. RestoreManager = require "./RestoreManager"
  12. module.exports = HistoryController =
  13. selectHistoryApi: (req, res, next = (error) ->) ->
  14. project_id = req.params?.Project_id
  15. # find out which type of history service this project uses
  16. ProjectDetailsHandler.getDetails project_id, (err, project) ->
  17. return next(err) if err?
  18. history = project.overleaf?.history
  19. if history?.id? and history?.display
  20. req.useProjectHistory = true
  21. else
  22. req.useProjectHistory = false
  23. next()
  24. ensureProjectHistoryEnabled: (req, res, next = (error) ->) ->
  25. if req.useProjectHistory?
  26. next()
  27. else
  28. logger.log {project_id}, "project history not enabled"
  29. res.sendStatus(404)
  30. proxyToHistoryApi: (req, res, next = (error) ->) ->
  31. user_id = AuthenticationController.getLoggedInUserId req
  32. url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url
  33. logger.log url: url, "proxying to history api"
  34. getReq = request(
  35. url: url
  36. method: req.method
  37. headers:
  38. "X-User-Id": user_id
  39. )
  40. getReq.pipe(res)
  41. getReq.on "error", (error) ->
  42. logger.error url: url, err: error, "history API error"
  43. next(error)
  44. proxyToHistoryApiAndInjectUserDetails: (req, res, next = (error) ->) ->
  45. user_id = AuthenticationController.getLoggedInUserId req
  46. url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url
  47. logger.log url: url, "proxying to history api"
  48. HistoryController._makeRequest {
  49. url: url
  50. method: req.method
  51. json: true
  52. headers:
  53. "X-User-Id": user_id
  54. }, (error, body) ->
  55. return next(error) if error?
  56. HistoryManager.injectUserDetails body, (error, data) ->
  57. return next(error) if error?
  58. res.json data
  59. buildHistoryServiceUrl: (useProjectHistory) ->
  60. # choose a history service, either document-level (trackchanges)
  61. # or project-level (project_history)
  62. if useProjectHistory
  63. return settings.apis.project_history.url
  64. else
  65. return settings.apis.trackchanges.url
  66. resyncProjectHistory: (req, res, next = (error) ->) ->
  67. project_id = req.params.Project_id
  68. ProjectEntityUpdateHandler.resyncProjectHistory project_id, (error) ->
  69. return res.sendStatus(404) if error instanceof Errors.ProjectHistoryDisabledError
  70. return next(error) if error?
  71. res.sendStatus 204
  72. restoreFileFromV2: (req, res, next) ->
  73. {project_id} = req.params
  74. {version, pathname} = req.body
  75. user_id = AuthenticationController.getLoggedInUserId req
  76. logger.log {project_id, version, pathname}, "restoring file from v2"
  77. RestoreManager.restoreFileFromV2 user_id, project_id, version, pathname, (error, entity) ->
  78. return next(error) if error?
  79. res.json {
  80. type: entity.type,
  81. id: entity._id
  82. }
  83. restoreDocFromDeletedDoc: (req, res, next) ->
  84. {project_id, doc_id} = req.params
  85. {name} = req.body
  86. user_id = AuthenticationController.getLoggedInUserId(req)
  87. if !name?
  88. return res.sendStatus 400 # Malformed request
  89. logger.log {project_id, doc_id, user_id}, "restoring doc from v1 deleted doc"
  90. RestoreManager.restoreDocFromDeletedDoc user_id, project_id, doc_id, name, (err, doc) =>
  91. return next(error) if error?
  92. res.json {
  93. doc_id: doc._id
  94. }
  95. getLabels: (req, res, next) ->
  96. project_id = req.params.Project_id
  97. user_id = AuthenticationController.getLoggedInUserId(req)
  98. HistoryController._makeRequest {
  99. method: "GET"
  100. url: "#{settings.apis.project_history.url}/project/#{project_id}/labels"
  101. json: true
  102. }, (error, labels) ->
  103. return next(error) if error?
  104. res.json labels
  105. createLabel: (req, res, next) ->
  106. project_id = req.params.Project_id
  107. {comment, version} = req.body
  108. user_id = AuthenticationController.getLoggedInUserId(req)
  109. HistoryController._makeRequest {
  110. method: "POST"
  111. url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels"
  112. json: {comment, version}
  113. }, (error, label) ->
  114. return next(error) if error?
  115. res.json label
  116. deleteLabel: (req, res, next) ->
  117. project_id = req.params.Project_id
  118. label_id = req.params.label_id
  119. user_id = AuthenticationController.getLoggedInUserId(req)
  120. HistoryController._makeRequest {
  121. method: "DELETE"
  122. url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels/#{label_id}"
  123. }, (error) ->
  124. return next(error) if error?
  125. res.sendStatus 204
  126. _makeRequest: (options, callback) ->
  127. request options, (error, response, body) ->
  128. return callback(error) if error?
  129. if 200 <= response.statusCode < 300
  130. callback(null, body)
  131. else
  132. error = new Error("history api responded with non-success code: #{response.statusCode}")
  133. logger.error err: error, "project-history api responded with non-success code: #{response.statusCode}"
  134. callback(error)
  135. downloadZipOfVersion: (req, res, next) ->
  136. {project_id, version} = req.params
  137. logger.log {project_id, version}, "got request for zip file at version"
  138. ProjectDetailsHandler.getDetails project_id, (err, project) ->
  139. return next(err) if err?
  140. v1_id = project.overleaf?.history?.id
  141. if !v1_id?
  142. logger.err {project_id, version}, 'got request for zip version of non-v1 history project'
  143. return res.sendStatus(402)
  144. HistoryController._pipeHistoryZipToResponse v1_id, version, "#{project.name} (Version #{version})", res, next
  145. _pipeHistoryZipToResponse: (v1_project_id, version, name, res, next) ->
  146. # increase timeout to 6 minutes
  147. res.setTimeout(6 * 60 * 1000)
  148. url = "#{settings.apis.v1_history.url}/projects/#{v1_project_id}/version/#{version}/zip"
  149. logger.log {v1_project_id, version, url}, "getting s3 url from history api"
  150. options =
  151. auth:
  152. user: settings.apis.v1_history.user
  153. pass: settings.apis.v1_history.pass
  154. json: true
  155. method: 'post'
  156. url: url
  157. request options, (err, response, body) ->
  158. if err
  159. logger.error {err, v1_project_id, version}, "history API error"
  160. return next(err)
  161. retryAttempt = 0
  162. retryDelay = 2000
  163. # retry for about 6 minutes starting with short delay
  164. async.retry 40,
  165. (callback) ->
  166. setTimeout(() ->
  167. # increase delay by 1 second up to 10
  168. retryDelay += 1000 if retryDelay < 10000
  169. retryAttempt++
  170. getReq = request(
  171. url: body.zipUrl
  172. sendImmediately: true
  173. )
  174. getReq.on 'response', (response) ->
  175. return callback new Error "invalid response" unless response.statusCode == 200
  176. # pipe also proxies the headers, but we want to customize these ones
  177. delete response.headers['content-disposition']
  178. delete response.headers['content-type']
  179. res.status response.statusCode
  180. res.setContentDisposition(
  181. 'attachment',
  182. {filename: "#{name}.zip"}
  183. )
  184. res.contentType('application/zip')
  185. getReq.pipe(res)
  186. callback()
  187. getReq.on "error", (err) ->
  188. logger.error {err, v1_project_id, version, retryAttempt}, "history s3 download error"
  189. callback(err)
  190. , retryDelay)
  191. (err) ->
  192. if err
  193. logger.error {err, v1_project_id, version, retryAttempt}, "history s3 download failed"
  194. next(err)