DocumentUpdaterHandler.coffee 12 KB

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