MongoManager.js 7.2 KB

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