DocManager.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. /* eslint-disable
  2. no-dupe-keys,
  3. no-undef,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  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 DocManager
  14. const MongoManager = require('./MongoManager')
  15. const Errors = require('./Errors')
  16. const logger = require('@overleaf/logger')
  17. const _ = require('lodash')
  18. const DocArchive = require('./DocArchiveManager')
  19. const RangeManager = require('./RangeManager')
  20. const Settings = require('@overleaf/settings')
  21. module.exports = DocManager = {
  22. // TODO: For historical reasons, the doc version is currently stored in the docOps
  23. // collection (which is all that this collection contains). In future, we should
  24. // migrate this version property to be part of the docs collection, to guarantee
  25. // consitency between lines and version when writing/reading, and for a simpler schema.
  26. _getDoc(projectId, docId, filter, callback) {
  27. if (filter == null) {
  28. filter = {}
  29. }
  30. if (callback == null) {
  31. callback = function () {}
  32. }
  33. if (filter.inS3 !== true) {
  34. return callback(new Error('must include inS3 when getting doc'))
  35. }
  36. return MongoManager.findDoc(projectId, docId, filter, function (err, doc) {
  37. if (err != null) {
  38. return callback(err)
  39. } else if (doc == null) {
  40. return callback(
  41. new Errors.NotFoundError(
  42. `No such doc: ${docId} in project ${projectId}`
  43. )
  44. )
  45. } else if (doc != null ? doc.inS3 : undefined) {
  46. return DocArchive.unarchiveDoc(projectId, docId, function (err) {
  47. if (err != null) {
  48. logger.err({ err, projectId, docId }, 'error unarchiving doc')
  49. return callback(err)
  50. }
  51. return DocManager._getDoc(projectId, docId, filter, callback)
  52. })
  53. } else {
  54. if (filter.version) {
  55. return MongoManager.getDocVersion(docId, function (error, version) {
  56. if (error != null) {
  57. return callback(error)
  58. }
  59. doc.version = version
  60. return callback(err, doc)
  61. })
  62. } else {
  63. return callback(err, doc)
  64. }
  65. }
  66. })
  67. },
  68. isDocDeleted(projectId, docId, callback) {
  69. MongoManager.findDoc(
  70. projectId,
  71. docId,
  72. { deleted: true },
  73. function (err, doc) {
  74. if (err) {
  75. return callback(err)
  76. }
  77. if (!doc) {
  78. return callback(
  79. new Errors.NotFoundError(
  80. `No such project/doc: ${projectId}/${docId}`
  81. )
  82. )
  83. }
  84. // `doc.deleted` is `undefined` for non deleted docs
  85. callback(null, Boolean(doc.deleted))
  86. }
  87. )
  88. },
  89. getFullDoc(projectId, docId, callback) {
  90. if (callback == null) {
  91. callback = function () {}
  92. }
  93. return DocManager._getDoc(
  94. projectId,
  95. docId,
  96. {
  97. lines: true,
  98. rev: true,
  99. deleted: true,
  100. version: true,
  101. ranges: true,
  102. inS3: true,
  103. },
  104. function (err, doc) {
  105. if (err != null) {
  106. return callback(err)
  107. }
  108. return callback(err, doc)
  109. }
  110. )
  111. },
  112. // returns the doc without any version information
  113. _peekRawDoc(projectId, docId, callback) {
  114. MongoManager.findDoc(
  115. projectId,
  116. docId,
  117. {
  118. lines: true,
  119. rev: true,
  120. deleted: true,
  121. version: true,
  122. ranges: true,
  123. inS3: true,
  124. },
  125. (err, doc) => {
  126. if (err) return callback(err)
  127. if (doc == null) {
  128. return callback(
  129. new Errors.NotFoundError(
  130. `No such doc: ${docId} in project ${projectId}`
  131. )
  132. )
  133. }
  134. if (doc && !doc.inS3) {
  135. return callback(null, doc)
  136. }
  137. // skip the unarchiving to mongo when getting a doc
  138. DocArchive.getDoc(projectId, docId, function (err, archivedDoc) {
  139. if (err != null) {
  140. logger.err(
  141. { err, projectId, docId },
  142. 'error getting doc from archive'
  143. )
  144. return callback(err)
  145. }
  146. Object.assign(doc, archivedDoc)
  147. callback(null, doc)
  148. })
  149. }
  150. )
  151. },
  152. // get the doc from mongo if possible, or from the persistent store otherwise,
  153. // without unarchiving it (avoids unnecessary writes to mongo)
  154. peekDoc(projectId, docId, callback) {
  155. DocManager._peekRawDoc(projectId, docId, (err, doc) => {
  156. if (err) {
  157. return callback(err)
  158. }
  159. MongoManager.withRevCheck(
  160. doc,
  161. MongoManager.getDocVersion,
  162. function (error, version) {
  163. // If the doc has been modified while we were retrieving it, we
  164. // will get a DocModified error
  165. if (error != null) {
  166. return callback(error)
  167. }
  168. doc.version = version
  169. return callback(err, doc)
  170. }
  171. )
  172. })
  173. },
  174. getDocLines(projectId, docId, callback) {
  175. if (callback == null) {
  176. callback = function () {}
  177. }
  178. return DocManager._getDoc(
  179. projectId,
  180. docId,
  181. { lines: true, inS3: true },
  182. function (err, doc) {
  183. if (err != null) {
  184. return callback(err)
  185. }
  186. return callback(err, doc)
  187. }
  188. )
  189. },
  190. getAllDeletedDocs(projectId, filter, callback) {
  191. MongoManager.getProjectsDeletedDocs(projectId, filter, callback)
  192. },
  193. getAllNonDeletedDocs(projectId, filter, callback) {
  194. if (callback == null) {
  195. callback = function () {}
  196. }
  197. return DocArchive.unArchiveAllDocs(projectId, function (error) {
  198. if (error != null) {
  199. return callback(error)
  200. }
  201. return MongoManager.getProjectsDocs(
  202. projectId,
  203. { include_deleted: false },
  204. filter,
  205. function (error, docs) {
  206. if (typeof err !== 'undefined' && err !== null) {
  207. return callback(error)
  208. } else if (docs == null) {
  209. return callback(
  210. new Errors.NotFoundError(`No docs for project ${projectId}`)
  211. )
  212. } else {
  213. return callback(null, docs)
  214. }
  215. }
  216. )
  217. })
  218. },
  219. updateDoc(projectId, docId, lines, version, ranges, callback) {
  220. DocManager._tryUpdateDoc(
  221. projectId,
  222. docId,
  223. lines,
  224. version,
  225. ranges,
  226. (err, modified, rev) => {
  227. if (err && err instanceof Errors.DocRevValueError) {
  228. // Another updateDoc call was racing with ours.
  229. // Retry once in a bit.
  230. logger.warn(
  231. { projectId, docId, err },
  232. 'detected concurrent updateDoc call'
  233. )
  234. setTimeout(() => {
  235. DocManager._tryUpdateDoc(
  236. projectId,
  237. docId,
  238. lines,
  239. version,
  240. ranges,
  241. callback
  242. )
  243. }, 100 + Math.random() * 100)
  244. } else {
  245. callback(err, modified, rev)
  246. }
  247. }
  248. )
  249. },
  250. _tryUpdateDoc(projectId, docId, lines, version, ranges, callback) {
  251. if (callback == null) {
  252. callback = function () {}
  253. }
  254. if (lines == null || version == null || ranges == null) {
  255. return callback(new Error('no lines, version or ranges provided'))
  256. }
  257. return DocManager._getDoc(
  258. projectId,
  259. docId,
  260. {
  261. version: true,
  262. rev: true,
  263. lines: true,
  264. version: true,
  265. ranges: true,
  266. inS3: true,
  267. },
  268. function (err, doc) {
  269. let updateLines, updateRanges, updateVersion
  270. if (err != null && !(err instanceof Errors.NotFoundError)) {
  271. logger.err(
  272. { projectId, docId, err },
  273. 'error getting document for update'
  274. )
  275. return callback(err)
  276. }
  277. ranges = RangeManager.jsonRangesToMongo(ranges)
  278. if (doc == null) {
  279. // If the document doesn't exist, we'll make sure to create/update all parts of it.
  280. updateLines = true
  281. updateVersion = true
  282. updateRanges = true
  283. } else {
  284. if (doc.version > version) {
  285. // Reject update when the version was decremented.
  286. // Potential reasons: racing flush, broken history.
  287. return callback(
  288. new Errors.DocVersionDecrementedError('rejecting stale update', {
  289. updateVersion: version,
  290. flushedVersion: doc.version,
  291. })
  292. )
  293. }
  294. updateLines = !_.isEqual(doc.lines, lines)
  295. updateVersion = doc.version !== version
  296. updateRanges = RangeManager.shouldUpdateRanges(doc.ranges, ranges)
  297. }
  298. let modified = false
  299. let rev = (doc != null ? doc.rev : undefined) || 0
  300. const updateLinesAndRangesIfNeeded = function (cb) {
  301. if (updateLines || updateRanges) {
  302. const update = {}
  303. if (updateLines) {
  304. update.lines = lines
  305. }
  306. if (updateRanges) {
  307. update.ranges = ranges
  308. }
  309. logger.debug({ projectId, docId }, 'updating doc lines and ranges')
  310. modified = true
  311. rev += 1 // rev will be incremented in mongo by MongoManager.upsertIntoDocCollection
  312. return MongoManager.upsertIntoDocCollection(
  313. projectId,
  314. docId,
  315. doc?.rev,
  316. update,
  317. cb
  318. )
  319. } else {
  320. logger.debug(
  321. { projectId, docId },
  322. 'doc lines have not changed - not updating'
  323. )
  324. return cb()
  325. }
  326. }
  327. const updateVersionIfNeeded = function (cb) {
  328. if (updateVersion) {
  329. logger.debug(
  330. {
  331. projectId,
  332. docId,
  333. oldVersion: doc != null ? doc.version : undefined,
  334. newVersion: version,
  335. },
  336. 'updating doc version'
  337. )
  338. modified = true
  339. return MongoManager.setDocVersion(docId, version, cb)
  340. } else {
  341. logger.debug(
  342. { projectId, docId, version },
  343. 'doc version has not changed - not updating'
  344. )
  345. return cb()
  346. }
  347. }
  348. return updateLinesAndRangesIfNeeded(function (error) {
  349. if (error != null) {
  350. return callback(error)
  351. }
  352. return updateVersionIfNeeded(function (error) {
  353. if (error != null) {
  354. return callback(error)
  355. }
  356. return callback(null, modified, rev)
  357. })
  358. })
  359. }
  360. )
  361. },
  362. patchDoc(projectId, docId, meta, callback) {
  363. const projection = { _id: 1, deleted: true }
  364. MongoManager.findDoc(projectId, docId, projection, (error, doc) => {
  365. if (error != null) {
  366. return callback(error)
  367. }
  368. if (!doc) {
  369. return callback(
  370. new Errors.NotFoundError(
  371. `No such project/doc to delete: ${projectId}/${docId}`
  372. )
  373. )
  374. }
  375. if (meta.deleted && Settings.docstore.archiveOnSoftDelete) {
  376. // The user will not read this doc anytime soon. Flush it out of mongo.
  377. DocArchive.archiveDoc(projectId, docId, err => {
  378. if (err) {
  379. logger.warn(
  380. { projectId, docId, err },
  381. 'archiving a single doc in the background failed'
  382. )
  383. }
  384. })
  385. }
  386. MongoManager.patchDoc(projectId, docId, meta, callback)
  387. })
  388. },
  389. }