MongoManager.js 7.2 KB

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