DocumentUpdaterHandler.coffee 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. request = require 'request'
  2. request = request.defaults()
  3. settings = require 'settings-sharelatex'
  4. _ = require 'underscore'
  5. async = require 'async'
  6. logger = require('logger-sharelatex')
  7. metrics = require('metrics-sharelatex')
  8. Project = require("../../models/Project").Project
  9. module.exports = DocumentUpdaterHandler =
  10. flushProjectToMongo: (project_id, callback = (error) ->)->
  11. logger.log project_id:project_id, "flushing project from document updater"
  12. DocumentUpdaterHandler._makeRequest {
  13. path: "/project/#{project_id}/flush"
  14. method: "POST"
  15. }, project_id, "flushing.mongo.project", callback
  16. flushMultipleProjectsToMongo: (project_ids, callback = (error) ->) ->
  17. jobs = []
  18. for project_id in project_ids
  19. do (project_id) ->
  20. jobs.push (callback) ->
  21. DocumentUpdaterHandler.flushProjectToMongo project_id, callback
  22. async.series jobs, callback
  23. flushProjectToMongoAndDelete: (project_id, callback = ()->) ->
  24. timer = new metrics.Timer("delete.mongo.project")
  25. url = "#{settings.apis.documentupdater.url}"
  26. DocumentUpdaterHandler._makeRequest {
  27. path: "/project/#{project_id}"
  28. method: "DELETE"
  29. }, project_id, "flushing.mongo.project", callback
  30. flushDocToMongo: (project_id, doc_id, callback = (error) ->) ->
  31. logger.log project_id:project_id, doc_id: doc_id, "flushing doc from document updater"
  32. DocumentUpdaterHandler._makeRequest {
  33. path: "/project/#{project_id}/doc/#{doc_id}/flush"
  34. method: "POST"
  35. }, project_id, "flushing.mongo.doc", callback
  36. deleteDoc : (project_id, doc_id, callback = ()->)->
  37. logger.log project_id:project_id, doc_id: doc_id, "deleting doc from document updater"
  38. DocumentUpdaterHandler._makeRequest {
  39. path: "/project/#{project_id}/doc/#{doc_id}"
  40. method: "DELETE"
  41. }, project_id, "delete.mongo.doc", callback
  42. getDocument: (project_id, doc_id, fromVersion, callback = (error, doclines, version, ranges, ops) ->) ->
  43. logger.log project_id:project_id, doc_id: doc_id, "getting doc from document updater"
  44. DocumentUpdaterHandler._makeRequest {
  45. path: "/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}"
  46. json: true
  47. }, project_id, "get-document", (error, doc) ->
  48. return callback(error) if error?
  49. callback null, doc.lines, doc.version, doc.ranges, doc.ops
  50. setDocument : (project_id, doc_id, user_id, docLines, source, callback = (error) ->)->
  51. logger.log project_id:project_id, doc_id: doc_id, source: source, user_id: user_id, "setting doc in document updater"
  52. DocumentUpdaterHandler._makeRequest {
  53. path: "/project/#{project_id}/doc/#{doc_id}"
  54. method: "POST"
  55. json:
  56. lines: docLines
  57. source: source
  58. user_id: user_id
  59. }, project_id, "set-document", callback
  60. getProjectDocsIfMatch: (project_id, projectStateHash, callback = (error, docs) ->) ->
  61. # If the project state hasn't changed, we can get all the latest
  62. # docs from redis via the docupdater. Otherwise we will need to
  63. # fall back to getting them from mongo.
  64. timer = new metrics.Timer("get-project-docs")
  65. url = "#{settings.apis.documentupdater.url}/project/#{project_id}/get_and_flush_if_old?state=#{projectStateHash}"
  66. logger.log project_id:project_id, "getting project docs from document updater"
  67. request.post url, (error, res, body)->
  68. timer.done()
  69. if error?
  70. logger.error err:error, url:url, project_id:project_id, "error getting project docs from doc updater"
  71. return callback(error)
  72. if res.statusCode is 409 # HTTP response code "409 Conflict"
  73. # Docupdater has checked the projectStateHash and found that
  74. # it has changed. This means that the docs currently in redis
  75. # aren't the only change to the project and the full set of
  76. # docs/files should be retreived from docstore/filestore
  77. # instead.
  78. return callback()
  79. else if res.statusCode >= 200 and res.statusCode < 300
  80. logger.log project_id:project_id, "got project docs from document document updater"
  81. try
  82. docs = JSON.parse(body)
  83. catch error
  84. return callback(error)
  85. callback null, docs
  86. else
  87. logger.error project_id:project_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
  88. callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
  89. clearProjectState: (project_id, callback = (error) ->) ->
  90. logger.log project_id:project_id, "clearing project state from document updater"
  91. DocumentUpdaterHandler._makeRequest {
  92. path: "/project/#{project_id}/clearState"
  93. method: "POST"
  94. }, project_id, "clear-project-state", callback
  95. acceptChanges: (project_id, doc_id, change_ids = [], callback = (error) ->) ->
  96. logger.log {project_id, doc_id }, "accepting #{ change_ids.length } changes"
  97. DocumentUpdaterHandler._makeRequest {
  98. path: "/project/#{project_id}/doc/#{doc_id}/change/accept"
  99. json:
  100. change_ids: change_ids
  101. method: "POST"
  102. }, project_id, "accept-changes", callback
  103. deleteThread: (project_id, doc_id, thread_id, callback = (error) ->) ->
  104. timer = new metrics.Timer("delete-thread")
  105. logger.log {project_id, doc_id, thread_id}, "deleting comment range in document updater"
  106. DocumentUpdaterHandler._makeRequest {
  107. path: "/project/#{project_id}/doc/#{doc_id}/comment/#{thread_id}"
  108. method: "DELETE"
  109. }, project_id, "delete-thread", callback
  110. resyncProjectHistory: (project_id, projectHistoryId, docs, files, callback) ->
  111. logger.info {project_id, docs, files}, "resyncing project history in doc updater"
  112. DocumentUpdaterHandler._makeRequest {
  113. path: "/project/#{project_id}/history/resync"
  114. json: { docs, files, projectHistoryId }
  115. method: "POST"
  116. }, project_id, "resync-project-history", callback
  117. updateProjectStructure: (project_id, projectHistoryId, userId, changes, callback = (error) ->)->
  118. return callback() if !settings.apis.project_history?.sendProjectStructureOps
  119. Project.findOne {_id: project_id}, {version:true}, (err, currentProject) ->
  120. return callback(err) if err?
  121. return callback new Error("project not found") if !currentProject?
  122. docUpdates = DocumentUpdaterHandler._getUpdates('doc', changes.oldDocs, changes.newDocs)
  123. fileUpdates = DocumentUpdaterHandler._getUpdates('file', changes.oldFiles, changes.newFiles)
  124. return callback() if (docUpdates.length + fileUpdates.length) < 1
  125. logger.log {project_id}, "updating project structure in doc updater"
  126. DocumentUpdaterHandler._makeRequest {
  127. path: "/project/#{project_id}"
  128. json: {
  129. docUpdates,
  130. fileUpdates,
  131. userId,
  132. version: currentProject.version
  133. projectHistoryId
  134. }
  135. method: "POST"
  136. }, project_id, "update-project-structure", callback
  137. _makeRequest: (options, project_id, metricsKey, callback) ->
  138. timer = new metrics.Timer(metricsKey)
  139. request {
  140. url: "#{settings.apis.documentupdater.url}#{options.path}"
  141. json: options.json
  142. method: options.method || "GET"
  143. }, (error, res, body)->
  144. timer.done()
  145. if error?
  146. logger.error {error, project_id}, "error making request to document updater"
  147. callback error
  148. else if res.statusCode >= 200 and res.statusCode < 300
  149. callback null, body
  150. else
  151. error = new Error("document updater returned a failure status code: #{res.statusCode}")
  152. logger.error {error, project_id}, "document updater returned failure status code: #{res.statusCode}"
  153. callback error
  154. _getUpdates: (entityType, oldEntities, newEntities) ->
  155. oldEntities ||= []
  156. newEntities ||= []
  157. updates = []
  158. oldEntitiesHash = _.indexBy oldEntities, (entity) -> entity[entityType]._id.toString()
  159. newEntitiesHash = _.indexBy newEntities, (entity) -> entity[entityType]._id.toString()
  160. # Send deletes before adds (and renames) to keep a 1:1 mapping between
  161. # paths and ids
  162. #
  163. # When a file is replaced, we first delete the old file and then add the
  164. # new file. If the 'add' operation is sent to project history before the
  165. # 'delete' then we would have two files with the same path at that point
  166. # in time.
  167. for id, oldEntity of oldEntitiesHash
  168. newEntity = newEntitiesHash[id]
  169. if !newEntity?
  170. # entity deleted
  171. updates.push
  172. id: id
  173. pathname: oldEntity.path
  174. newPathname: ''
  175. for id, newEntity of newEntitiesHash
  176. oldEntity = oldEntitiesHash[id]
  177. if !oldEntity?
  178. # entity added
  179. updates.push
  180. id: id
  181. pathname: newEntity.path
  182. docLines: newEntity.docLines
  183. url: newEntity.url
  184. else if newEntity.path != oldEntity.path
  185. # entity renamed
  186. updates.push
  187. id: id
  188. pathname: oldEntity.path
  189. newPathname: newEntity.path
  190. updates
  191. PENDINGUPDATESKEY = "PendingUpdates"
  192. DOCLINESKEY = "doclines"
  193. DOCIDSWITHPENDINGUPDATES = "DocsWithPendingUpdates"
  194. keys =
  195. pendingUpdates : (op) -> "#{PENDINGUPDATESKEY}:#{op.doc_id}"
  196. docsWithPendingUpdates: DOCIDSWITHPENDINGUPDATES
  197. docLines : (op) -> "#{DOCLINESKEY}:#{op.doc_id}"
  198. combineProjectIdAndDocId: (project_id, doc_id) -> "#{project_id}:#{doc_id}"