MongoManager.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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: new ObjectId(docId.toString()),
  13. project_id: new 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: new 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: new 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: new 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: new 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: new 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. const update = {
  89. $set: updates,
  90. $unset: { inS3: true },
  91. }
  92. if (updates.lines || updates.ranges) {
  93. update.$inc = { rev: 1 }
  94. }
  95. db.docs.updateOne(
  96. {
  97. _id: new ObjectId(docId),
  98. project_id: new ObjectId(projectId),
  99. rev: previousRev,
  100. },
  101. update,
  102. (err, result) => {
  103. if (err) return callback(err)
  104. if (result.matchedCount !== 1) {
  105. return callback(new Errors.DocRevValueError())
  106. }
  107. callback()
  108. }
  109. )
  110. } else {
  111. db.docs.insertOne(
  112. {
  113. _id: new ObjectId(docId),
  114. project_id: new ObjectId(projectId),
  115. rev: 1,
  116. ...updates,
  117. },
  118. err => {
  119. if (err) {
  120. if (err.code === 11000) {
  121. // duplicate doc _id
  122. return callback(new Errors.DocRevValueError())
  123. }
  124. return callback(err)
  125. }
  126. callback()
  127. }
  128. )
  129. }
  130. }
  131. function patchDoc(projectId, docId, meta, callback) {
  132. db.docs.updateOne(
  133. {
  134. _id: new ObjectId(docId),
  135. project_id: new ObjectId(projectId),
  136. },
  137. { $set: meta },
  138. callback
  139. )
  140. }
  141. /**
  142. * Fetch a doc and lock it for archiving
  143. *
  144. * This will return null if the doc is not found, if it's already archived or
  145. * if the lock can't be acquired.
  146. */
  147. function getDocForArchiving(projectId, docId, callback) {
  148. const archivingUntil = new Date(Date.now() + ARCHIVING_LOCK_DURATION_MS)
  149. db.docs.findOneAndUpdate(
  150. {
  151. _id: new ObjectId(docId),
  152. project_id: new ObjectId(projectId),
  153. inS3: { $ne: true },
  154. $or: [{ archivingUntil: null }, { archivingUntil: { $lt: new Date() } }],
  155. },
  156. { $set: { archivingUntil } },
  157. {
  158. projection: { lines: 1, ranges: 1, rev: 1 },
  159. includeResultMetadata: true,
  160. },
  161. (err, result) => {
  162. if (err) {
  163. return callback(err)
  164. }
  165. callback(null, result.value)
  166. }
  167. )
  168. }
  169. /**
  170. * Clear the doc contents from Mongo and release the archiving lock
  171. */
  172. function markDocAsArchived(projectId, docId, rev, callback) {
  173. db.docs.updateOne(
  174. { _id: new ObjectId(docId), rev },
  175. {
  176. $set: { inS3: true },
  177. $unset: { lines: 1, ranges: 1, archivingUntil: 1 },
  178. },
  179. callback
  180. )
  181. }
  182. /**
  183. * Restore an archived doc
  184. *
  185. * This checks that the archived doc's rev matches.
  186. */
  187. function restoreArchivedDoc(projectId, docId, archivedDoc, callback) {
  188. const query = {
  189. _id: new ObjectId(docId),
  190. project_id: new ObjectId(projectId),
  191. rev: archivedDoc.rev,
  192. }
  193. const update = {
  194. $set: {
  195. lines: archivedDoc.lines,
  196. ranges: archivedDoc.ranges || {},
  197. },
  198. $unset: {
  199. inS3: true,
  200. },
  201. }
  202. db.docs.updateOne(query, update, (err, result) => {
  203. if (err) {
  204. OError.tag(err, 'failed to unarchive doc', {
  205. docId,
  206. rev: archivedDoc.rev,
  207. })
  208. return callback(err)
  209. }
  210. if (result.matchedCount === 0) {
  211. return callback(
  212. new Errors.DocRevValueError('failed to unarchive doc', {
  213. docId,
  214. rev: archivedDoc.rev,
  215. })
  216. )
  217. }
  218. callback()
  219. })
  220. }
  221. function getDocVersion(docId, callback) {
  222. db.docOps.findOne(
  223. {
  224. doc_id: new ObjectId(docId),
  225. },
  226. {
  227. projection: {
  228. version: 1,
  229. },
  230. },
  231. function (error, doc) {
  232. if (error) {
  233. return callback(error)
  234. }
  235. callback(null, (doc && doc.version) || 0)
  236. }
  237. )
  238. }
  239. function getDocRev(docId, callback) {
  240. db.docs.findOne(
  241. {
  242. _id: new ObjectId(docId.toString()),
  243. },
  244. {
  245. projection: { rev: 1 },
  246. },
  247. function (err, doc) {
  248. if (err) {
  249. return callback(err)
  250. }
  251. callback(null, doc && doc.rev)
  252. }
  253. )
  254. }
  255. /**
  256. * Helper method to support optimistic locking.
  257. *
  258. * Check that the rev of an existing doc is unchanged. If the rev has
  259. * changed, return a DocModifiedError.
  260. */
  261. function checkRevUnchanged(doc, callback) {
  262. getDocRev(doc._id, function (err, currentRev) {
  263. if (err) return callback(err)
  264. if (isNaN(currentRev) || isNaN(doc.rev)) {
  265. return callback(
  266. new Errors.DocRevValueError('doc rev is NaN', {
  267. doc_id: doc._id,
  268. rev: doc.rev,
  269. currentRev,
  270. })
  271. )
  272. }
  273. if (doc.rev !== currentRev) {
  274. return callback(
  275. new Errors.DocModifiedError('doc rev has changed', {
  276. doc_id: doc._id,
  277. rev: doc.rev,
  278. currentRev,
  279. })
  280. )
  281. }
  282. callback()
  283. })
  284. }
  285. function destroyProject(projectId, callback) {
  286. db.docs
  287. .find({ project_id: new ObjectId(projectId) }, { projection: { _id: 1 } })
  288. .toArray((err, records) => {
  289. const docIds = records.map(r => r._id)
  290. if (err) {
  291. return callback(err)
  292. }
  293. db.docOps.deleteMany({ doc_id: { $in: docIds } }, err => {
  294. if (err) {
  295. return callback(err)
  296. }
  297. db.docs.deleteMany({ project_id: new ObjectId(projectId) }, callback)
  298. })
  299. })
  300. }
  301. module.exports = {
  302. findDoc,
  303. getProjectsDeletedDocs,
  304. getProjectsDocs,
  305. getArchivedProjectDocs,
  306. getNonArchivedProjectDocIds,
  307. getNonDeletedArchivedProjectDocs,
  308. upsertIntoDocCollection,
  309. restoreArchivedDoc,
  310. patchDoc,
  311. getDocForArchiving,
  312. markDocAsArchived,
  313. getDocVersion,
  314. checkRevUnchanged,
  315. destroyProject,
  316. }
  317. const methods = Object.getOwnPropertyNames(module.exports)
  318. module.exports.promises = {}
  319. for (const method of methods) {
  320. metrics.timeAsyncMethod(module.exports, method, 'mongo.MongoManager', logger)
  321. module.exports.promises[method] = promisify(module.exports[method])
  322. }