TrackChangesClient.js 7.7 KB

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