| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- import mongodb from './mongodb.js'
- import Settings from '@overleaf/settings'
- import Errors from './Errors.js'
- import Metrics from '@overleaf/metrics'
- const { db, ObjectId, BSON } = mongodb
- const ARCHIVING_LOCK_DURATION_MS = Settings.archivingLockDurationMs
- async function findDoc(projectId, docId, projection) {
- const doc = await db.docs.findOne(
- {
- _id: new ObjectId(docId.toString()),
- project_id: new ObjectId(projectId.toString()),
- },
- { projection }
- )
- if (doc && projection.version && !doc.version) {
- doc.version = 0
- }
- return doc
- }
- async function getProjectsDeletedDocs(projectId, projection) {
- const docs = await db.docs
- .find(
- {
- project_id: new ObjectId(projectId.toString()),
- deleted: true,
- },
- {
- projection,
- sort: { deletedAt: -1 },
- limit: Settings.max_deleted_docs,
- }
- )
- .toArray()
- return docs
- }
- async function getProjectsDocs(projectId, options, projection) {
- const query = { project_id: new ObjectId(projectId.toString()) }
- if (!options.include_deleted) {
- query.deleted = { $ne: true }
- }
- const queryOptions = {
- projection,
- }
- if (options.limit) {
- queryOptions.limit = options.limit
- }
- const docs = await db.docs.find(query, queryOptions).toArray()
- return docs
- }
- async function getArchivedProjectDocs(projectId, maxResults) {
- const query = {
- project_id: new ObjectId(projectId.toString()),
- inS3: true,
- }
- const docs = await db.docs
- .find(query, { projection: { _id: 1 }, limit: maxResults })
- .toArray()
- return docs
- }
- async function getNonArchivedProjectDocIds(projectId) {
- const docs = await db.docs
- .find(
- {
- project_id: new ObjectId(projectId),
- inS3: { $ne: true },
- },
- { projection: { _id: 1 } }
- )
- .map(doc => doc._id)
- .toArray()
- return docs
- }
- async function getNonDeletedArchivedProjectDocs(projectId, maxResults) {
- const query = {
- project_id: new ObjectId(projectId.toString()),
- deleted: { $ne: true },
- inS3: true,
- }
- const docs = await db.docs
- .find(query, { projection: { _id: 1 }, limit: maxResults })
- .toArray()
- return docs
- }
- async function upsertIntoDocCollection(projectId, docId, previousRev, updates) {
- if (previousRev) {
- const update = {
- $set: updates,
- $unset: { inS3: true },
- }
- if (updates.lines || updates.ranges) {
- update.$inc = { rev: 1 }
- }
- const payloadSize = BSON.calculateObjectSize(update)
- Metrics.count('mongo_docs_write', payloadSize, 1, { method: 'update' })
- const result = await db.docs.updateOne(
- {
- _id: new ObjectId(docId),
- project_id: new ObjectId(projectId),
- rev: previousRev,
- },
- update
- )
- if (result.matchedCount !== 1) {
- throw new Errors.DocRevValueError()
- }
- } else {
- const payloadSize = BSON.calculateObjectSize(updates)
- Metrics.count('mongo_docs_write', payloadSize, 1, { method: 'insert' })
- try {
- await db.docs.insertOne({
- _id: new ObjectId(docId),
- project_id: new ObjectId(projectId),
- rev: 1,
- ...updates,
- })
- } catch (err) {
- if (err.code === 11000) {
- // duplicate doc _id
- throw new Errors.DocRevValueError()
- } else {
- throw err
- }
- }
- }
- }
- async function patchDoc(projectId, docId, meta) {
- const payloadSize = BSON.calculateObjectSize(meta)
- Metrics.count('mongo_docs_write', payloadSize, 1, { method: 'patch' })
- await db.docs.updateOne(
- {
- _id: new ObjectId(docId),
- project_id: new ObjectId(projectId),
- },
- { $set: meta }
- )
- }
- /**
- * Fetch a doc and lock it for archiving
- *
- * This will return null if the doc is not found, if it's already archived or
- * if the lock can't be acquired.
- */
- async function getDocForArchiving(projectId, docId) {
- const archivingUntil = new Date(Date.now() + ARCHIVING_LOCK_DURATION_MS)
- const result = await db.docs.findOneAndUpdate(
- {
- _id: new ObjectId(docId),
- project_id: new ObjectId(projectId),
- inS3: { $ne: true },
- $or: [{ archivingUntil: null }, { archivingUntil: { $lt: new Date() } }],
- },
- { $set: { archivingUntil } },
- {
- projection: { lines: 1, ranges: 1, rev: 1 },
- includeResultMetadata: true,
- }
- )
- return result.value
- }
- /**
- * Clear the doc contents from Mongo and release the archiving lock
- */
- async function markDocAsArchived(projectId, docId, rev) {
- await db.docs.updateOne(
- { _id: new ObjectId(docId), rev },
- {
- $set: { inS3: true },
- $unset: { lines: 1, ranges: 1, archivingUntil: 1 },
- }
- )
- }
- /**
- * Restore an archived doc
- *
- * This checks that the archived doc's rev matches.
- */
- async function restoreArchivedDoc(projectId, docId, archivedDoc) {
- const query = {
- _id: new ObjectId(docId),
- project_id: new ObjectId(projectId),
- rev: archivedDoc.rev,
- }
- const update = {
- $set: {
- lines: archivedDoc.lines,
- ranges: archivedDoc.ranges || {},
- },
- $unset: {
- inS3: true,
- },
- }
- const payloadSize = BSON.calculateObjectSize(update)
- Metrics.count('mongo_docs_write', payloadSize, 1, { method: 'restore' })
- const result = await db.docs.updateOne(query, update)
- if (result.matchedCount === 0) {
- throw new Errors.DocRevValueError('failed to unarchive doc', {
- docId,
- rev: archivedDoc.rev,
- })
- }
- }
- async function getDocRev(docId) {
- const doc = await db.docs.findOne(
- { _id: new ObjectId(docId.toString()) },
- { projection: { rev: 1 } }
- )
- return doc && doc.rev
- }
- /**
- * Helper method to support optimistic locking.
- *
- * Check that the rev of an existing doc is unchanged. If the rev has
- * changed, return a DocModifiedError.
- */
- async function checkRevUnchanged(doc) {
- const currentRev = await getDocRev(doc._id)
- if (isNaN(currentRev) || isNaN(doc.rev)) {
- throw new Errors.DocRevValueError('doc rev is NaN', {
- doc_id: doc._id,
- rev: doc.rev,
- currentRev,
- })
- }
- if (doc.rev !== currentRev) {
- throw new Errors.DocModifiedError('doc rev has changed', {
- doc_id: doc._id,
- rev: doc.rev,
- currentRev,
- })
- }
- }
- async function destroyProject(projectId) {
- await db.docs.deleteMany({ project_id: new ObjectId(projectId) })
- }
- export default {
- findDoc,
- getProjectsDeletedDocs,
- getProjectsDocs,
- getArchivedProjectDocs,
- getNonArchivedProjectDocIds,
- getNonDeletedArchivedProjectDocs,
- upsertIntoDocCollection,
- restoreArchivedDoc,
- patchDoc,
- getDocForArchiving,
- markDocAsArchived,
- checkRevUnchanged,
- destroyProject,
- }
|