TrackChangesClient.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. no-unused-vars,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS101: Remove unnecessary use of Array.from
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS207: Consider shorter variations of null checks
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. let TrackChangesClient
  16. const async = require('async')
  17. const zlib = require('zlib')
  18. const request = require('request')
  19. const Settings = require('@overleaf/settings')
  20. const rclient = require('@overleaf/redis-wrapper').createClient(
  21. Settings.redis.history
  22. ) // Only works locally for now
  23. const Keys = Settings.redis.history.key_schema
  24. const { db, ObjectId } = require('../../../../app/js/mongodb')
  25. const aws = require('aws-sdk')
  26. const s3 = new aws.S3({
  27. accessKeyId: Settings.trackchanges.s3.key,
  28. secretAccessKey: Settings.trackchanges.s3.secret,
  29. endpoint: Settings.trackchanges.s3.endpoint,
  30. s3ForcePathStyle: Settings.trackchanges.s3.pathStyle,
  31. })
  32. const S3_BUCKET = Settings.trackchanges.stores.doc_history
  33. module.exports = TrackChangesClient = {
  34. flushAndGetCompressedUpdates(project_id, doc_id, callback) {
  35. if (callback == null) {
  36. callback = function (error, updates) {}
  37. }
  38. return TrackChangesClient.flushDoc(project_id, doc_id, error => {
  39. if (error != null) {
  40. return callback(error)
  41. }
  42. return TrackChangesClient.getCompressedUpdates(doc_id, callback)
  43. })
  44. },
  45. flushDoc(project_id, doc_id, callback) {
  46. if (callback == null) {
  47. callback = function (error) {}
  48. }
  49. return request.post(
  50. {
  51. url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/flush`,
  52. },
  53. (error, response, body) => {
  54. response.statusCode.should.equal(204)
  55. return callback(error)
  56. }
  57. )
  58. },
  59. flushProject(project_id, callback) {
  60. if (callback == null) {
  61. callback = function (error) {}
  62. }
  63. return request.post(
  64. {
  65. url: `http://localhost:3015/project/${project_id}/flush`,
  66. },
  67. (error, response, body) => {
  68. response.statusCode.should.equal(204)
  69. return callback(error)
  70. }
  71. )
  72. },
  73. getCompressedUpdates(doc_id, callback) {
  74. if (callback == null) {
  75. callback = function (error, updates) {}
  76. }
  77. return db.docHistory
  78. .find({ doc_id: ObjectId(doc_id) })
  79. .sort({ 'meta.end_ts': 1 })
  80. .toArray(callback)
  81. },
  82. getProjectMetaData(project_id, callback) {
  83. if (callback == null) {
  84. callback = function (error, updates) {}
  85. }
  86. return db.projectHistoryMetaData.findOne(
  87. {
  88. project_id: ObjectId(project_id),
  89. },
  90. callback
  91. )
  92. },
  93. setPreserveHistoryForProject(project_id, callback) {
  94. if (callback == null) {
  95. callback = function (error) {}
  96. }
  97. return db.projectHistoryMetaData.updateOne(
  98. {
  99. project_id: ObjectId(project_id),
  100. },
  101. {
  102. $set: { preserveHistory: true },
  103. },
  104. {
  105. upsert: true,
  106. },
  107. callback
  108. )
  109. },
  110. pushRawUpdates(project_id, doc_id, updates, callback) {
  111. if (callback == null) {
  112. callback = function (error) {}
  113. }
  114. return rclient.sadd(
  115. Keys.docsWithHistoryOps({ project_id }),
  116. doc_id,
  117. error => {
  118. if (error != null) {
  119. return callback(error)
  120. }
  121. return rclient.rpush(
  122. Keys.uncompressedHistoryOps({ doc_id }),
  123. ...Array.from(Array.from(updates).map(u => JSON.stringify(u))),
  124. callback
  125. )
  126. }
  127. )
  128. },
  129. getDiff(project_id, doc_id, from, to, callback) {
  130. if (callback == null) {
  131. callback = function (error, diff) {}
  132. }
  133. return request.get(
  134. {
  135. url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/diff?from=${from}&to=${to}`,
  136. },
  137. (error, response, body) => {
  138. response.statusCode.should.equal(200)
  139. return callback(null, JSON.parse(body))
  140. }
  141. )
  142. },
  143. getUpdates(project_id, options, callback) {
  144. if (callback == null) {
  145. callback = function (error, body) {}
  146. }
  147. return request.get(
  148. {
  149. url: `http://localhost:3015/project/${project_id}/updates?before=${options.before}&min_count=${options.min_count}`,
  150. },
  151. (error, response, body) => {
  152. response.statusCode.should.equal(200)
  153. return callback(null, JSON.parse(body))
  154. }
  155. )
  156. },
  157. exportProject(project_id, callback) {
  158. request.get(
  159. { url: `http://localhost:3015/project/${project_id}/export`, json: true },
  160. (error, response, updates) => {
  161. if (error) return callback(error)
  162. response.statusCode.should.equal(200)
  163. callback(null, updates, JSON.parse(response.trailers['x-user-ids']))
  164. }
  165. )
  166. },
  167. restoreDoc(project_id, doc_id, version, user_id, callback) {
  168. if (callback == null) {
  169. callback = function (error) {}
  170. }
  171. return request.post(
  172. {
  173. url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/version/${version}/restore`,
  174. headers: {
  175. 'X-User-Id': user_id,
  176. },
  177. },
  178. (error, response, body) => {
  179. response.statusCode.should.equal(204)
  180. return callback(null)
  181. }
  182. )
  183. },
  184. pushDocHistory(project_id, doc_id, callback) {
  185. if (callback == null) {
  186. callback = function (error) {}
  187. }
  188. return request.post(
  189. {
  190. url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/push`,
  191. },
  192. (error, response, body) => {
  193. response.statusCode.should.equal(204)
  194. return callback(error)
  195. }
  196. )
  197. },
  198. pullDocHistory(project_id, doc_id, callback) {
  199. if (callback == null) {
  200. callback = function (error) {}
  201. }
  202. return request.post(
  203. {
  204. url: `http://localhost:3015/project/${project_id}/doc/${doc_id}/pull`,
  205. },
  206. (error, response, body) => {
  207. response.statusCode.should.equal(204)
  208. return callback(error)
  209. }
  210. )
  211. },
  212. waitForS3(done, retries) {
  213. if (retries == null) {
  214. retries = 42
  215. }
  216. if (!Settings.trackchanges.s3.endpoint) {
  217. return done()
  218. }
  219. return request.get(`${Settings.trackchanges.s3.endpoint}/`, (err, res) => {
  220. if (res && res.statusCode < 500) {
  221. return done()
  222. }
  223. if (retries === 0) {
  224. return done(err || new Error(`s3 returned ${res.statusCode}`))
  225. }
  226. return setTimeout(
  227. () => TrackChangesClient.waitForS3(done, --retries),
  228. 1000
  229. )
  230. })
  231. },
  232. getS3Doc(project_id, doc_id, pack_id, callback) {
  233. if (callback == null) {
  234. callback = function (error, body) {}
  235. }
  236. const params = {
  237. Bucket: S3_BUCKET,
  238. Key: `${project_id}/changes-${doc_id}/pack-${pack_id}`,
  239. }
  240. return s3.getObject(params, (error, data) => {
  241. if (error != null) {
  242. return callback(error)
  243. }
  244. const body = data.Body
  245. if (body == null) {
  246. return callback(new Error('empty response from s3'))
  247. }
  248. return zlib.gunzip(body, (err, result) => {
  249. if (err != null) {
  250. return callback(err)
  251. }
  252. return callback(null, JSON.parse(result.toString()))
  253. })
  254. })
  255. },
  256. removeS3Doc(project_id, doc_id, callback) {
  257. if (callback == null) {
  258. callback = function (error, res, body) {}
  259. }
  260. let params = {
  261. Bucket: S3_BUCKET,
  262. Prefix: `${project_id}/changes-${doc_id}`,
  263. }
  264. return s3.listObjects(params, (error, data) => {
  265. if (error != null) {
  266. return callback(error)
  267. }
  268. params = {
  269. Bucket: S3_BUCKET,
  270. Delete: {
  271. Objects: data.Contents.map(s3object => ({ Key: s3object.Key })),
  272. },
  273. }
  274. return s3.deleteObjects(params, callback)
  275. })
  276. },
  277. }