DocArchiveManager.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. no-useless-escape,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS207: Consider shorter variations of null checks
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. let DocArchive
  15. const MongoManager = require('./MongoManager')
  16. const Errors = require('./Errors')
  17. const logger = require('logger-sharelatex')
  18. const _ = require('underscore')
  19. const async = require('async')
  20. const settings = require('settings-sharelatex')
  21. const request = require('request')
  22. const crypto = require('crypto')
  23. const RangeManager = require('./RangeManager')
  24. const thirtySeconds = 30 * 1000
  25. module.exports = DocArchive = {
  26. archiveAllDocs(project_id, callback) {
  27. if (callback == null) {
  28. callback = function (err, docs) {}
  29. }
  30. return MongoManager.getProjectsDocs(
  31. project_id,
  32. { include_deleted: true },
  33. { lines: true, ranges: true, rev: true, inS3: true },
  34. function (err, docs) {
  35. if (err != null) {
  36. return callback(err)
  37. } else if (docs == null) {
  38. return callback(
  39. new Errors.NotFoundError(`No docs for project ${project_id}`)
  40. )
  41. }
  42. docs = _.filter(docs, (doc) => doc.inS3 !== true)
  43. const jobs = _.map(docs, (doc) => (cb) =>
  44. DocArchive.archiveDoc(project_id, doc, cb)
  45. )
  46. return async.parallelLimit(jobs, 5, callback)
  47. }
  48. )
  49. },
  50. archiveDoc(project_id, doc, callback) {
  51. let options
  52. logger.log({ project_id, doc_id: doc._id }, 'sending doc to s3')
  53. try {
  54. options = DocArchive.buildS3Options(project_id + '/' + doc._id)
  55. } catch (e) {
  56. return callback(e)
  57. }
  58. return DocArchive._mongoDocToS3Doc(doc, function (error, json_doc) {
  59. if (error != null) {
  60. return callback(error)
  61. }
  62. options.body = json_doc
  63. options.headers = { 'Content-Type': 'application/json' }
  64. return request.put(options, function (err, res) {
  65. if (err != null || res.statusCode !== 200) {
  66. logger.err(
  67. {
  68. err,
  69. res,
  70. project_id,
  71. doc_id: doc._id,
  72. statusCode: res != null ? res.statusCode : undefined
  73. },
  74. 'something went wrong archiving doc in aws'
  75. )
  76. return callback(new Error('Error in S3 request'))
  77. }
  78. const md5lines = crypto
  79. .createHash('md5')
  80. .update(json_doc, 'utf8')
  81. .digest('hex')
  82. const md5response = res.headers.etag.toString().replace(/\"/g, '')
  83. if (md5lines !== md5response) {
  84. logger.err(
  85. {
  86. responseMD5: md5response,
  87. linesMD5: md5lines,
  88. project_id,
  89. doc_id: doc != null ? doc._id : undefined
  90. },
  91. 'err in response md5 from s3'
  92. )
  93. return callback(new Error('Error in S3 md5 response'))
  94. }
  95. return MongoManager.markDocAsArchived(doc._id, doc.rev, function (err) {
  96. if (err != null) {
  97. return callback(err)
  98. }
  99. return callback()
  100. })
  101. })
  102. })
  103. },
  104. unArchiveAllDocs(project_id, callback) {
  105. if (callback == null) {
  106. callback = function (err) {}
  107. }
  108. return MongoManager.getArchivedProjectDocs(project_id, function (
  109. err,
  110. docs
  111. ) {
  112. if (err != null) {
  113. logger.err({ err, project_id }, 'error unarchiving all docs')
  114. return callback(err)
  115. } else if (docs == null) {
  116. return callback(
  117. new Errors.NotFoundError(`No docs for project ${project_id}`)
  118. )
  119. }
  120. const jobs = _.map(
  121. docs,
  122. (doc) =>
  123. function (cb) {
  124. if (doc.inS3 == null) {
  125. return cb()
  126. } else {
  127. return DocArchive.unarchiveDoc(project_id, doc._id, cb)
  128. }
  129. }
  130. )
  131. return async.parallelLimit(jobs, 5, callback)
  132. })
  133. },
  134. unarchiveDoc(project_id, doc_id, callback) {
  135. let options
  136. logger.log({ project_id, doc_id }, 'getting doc from s3')
  137. try {
  138. options = DocArchive.buildS3Options(project_id + '/' + doc_id)
  139. } catch (e) {
  140. return callback(e)
  141. }
  142. options.json = true
  143. return request.get(options, function (err, res, doc) {
  144. if (err != null || res.statusCode !== 200) {
  145. logger.err(
  146. { err, res, project_id, doc_id },
  147. 'something went wrong unarchiving doc from aws'
  148. )
  149. return callback(new Errors.NotFoundError('Error in S3 request'))
  150. }
  151. return DocArchive._s3DocToMongoDoc(doc, function (error, mongo_doc) {
  152. if (error != null) {
  153. return callback(error)
  154. }
  155. return MongoManager.upsertIntoDocCollection(
  156. project_id,
  157. doc_id.toString(),
  158. mongo_doc,
  159. function (err) {
  160. if (err != null) {
  161. return callback(err)
  162. }
  163. logger.log({ project_id, doc_id }, 'deleting doc from s3')
  164. return DocArchive._deleteDocFromS3(project_id, doc_id, callback)
  165. }
  166. )
  167. })
  168. })
  169. },
  170. destroyAllDocs(project_id, callback) {
  171. if (callback == null) {
  172. callback = function (err) {}
  173. }
  174. return MongoManager.getProjectsDocs(
  175. project_id,
  176. { include_deleted: true },
  177. { _id: 1 },
  178. function (err, docs) {
  179. if (err != null) {
  180. logger.err({ err, project_id }, "error getting project's docs")
  181. return callback(err)
  182. } else if (docs == null) {
  183. return callback()
  184. }
  185. const jobs = _.map(docs, (doc) => (cb) =>
  186. DocArchive.destroyDoc(project_id, doc._id, cb)
  187. )
  188. return async.parallelLimit(jobs, 5, callback)
  189. }
  190. )
  191. },
  192. destroyDoc(project_id, doc_id, callback) {
  193. logger.log({ project_id, doc_id }, 'removing doc from mongo and s3')
  194. return MongoManager.findDoc(project_id, doc_id, { inS3: 1 }, function (
  195. error,
  196. doc
  197. ) {
  198. if (error != null) {
  199. return callback(error)
  200. }
  201. if (doc == null) {
  202. return callback(new Errors.NotFoundError('Doc not found in Mongo'))
  203. }
  204. if (doc.inS3 === true) {
  205. return DocArchive._deleteDocFromS3(project_id, doc_id, function (err) {
  206. if (err != null) {
  207. return err
  208. }
  209. return MongoManager.destroyDoc(doc_id, callback)
  210. })
  211. } else {
  212. return MongoManager.destroyDoc(doc_id, callback)
  213. }
  214. })
  215. },
  216. _deleteDocFromS3(project_id, doc_id, callback) {
  217. let options
  218. try {
  219. options = DocArchive.buildS3Options(project_id + '/' + doc_id)
  220. } catch (e) {
  221. return callback(e)
  222. }
  223. options.json = true
  224. return request.del(options, function (err, res, body) {
  225. if (err != null || res.statusCode !== 204) {
  226. logger.err(
  227. { err, res, project_id, doc_id },
  228. 'something went wrong deleting doc from aws'
  229. )
  230. return callback(new Error('Error in S3 request'))
  231. }
  232. return callback()
  233. })
  234. },
  235. _s3DocToMongoDoc(doc, callback) {
  236. if (callback == null) {
  237. callback = function (error, mongo_doc) {}
  238. }
  239. const mongo_doc = {}
  240. if (doc.schema_v === 1 && doc.lines != null) {
  241. mongo_doc.lines = doc.lines
  242. if (doc.ranges != null) {
  243. mongo_doc.ranges = RangeManager.jsonRangesToMongo(doc.ranges)
  244. }
  245. } else if (doc instanceof Array) {
  246. mongo_doc.lines = doc
  247. } else {
  248. return callback(new Error("I don't understand the doc format in s3"))
  249. }
  250. return callback(null, mongo_doc)
  251. },
  252. _mongoDocToS3Doc(doc, callback) {
  253. if (callback == null) {
  254. callback = function (error, s3_doc) {}
  255. }
  256. if (doc.lines == null) {
  257. return callback(new Error('doc has no lines'))
  258. }
  259. const json = JSON.stringify({
  260. lines: doc.lines,
  261. ranges: doc.ranges,
  262. schema_v: 1
  263. })
  264. if (json.indexOf('\u0000') !== -1) {
  265. const error = new Error('null bytes detected')
  266. logger.err({ err: error, doc, json }, error.message)
  267. return callback(error)
  268. }
  269. return callback(null, json)
  270. },
  271. buildS3Options(key) {
  272. if (settings.docstore.s3 == null) {
  273. throw new Error('S3 settings are not configured')
  274. }
  275. return {
  276. aws: {
  277. key: settings.docstore.s3.key,
  278. secret: settings.docstore.s3.secret,
  279. bucket: settings.docstore.s3.bucket
  280. },
  281. timeout: thirtySeconds,
  282. uri: `https://${settings.docstore.s3.bucket}.s3.amazonaws.com/${key}`
  283. }
  284. }
  285. }