MongoManager.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. const { db, ObjectId } = require('./mongodb')
  2. const Settings = require('@overleaf/settings')
  3. const Errors = require('./Errors')
  4. const ARCHIVING_LOCK_DURATION_MS = Settings.archivingLockDurationMs
  5. async function findDoc(projectId, docId, projection) {
  6. const doc = await db.docs.findOne(
  7. {
  8. _id: new ObjectId(docId.toString()),
  9. project_id: new ObjectId(projectId.toString()),
  10. },
  11. { projection }
  12. )
  13. if (doc && projection.version && !doc.version) {
  14. doc.version = 0
  15. }
  16. return doc
  17. }
  18. async function getProjectsDeletedDocs(projectId, projection) {
  19. const docs = await db.docs
  20. .find(
  21. {
  22. project_id: new ObjectId(projectId.toString()),
  23. deleted: true,
  24. },
  25. {
  26. projection,
  27. sort: { deletedAt: -1 },
  28. limit: Settings.max_deleted_docs,
  29. }
  30. )
  31. .toArray()
  32. return docs
  33. }
  34. async function getProjectsDocs(projectId, options, projection) {
  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,
  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 getDocRev(docId) {
  193. const doc = await db.docs.findOne(
  194. { _id: new ObjectId(docId.toString()) },
  195. { projection: { rev: 1 } }
  196. )
  197. return doc && doc.rev
  198. }
  199. /**
  200. * Helper method to support optimistic locking.
  201. *
  202. * Check that the rev of an existing doc is unchanged. If the rev has
  203. * changed, return a DocModifiedError.
  204. */
  205. async function checkRevUnchanged(doc) {
  206. const currentRev = await getDocRev(doc._id)
  207. if (isNaN(currentRev) || isNaN(doc.rev)) {
  208. throw new Errors.DocRevValueError('doc rev is NaN', {
  209. doc_id: doc._id,
  210. rev: doc.rev,
  211. currentRev,
  212. })
  213. }
  214. if (doc.rev !== currentRev) {
  215. throw new Errors.DocModifiedError('doc rev has changed', {
  216. doc_id: doc._id,
  217. rev: doc.rev,
  218. currentRev,
  219. })
  220. }
  221. }
  222. async function destroyProject(projectId) {
  223. await db.docs.deleteMany({ project_id: new ObjectId(projectId) })
  224. }
  225. module.exports = {
  226. findDoc,
  227. getProjectsDeletedDocs,
  228. getProjectsDocs,
  229. getArchivedProjectDocs,
  230. getNonArchivedProjectDocIds,
  231. getNonDeletedArchivedProjectDocs,
  232. upsertIntoDocCollection,
  233. restoreArchivedDoc,
  234. patchDoc,
  235. getDocForArchiving,
  236. markDocAsArchived,
  237. checkRevUnchanged,
  238. destroyProject,
  239. }