MongoManager.js 5.9 KB

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