MongoManager.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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. 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: ObjectId(docId),
  98. project_id: 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: ObjectId(docId),
  114. project_id: 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: ObjectId(docId),
  135. project_id: 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: ObjectId(docId),
  152. project_id: ObjectId(projectId),
  153. inS3: { $ne: true },
  154. $or: [{ archivingUntil: null }, { archivingUntil: { $lt: new Date() } }],
  155. },
  156. { $set: { archivingUntil } },
  157. { projection: { lines: 1, ranges: 1, rev: 1 } },
  158. (err, result) => {
  159. if (err) {
  160. return callback(err)
  161. }
  162. callback(null, result.value)
  163. }
  164. )
  165. }
  166. /**
  167. * Clear the doc contents from Mongo and release the archiving lock
  168. */
  169. function markDocAsArchived(projectId, docId, rev, callback) {
  170. db.docs.updateOne(
  171. { _id: ObjectId(docId), rev },
  172. {
  173. $set: { inS3: true },
  174. $unset: { lines: 1, ranges: 1, archivingUntil: 1 },
  175. },
  176. callback
  177. )
  178. }
  179. /**
  180. * Restore an archived doc
  181. *
  182. * This checks that the archived doc's rev matches.
  183. */
  184. function restoreArchivedDoc(projectId, docId, archivedDoc, callback) {
  185. const query = {
  186. _id: ObjectId(docId),
  187. project_id: ObjectId(projectId),
  188. rev: archivedDoc.rev,
  189. }
  190. const update = {
  191. $set: {
  192. lines: archivedDoc.lines,
  193. ranges: archivedDoc.ranges || {},
  194. },
  195. $unset: {
  196. inS3: true,
  197. },
  198. }
  199. db.docs.updateOne(query, update, (err, result) => {
  200. if (err) {
  201. OError.tag(err, 'failed to unarchive doc', {
  202. docId,
  203. rev: archivedDoc.rev,
  204. })
  205. return callback(err)
  206. }
  207. if (result.matchedCount === 0) {
  208. return callback(
  209. new Errors.DocRevValueError('failed to unarchive doc', {
  210. docId,
  211. rev: archivedDoc.rev,
  212. })
  213. )
  214. }
  215. callback()
  216. })
  217. }
  218. function getDocVersion(docId, callback) {
  219. db.docOps.findOne(
  220. {
  221. doc_id: ObjectId(docId),
  222. },
  223. {
  224. projection: {
  225. version: 1,
  226. },
  227. },
  228. function (error, doc) {
  229. if (error) {
  230. return callback(error)
  231. }
  232. callback(null, (doc && doc.version) || 0)
  233. }
  234. )
  235. }
  236. function getDocRev(docId, callback) {
  237. db.docs.findOne(
  238. {
  239. _id: ObjectId(docId.toString()),
  240. },
  241. {
  242. projection: { rev: 1 },
  243. },
  244. function (err, doc) {
  245. if (err) {
  246. return callback(err)
  247. }
  248. callback(null, doc && doc.rev)
  249. }
  250. )
  251. }
  252. /**
  253. * Helper method to support optimistic locking.
  254. *
  255. * Check that the rev of an existing doc is unchanged. If the rev has
  256. * changed, return a DocModifiedError.
  257. */
  258. function checkRevUnchanged(doc, callback) {
  259. getDocRev(doc._id, function (err, currentRev) {
  260. if (err) return callback(err)
  261. if (isNaN(currentRev) || isNaN(doc.rev)) {
  262. return callback(
  263. new Errors.DocRevValueError('doc rev is NaN', {
  264. doc_id: doc._id,
  265. rev: doc.rev,
  266. currentRev,
  267. })
  268. )
  269. }
  270. if (doc.rev !== currentRev) {
  271. return callback(
  272. new Errors.DocModifiedError('doc rev has changed', {
  273. doc_id: doc._id,
  274. rev: doc.rev,
  275. currentRev,
  276. })
  277. )
  278. }
  279. callback()
  280. })
  281. }
  282. function destroyProject(projectId, callback) {
  283. db.docs
  284. .find({ project_id: ObjectId(projectId) }, { projection: { _id: 1 } })
  285. .toArray((err, records) => {
  286. const docIds = records.map(r => r._id)
  287. if (err) {
  288. return callback(err)
  289. }
  290. db.docOps.deleteMany({ doc_id: { $in: docIds } }, err => {
  291. if (err) {
  292. return callback(err)
  293. }
  294. db.docs.deleteMany({ project_id: ObjectId(projectId) }, callback)
  295. })
  296. })
  297. }
  298. module.exports = {
  299. findDoc,
  300. getProjectsDeletedDocs,
  301. getProjectsDocs,
  302. getArchivedProjectDocs,
  303. getNonArchivedProjectDocIds,
  304. getNonDeletedArchivedProjectDocs,
  305. upsertIntoDocCollection,
  306. restoreArchivedDoc,
  307. patchDoc,
  308. getDocForArchiving,
  309. markDocAsArchived,
  310. getDocVersion,
  311. checkRevUnchanged,
  312. destroyProject,
  313. }
  314. const methods = Object.getOwnPropertyNames(module.exports)
  315. module.exports.promises = {}
  316. for (const method of methods) {
  317. metrics.timeAsyncMethod(module.exports, method, 'mongo.MongoManager', logger)
  318. module.exports.promises[method] = promisify(module.exports[method])
  319. }