MongoManager.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. const { db, ObjectId } = require('./mongodb')
  2. const logger = require('@overleaf/logger')
  3. const metrics = require('@overleaf/metrics')
  4. const Settings = require('@overleaf/settings')
  5. const OError = require('@overleaf/o-error')
  6. const Errors = require('./Errors')
  7. const { promisify } = require('util')
  8. const ARCHIVING_LOCK_DURATION_MS = Settings.archivingLockDurationMs
  9. function findDoc(projectId, docId, filter, callback) {
  10. db.docs.findOne(
  11. {
  12. _id: ObjectId(docId.toString()),
  13. project_id: ObjectId(projectId.toString()),
  14. },
  15. {
  16. projection: filter,
  17. },
  18. callback
  19. )
  20. }
  21. function getProjectsDeletedDocs(projectId, filter, callback) {
  22. db.docs
  23. .find(
  24. {
  25. project_id: ObjectId(projectId.toString()),
  26. deleted: true,
  27. },
  28. {
  29. projection: filter,
  30. sort: { deletedAt: -1 },
  31. limit: Settings.max_deleted_docs,
  32. }
  33. )
  34. .toArray(callback)
  35. }
  36. function getProjectsDocs(projectId, options, filter, callback) {
  37. const query = { project_id: ObjectId(projectId.toString()) }
  38. if (!options.include_deleted) {
  39. query.deleted = { $ne: true }
  40. }
  41. const queryOptions = {
  42. projection: filter,
  43. }
  44. if (options.limit) {
  45. queryOptions.limit = options.limit
  46. }
  47. db.docs.find(query, queryOptions).toArray(callback)
  48. }
  49. function getArchivedProjectDocs(projectId, maxResults, callback) {
  50. const query = {
  51. project_id: ObjectId(projectId.toString()),
  52. inS3: true,
  53. }
  54. db.docs
  55. .find(query, { projection: { _id: 1 }, limit: maxResults })
  56. .toArray(callback)
  57. }
  58. function getNonArchivedProjectDocIds(projectId, callback) {
  59. db.docs
  60. .find(
  61. {
  62. project_id: ObjectId(projectId),
  63. inS3: { $ne: true },
  64. },
  65. { projection: { _id: 1 } }
  66. )
  67. .map(doc => doc._id)
  68. .toArray(callback)
  69. }
  70. function getNonDeletedArchivedProjectDocs(projectId, maxResults, callback) {
  71. const query = {
  72. project_id: ObjectId(projectId.toString()),
  73. deleted: { $ne: true },
  74. inS3: true,
  75. }
  76. db.docs
  77. .find(query, { projection: { _id: 1 }, limit: maxResults })
  78. .toArray(callback)
  79. }
  80. function upsertIntoDocCollection(
  81. projectId,
  82. docId,
  83. previousRev,
  84. updates,
  85. callback
  86. ) {
  87. if (previousRev) {
  88. db.docs.updateOne(
  89. {
  90. _id: ObjectId(docId),
  91. project_id: ObjectId(projectId),
  92. rev: previousRev,
  93. },
  94. {
  95. $set: updates,
  96. $inc: {
  97. rev: 1,
  98. },
  99. $unset: {
  100. inS3: true,
  101. },
  102. },
  103. (err, result) => {
  104. if (err) return callback(err)
  105. if (result.matchedCount !== 1) {
  106. return callback(new Errors.DocRevValueError())
  107. }
  108. callback()
  109. }
  110. )
  111. } else {
  112. db.docs.insertOne(
  113. {
  114. _id: ObjectId(docId),
  115. project_id: ObjectId(projectId),
  116. rev: 1,
  117. ...updates,
  118. },
  119. err => {
  120. if (err) {
  121. if (err.code === 11000) {
  122. // duplicate doc _id
  123. return callback(new Errors.DocRevValueError())
  124. }
  125. return callback(err)
  126. }
  127. callback()
  128. }
  129. )
  130. }
  131. }
  132. function patchDoc(projectId, docId, meta, callback) {
  133. db.docs.updateOne(
  134. {
  135. _id: ObjectId(docId),
  136. project_id: ObjectId(projectId),
  137. },
  138. { $set: meta },
  139. callback
  140. )
  141. }
  142. /**
  143. * Fetch a doc and lock it for archiving
  144. *
  145. * This will return null if the doc is not found, if it's already archived or
  146. * if the lock can't be acquired.
  147. */
  148. function getDocForArchiving(projectId, docId, callback) {
  149. const archivingUntil = new Date(Date.now() + ARCHIVING_LOCK_DURATION_MS)
  150. db.docs.findOneAndUpdate(
  151. {
  152. _id: ObjectId(docId),
  153. project_id: ObjectId(projectId),
  154. inS3: { $ne: true },
  155. $or: [{ archivingUntil: null }, { archivingUntil: { $lt: new Date() } }],
  156. },
  157. { $set: { archivingUntil } },
  158. { projection: { lines: 1, ranges: 1, rev: 1 } },
  159. (err, result) => {
  160. if (err) {
  161. return callback(err)
  162. }
  163. callback(null, result.value)
  164. }
  165. )
  166. }
  167. /**
  168. * Clear the doc contents from Mongo and release the archiving lock
  169. */
  170. function markDocAsArchived(projectId, docId, rev, callback) {
  171. db.docs.updateOne(
  172. { _id: ObjectId(docId), rev },
  173. {
  174. $set: { inS3: true },
  175. $unset: { lines: 1, ranges: 1, archivingUntil: 1 },
  176. },
  177. callback
  178. )
  179. }
  180. /**
  181. * Restore an archived doc
  182. *
  183. * This checks that the archived doc's rev matches.
  184. */
  185. function restoreArchivedDoc(projectId, docId, archivedDoc, callback) {
  186. const query = {
  187. _id: ObjectId(docId),
  188. project_id: ObjectId(projectId),
  189. rev: archivedDoc.rev,
  190. }
  191. const update = {
  192. $set: {
  193. lines: archivedDoc.lines,
  194. ranges: archivedDoc.ranges || {},
  195. },
  196. $unset: {
  197. inS3: true,
  198. },
  199. }
  200. db.docs.updateOne(query, update, (err, result) => {
  201. if (err) {
  202. OError.tag(err, 'failed to unarchive doc', {
  203. docId,
  204. rev: archivedDoc.rev,
  205. })
  206. return callback(err)
  207. }
  208. if (result.matchedCount === 0) {
  209. return callback(
  210. new Errors.DocRevValueError('failed to unarchive doc', {
  211. docId,
  212. rev: archivedDoc.rev,
  213. })
  214. )
  215. }
  216. callback()
  217. })
  218. }
  219. function getDocVersion(docId, callback) {
  220. db.docOps.findOne(
  221. {
  222. doc_id: ObjectId(docId),
  223. },
  224. {
  225. projection: {
  226. version: 1,
  227. },
  228. },
  229. function (error, doc) {
  230. if (error) {
  231. return callback(error)
  232. }
  233. callback(null, (doc && doc.version) || 0)
  234. }
  235. )
  236. }
  237. function setDocVersion(docId, version, callback) {
  238. db.docOps.updateOne(
  239. {
  240. doc_id: ObjectId(docId),
  241. },
  242. {
  243. $set: { version },
  244. },
  245. {
  246. upsert: true,
  247. },
  248. callback
  249. )
  250. }
  251. function getDocRev(docId, callback) {
  252. db.docs.findOne(
  253. {
  254. _id: ObjectId(docId.toString()),
  255. },
  256. {
  257. projection: { rev: 1 },
  258. },
  259. function (err, doc) {
  260. if (err) {
  261. return callback(err)
  262. }
  263. callback(null, doc && doc.rev)
  264. }
  265. )
  266. }
  267. // Helper method to support optimistic locking. Call the provided method for
  268. // an existing doc and return the result if the rev in mongo is unchanged when
  269. // checked afterwards. If the rev has changed, return a DocModifiedError.
  270. function withRevCheck(doc, method, callback) {
  271. method(doc._id, function (err, result) {
  272. if (err) return callback(err)
  273. getDocRev(doc._id, function (err, currentRev) {
  274. if (err) return callback(err)
  275. if (isNaN(currentRev) || isNaN(doc.rev)) {
  276. return callback(
  277. new Errors.DocRevValueError('doc rev is NaN', {
  278. doc_id: doc._id,
  279. rev: doc.rev,
  280. currentRev,
  281. })
  282. )
  283. }
  284. if (doc.rev !== currentRev) {
  285. return callback(
  286. new Errors.DocModifiedError('doc rev has changed', {
  287. doc_id: doc._id,
  288. rev: doc.rev,
  289. currentRev,
  290. })
  291. )
  292. }
  293. callback(null, result)
  294. })
  295. })
  296. }
  297. function destroyProject(projectId, callback) {
  298. db.docs
  299. .find({ project_id: ObjectId(projectId) }, { projection: { _id: 1 } })
  300. .toArray((err, records) => {
  301. const docIds = records.map(r => r._id)
  302. if (err) {
  303. return callback(err)
  304. }
  305. db.docOps.deleteMany({ doc_id: { $in: docIds } }, err => {
  306. if (err) {
  307. return callback(err)
  308. }
  309. db.docs.deleteMany({ project_id: ObjectId(projectId) }, callback)
  310. })
  311. })
  312. }
  313. module.exports = {
  314. findDoc,
  315. getProjectsDeletedDocs,
  316. getProjectsDocs,
  317. getArchivedProjectDocs,
  318. getNonArchivedProjectDocIds,
  319. getNonDeletedArchivedProjectDocs,
  320. upsertIntoDocCollection,
  321. restoreArchivedDoc,
  322. patchDoc,
  323. getDocForArchiving,
  324. markDocAsArchived,
  325. getDocVersion,
  326. setDocVersion,
  327. withRevCheck,
  328. destroyProject,
  329. }
  330. const methods = Object.getOwnPropertyNames(module.exports)
  331. module.exports.promises = {}
  332. for (const method of methods) {
  333. metrics.timeAsyncMethod(module.exports, method, 'mongo.MongoManager', logger)
  334. module.exports.promises[method] = promisify(module.exports[method])
  335. }