DocumentUpdaterHandler.coffee 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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. timer = new metrics.Timer("flushing.mongo.project")
  13. url = "#{settings.apis.documentupdater.url}/project/#{project_id}/flush"
  14. request.post url, (error, res, body)->
  15. if error?
  16. logger.error err: error, project_id: project_id, "error flushing project from document updater"
  17. return callback(error)
  18. else if res.statusCode >= 200 and res.statusCode < 300
  19. logger.log project_id: project_id, "flushed project from document updater"
  20. return callback(null)
  21. else
  22. error = new Error("document updater returned a failure status code: #{res.statusCode}")
  23. logger.error err: error, project_id: project_id, "document updater returned failure status code: #{res.statusCode}"
  24. return callback(error)
  25. flushMultipleProjectsToMongo: (project_ids, callback = (error) ->) ->
  26. jobs = []
  27. for project_id in project_ids
  28. do (project_id) ->
  29. jobs.push (callback) ->
  30. DocumentUpdaterHandler.flushProjectToMongo project_id, callback
  31. async.series jobs, callback
  32. flushProjectToMongoAndDelete: (project_id, callback = ()->) ->
  33. logger.log project_id:project_id, "deleting project from document updater"
  34. timer = new metrics.Timer("delete.mongo.project")
  35. url = "#{settings.apis.documentupdater.url}/project/#{project_id}"
  36. request.del url, (error, res, body)->
  37. if error?
  38. logger.error err: error, project_id: project_id, "error deleting project from document updater"
  39. return callback(error)
  40. else if res.statusCode >= 200 and res.statusCode < 300
  41. logger.log project_id: project_id, "deleted project from document updater"
  42. return callback(null)
  43. else
  44. error = new Error("document updater returned a failure status code: #{res.statusCode}")
  45. logger.error err: error, project_id: project_id, "document updater returned failure status code: #{res.statusCode}"
  46. return callback(error)
  47. flushDocToMongo: (project_id, doc_id, callback = (error) ->) ->
  48. logger.log project_id:project_id, doc_id: doc_id, "flushing doc from document updater"
  49. timer = new metrics.Timer("flushing.mongo.doc")
  50. url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/flush"
  51. request.post url, (error, res, body)->
  52. if error?
  53. logger.error err: error, project_id: project_id, doc_id: doc_id, "error flushing doc from document updater"
  54. return callback(error)
  55. else if res.statusCode >= 200 and res.statusCode < 300
  56. logger.log project_id: project_id, doc_id: doc_id, "flushed doc from document updater"
  57. return callback(null)
  58. else
  59. error = new Error("document updater returned a failure status code: #{res.statusCode}")
  60. logger.error err: error, project_id: project_id, doc_id: doc_id, "document updater returned failure status code: #{res.statusCode}"
  61. return callback(error)
  62. deleteDoc : (project_id, doc_id, callback = ()->)->
  63. logger.log project_id:project_id, doc_id: doc_id, "deleting doc from document updater"
  64. timer = new metrics.Timer("delete.mongo.doc")
  65. url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}"
  66. request.del url, (error, res, body)->
  67. if error?
  68. logger.error err: error, project_id: project_id, doc_id: doc_id, "error deleting doc from document updater"
  69. return callback(error)
  70. else if res.statusCode >= 200 and res.statusCode < 300
  71. logger.log project_id: project_id, doc_id: doc_id, "deleted doc from document updater"
  72. return callback(null)
  73. else
  74. error = new Error("document updater returned a failure status code: #{res.statusCode}")
  75. logger.error err: error, project_id: project_id, doc_id: doc_id, "document updater returned failure status code: #{res.statusCode}"
  76. return callback(error)
  77. getDocument: (project_id, doc_id, fromVersion, callback = (error, doclines, version, ranges, ops) ->) ->
  78. timer = new metrics.Timer("get-document")
  79. url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}"
  80. logger.log project_id:project_id, doc_id: doc_id, "getting doc from document updater"
  81. request.get url, (error, res, body)->
  82. timer.done()
  83. if error?
  84. logger.error err:error, url:url, project_id:project_id, doc_id:doc_id, "error getting doc from doc updater"
  85. return callback(error)
  86. if res.statusCode >= 200 and res.statusCode < 300
  87. logger.log project_id:project_id, doc_id:doc_id, "got doc from document document updater"
  88. try
  89. body = JSON.parse(body)
  90. catch error
  91. return callback(error)
  92. callback null, body.lines, body.version, body.ranges, body.ops
  93. else
  94. logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
  95. callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
  96. setDocument : (project_id, doc_id, user_id, docLines, source, callback = (error) ->)->
  97. timer = new metrics.Timer("set-document")
  98. url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}"
  99. body =
  100. url: url
  101. json:
  102. lines: docLines
  103. source: source
  104. user_id: user_id
  105. logger.log project_id:project_id, doc_id: doc_id, source: source, user_id: user_id, "setting doc in document updater"
  106. request.post body, (error, res, body)->
  107. timer.done()
  108. if error?
  109. logger.error err:error, url:url, project_id:project_id, doc_id:doc_id, "error setting doc in doc updater"
  110. return callback(error)
  111. if res.statusCode >= 200 and res.statusCode < 300
  112. logger.log project_id: project_id, doc_id: doc_id, "set doc in document updater"
  113. return callback(null)
  114. else
  115. logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
  116. callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
  117. getProjectDocsIfMatch: (project_id, projectStateHash, callback = (error, docs) ->) ->
  118. # If the project state hasn't changed, we can get all the latest
  119. # docs from redis via the docupdater. Otherwise we will need to
  120. # fall back to getting them from mongo.
  121. timer = new metrics.Timer("get-project-docs")
  122. url = "#{settings.apis.documentupdater.url}/project/#{project_id}/get_and_flush_if_old?state=#{projectStateHash}"
  123. logger.log project_id:project_id, "getting project docs from document updater"
  124. request.post url, (error, res, body)->
  125. timer.done()
  126. if error?
  127. logger.error err:error, url:url, project_id:project_id, "error getting project docs from doc updater"
  128. return callback(error)
  129. if res.statusCode is 409 # HTTP response code "409 Conflict"
  130. # Docupdater has checked the projectStateHash and found that
  131. # it has changed. This means that the docs currently in redis
  132. # aren't the only change to the project and the full set of
  133. # docs/files should be retreived from docstore/filestore
  134. # instead.
  135. return callback()
  136. else if res.statusCode >= 200 and res.statusCode < 300
  137. logger.log project_id:project_id, "got project docs from document document updater"
  138. try
  139. docs = JSON.parse(body)
  140. catch error
  141. return callback(error)
  142. callback null, docs
  143. else
  144. logger.error project_id:project_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
  145. callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
  146. clearProjectState: (project_id, callback = (error) ->) ->
  147. timer = new metrics.Timer("clear-project-state")
  148. url = "#{settings.apis.documentupdater.url}/project/#{project_id}/clearState"
  149. logger.log project_id:project_id, "clearing project state from document updater"
  150. request.post url, (error, res)->
  151. timer.done()
  152. if error?
  153. logger.error err:error, url:url, project_id:project_id, "error clearing project state from doc updater"
  154. return callback(error)
  155. else if res.statusCode is 200
  156. logger.log project_id:project_id, "cleared project state from doc updater"
  157. callback()
  158. else
  159. logger.error project_id:project_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
  160. callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
  161. acceptChanges: (project_id, doc_id, change_ids = [], callback = (error) ->) ->
  162. timer = new metrics.Timer("accept-changes")
  163. reqSettings =
  164. url: "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/change/accept"
  165. json:
  166. change_ids: change_ids
  167. logger.log {project_id, doc_id }, "accepting #{ change_ids.length } changes"
  168. request.post reqSettings, (error, res, body)->
  169. timer.done()
  170. if error?
  171. logger.error {err:error, project_id, doc_id }, "error accepting #{ change_ids.length } changes in doc updater"
  172. return callback(error)
  173. if res.statusCode >= 200 and res.statusCode < 300
  174. logger.log {project_id, doc_id }, "accepted #{ change_ids.length } changes in document updater"
  175. return callback(null)
  176. else
  177. logger.error {project_id, doc_id }, "doc updater returned a non-success status code: #{res.statusCode}"
  178. callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
  179. deleteThread: (project_id, doc_id, thread_id, callback = (error) ->) ->
  180. timer = new metrics.Timer("delete-thread")
  181. url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/comment/#{thread_id}"
  182. logger.log {project_id, doc_id, thread_id}, "deleting comment range in document updater"
  183. request.del url, (error, res, body)->
  184. timer.done()
  185. if error?
  186. logger.error {err:error, project_id, doc_id, thread_id}, "error deleting comment range in doc updater"
  187. return callback(error)
  188. if res.statusCode >= 200 and res.statusCode < 300
  189. logger.log {project_id, doc_id, thread_id}, "deleted comment rangee in document updater"
  190. return callback(null)
  191. else
  192. logger.error {project_id, doc_id, thread_id}, "doc updater returned a non-success status code: #{res.statusCode}"
  193. callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
  194. updateProjectStructure : (project_id, userId, changes, callback = (error) ->)->
  195. return callback() if !settings.apis.project_history?.sendProjectStructureOps
  196. docUpdates = DocumentUpdaterHandler._getUpdates('doc', changes.oldDocs, changes.newDocs)
  197. fileUpdates = DocumentUpdaterHandler._getUpdates('file', changes.oldFiles, changes.newFiles)
  198. timer = new metrics.Timer("set-document")
  199. url = "#{settings.apis.documentupdater.url}/project/#{project_id}"
  200. body =
  201. url: url
  202. json: { docUpdates, fileUpdates, userId }
  203. return callback() if (docUpdates.length + fileUpdates.length) < 1
  204. request.post body, (error, res, body)->
  205. timer.done()
  206. if error?
  207. logger.error {error, url, project_id}, "error update project structure in doc updater"
  208. callback(error)
  209. else if res.statusCode >= 200 and res.statusCode < 300
  210. logger.log {project_id}, "updated project structure in doc updater"
  211. callback(null)
  212. else
  213. logger.error {project_id, url}, "doc updater returned a non-success status code: #{res.statusCode}"
  214. callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
  215. _getUpdates: (entityType, oldEntities, newEntities) ->
  216. oldEntities ||= []
  217. newEntities ||= []
  218. updates = []
  219. oldEntitiesHash = _.indexBy oldEntities, (entity) -> entity[entityType]._id.toString()
  220. newEntitiesHash = _.indexBy newEntities, (entity) -> entity[entityType]._id.toString()
  221. for id, newEntity of newEntitiesHash
  222. oldEntity = oldEntitiesHash[id]
  223. if !oldEntity?
  224. # entity added
  225. updates.push
  226. id: id
  227. pathname: newEntity.path
  228. docLines: newEntity.docLines
  229. url: newEntity.url
  230. else if newEntity.path != oldEntity.path
  231. # entity renamed
  232. updates.push
  233. id: id
  234. pathname: oldEntity.path
  235. newPathname: newEntity.path
  236. for id, oldEntity of oldEntitiesHash
  237. newEntity = newEntitiesHash[id]
  238. if !newEntity?
  239. # entity deleted
  240. updates.push
  241. id: id
  242. pathname: oldEntity.path
  243. newPathname: ''
  244. updates
  245. PENDINGUPDATESKEY = "PendingUpdates"
  246. DOCLINESKEY = "doclines"
  247. DOCIDSWITHPENDINGUPDATES = "DocsWithPendingUpdates"
  248. keys =
  249. pendingUpdates : (op) -> "#{PENDINGUPDATESKEY}:#{op.doc_id}"
  250. docsWithPendingUpdates: DOCIDSWITHPENDINGUPDATES
  251. docLines : (op) -> "#{DOCLINESKEY}:#{op.doc_id}"
  252. combineProjectIdAndDocId: (project_id, doc_id) -> "#{project_id}:#{doc_id}"