| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- const RedisManager = require('./RedisManager')
- const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
- const DocumentManager = require('./DocumentManager')
- const HistoryManager = require('./HistoryManager')
- const logger = require('@overleaf/logger')
- const Metrics = require('./Metrics')
- const Errors = require('./Errors')
- const { callbackifyAll } = require('@overleaf/promise-utils')
- async function flushProjectWithLocks(projectId) {
- const timer = new Metrics.Timer('projectManager.flushProjectWithLocks')
- const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
- logger.debug({ projectId, docIds }, 'flushing docs')
- const errors = []
- for (const docId of docIds) {
- try {
- await DocumentManager.promises.flushDocIfLoadedWithLock(projectId, docId)
- } catch (error) {
- if (error instanceof Errors.NotFoundError) {
- logger.warn(
- { err: error, projectId, docId },
- 'found deleted doc when flushing'
- )
- } else {
- logger.error({ err: error, projectId, docId }, 'error flushing doc')
- errors.push(error)
- }
- }
- }
- timer.done()
- if (errors.length > 0) {
- throw new Error('Errors flushing docs. See log for details')
- }
- }
- async function flushAndDeleteProjectWithLocks(projectId, options) {
- const timer = new Metrics.Timer(
- 'projectManager.flushAndDeleteProjectWithLocks'
- )
- const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
- logger.debug({ projectId, docIds }, 'deleting docs')
- const errors = []
- for (const docId of docIds) {
- try {
- await DocumentManager.promises.flushAndDeleteDocWithLock(
- projectId,
- docId,
- {}
- )
- } catch (error) {
- logger.error({ err: error, projectId, docId }, 'error deleting doc')
- errors.push(error)
- }
- }
- // When deleting the project here we want to ensure that project
- // history is completely flushed because the project may be
- // deleted in web after this call completes, and so further
- // attempts to flush would fail after that.
- await HistoryManager.promises.flushProjectChanges(projectId, options)
- timer.done()
- if (errors.length > 0) {
- throw new Error('Errors deleting docs. See log for details')
- }
- }
- async function queueFlushAndDeleteProject(projectId) {
- await RedisManager.promises.queueFlushAndDeleteProject(projectId)
- Metrics.inc('queued-delete')
- }
- async function getProjectDocsTimestamps(projectId, callback) {
- const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
- if (docIds.length === 0) {
- return []
- }
- const timestamps = await RedisManager.promises.getDocTimestamps(docIds)
- return timestamps
- }
- async function getProjectDocsAndFlushIfOld(
- projectId,
- projectStateHash,
- excludeVersions
- ) {
- const timer = new Metrics.Timer('projectManager.getProjectDocsAndFlushIfOld')
- const projectStateChanged =
- await RedisManager.promises.checkOrSetProjectState(
- projectId,
- projectStateHash
- )
- // we can't return docs if project structure has changed
- if (projectStateChanged) {
- timer.done()
- throw new Errors.ProjectStateChangedError('project state changed')
- }
- // project structure hasn't changed, return doc content from redis
- const docs = []
- const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
- for (const docId of docIds) {
- const { lines, version } =
- await DocumentManager.promises.getDocAndFlushIfOldWithLock(
- projectId,
- docId
- )
- docs.push({ _id: docId, lines, v: version })
- }
- timer.done()
- return docs
- }
- async function getProjectRanges(projectId) {
- const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
- const docs = []
- for (const docId of docIds) {
- const ranges = await RedisManager.promises.getDocRanges(docId)
- docs.push({ id: docId, ranges })
- }
- return docs
- }
- async function clearProjectState(projectId) {
- await RedisManager.promises.clearProjectState(projectId)
- }
- async function updateProjectWithLocks(
- projectId,
- projectHistoryId,
- userId,
- updates,
- projectVersion,
- source
- ) {
- const timer = new Metrics.Timer('projectManager.updateProject')
- let projectSubversion = 0 // project versions can have multiple operations
- let projectOpsLength = 0
- for (const update of updates) {
- update.version = `${projectVersion}.${projectSubversion++}`
- switch (update.type) {
- case 'add-doc':
- projectOpsLength =
- await ProjectHistoryRedisManager.promises.queueAddEntity(
- projectId,
- projectHistoryId,
- 'doc',
- update.id,
- userId,
- update,
- source
- )
- break
- case 'rename-doc':
- if (!update.newPathname) {
- // an empty newPathname signifies a delete, so there is no need to
- // update the pathname in redis
- projectOpsLength =
- await ProjectHistoryRedisManager.promises.queueRenameEntity(
- projectId,
- projectHistoryId,
- 'doc',
- update.id,
- userId,
- update,
- source
- )
- } else {
- // rename the doc in redis before queuing the update
- await DocumentManager.promises.renameDocWithLock(
- projectId,
- update.id,
- userId,
- update,
- projectHistoryId
- )
- projectOpsLength =
- await ProjectHistoryRedisManager.promises.queueRenameEntity(
- projectId,
- projectHistoryId,
- 'doc',
- update.id,
- userId,
- update,
- source
- )
- }
- break
- case 'add-file':
- projectOpsLength =
- await ProjectHistoryRedisManager.promises.queueAddEntity(
- projectId,
- projectHistoryId,
- 'file',
- update.id,
- userId,
- update,
- source
- )
- break
- case 'rename-file':
- projectOpsLength =
- await ProjectHistoryRedisManager.promises.queueRenameEntity(
- projectId,
- projectHistoryId,
- 'file',
- update.id,
- userId,
- update,
- source
- )
- break
- default:
- throw new Error(`Unknown update type: ${update.type}`)
- }
- }
- if (
- HistoryManager.shouldFlushHistoryOps(
- projectId,
- projectOpsLength,
- updates.length
- )
- ) {
- HistoryManager.flushProjectChangesAsync(projectId)
- }
- timer.done()
- }
- const ProjectManager = {
- flushProjectWithLocks,
- flushAndDeleteProjectWithLocks,
- queueFlushAndDeleteProject,
- getProjectDocsTimestamps,
- getProjectDocsAndFlushIfOld,
- getProjectRanges,
- clearProjectState,
- updateProjectWithLocks,
- }
- module.exports = {
- ...callbackifyAll(ProjectManager),
- promises: ProjectManager,
- }
|