2021-07-12 16:47:15 +00:00
|
|
|
const Settings = require('@overleaf/settings')
|
2024-01-30 15:35:54 +00:00
|
|
|
const { promisifyAll } = require('@overleaf/promise-utils')
|
2022-07-28 08:23:50 +00:00
|
|
|
const projectHistoryKeys = Settings.redis?.project_history?.key_schema
|
2020-11-10 11:32:04 +00:00
|
|
|
const rclient = require('@overleaf/redis-wrapper').createClient(
|
2020-05-06 10:09:33 +00:00
|
|
|
Settings.redis.project_history
|
|
|
|
)
|
2021-10-06 09:10:28 +00:00
|
|
|
const logger = require('@overleaf/logger')
|
2020-05-06 10:09:33 +00:00
|
|
|
const metrics = require('./Metrics')
|
2021-11-25 09:25:28 +00:00
|
|
|
const { docIsTooLarge } = require('./Limits')
|
2018-03-09 14:14:14 +00:00
|
|
|
|
2024-01-30 15:35:54 +00:00
|
|
|
const ProjectHistoryRedisManager = {
|
2022-08-25 12:01:39 +00:00
|
|
|
queueOps(projectId, ...rest) {
|
2020-05-06 10:09:33 +00:00
|
|
|
// Record metric for ops pushed onto queue
|
2021-10-27 09:49:18 +00:00
|
|
|
const callback = rest.pop()
|
|
|
|
const ops = rest
|
2022-08-25 12:01:39 +00:00
|
|
|
for (const op of ops) {
|
2020-05-06 10:09:33 +00:00
|
|
|
metrics.summary('redis.projectHistoryOps', op.length, { status: 'push' })
|
|
|
|
}
|
|
|
|
const multi = rclient.multi()
|
|
|
|
// Push the ops onto the project history queue
|
|
|
|
multi.rpush(
|
2022-08-25 12:01:39 +00:00
|
|
|
projectHistoryKeys.projectHistoryOps({ project_id: projectId }),
|
|
|
|
...ops
|
2020-05-06 10:09:33 +00:00
|
|
|
)
|
|
|
|
// To record the age of the oldest op on the queue set a timestamp if not
|
|
|
|
// already present (SETNX).
|
|
|
|
multi.setnx(
|
2022-08-25 12:01:39 +00:00
|
|
|
projectHistoryKeys.projectHistoryFirstOpTimestamp({
|
|
|
|
project_id: projectId,
|
|
|
|
}),
|
2020-05-06 10:09:33 +00:00
|
|
|
Date.now()
|
|
|
|
)
|
2022-08-25 12:01:39 +00:00
|
|
|
multi.exec(function (error, result) {
|
|
|
|
if (error) {
|
2020-05-06 10:09:33 +00:00
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
// return the number of entries pushed onto the project history queue
|
2022-08-25 12:01:39 +00:00
|
|
|
callback(null, result[0])
|
2020-05-06 10:09:33 +00:00
|
|
|
})
|
|
|
|
},
|
2018-07-20 09:43:31 +00:00
|
|
|
|
2020-05-06 10:09:33 +00:00
|
|
|
queueRenameEntity(
|
2022-08-25 12:01:39 +00:00
|
|
|
projectId,
|
2020-05-06 10:09:33 +00:00
|
|
|
projectHistoryId,
|
2022-08-25 12:01:39 +00:00
|
|
|
entityType,
|
|
|
|
entityId,
|
|
|
|
userId,
|
2020-05-06 10:09:33 +00:00
|
|
|
projectUpdate,
|
2022-08-25 12:01:39 +00:00
|
|
|
source,
|
2020-05-06 10:09:33 +00:00
|
|
|
callback
|
|
|
|
) {
|
|
|
|
projectUpdate = {
|
|
|
|
pathname: projectUpdate.pathname,
|
|
|
|
new_pathname: projectUpdate.newPathname,
|
|
|
|
meta: {
|
2022-08-25 12:01:39 +00:00
|
|
|
user_id: userId,
|
2021-07-13 11:04:42 +00:00
|
|
|
ts: new Date(),
|
2020-05-06 10:09:33 +00:00
|
|
|
},
|
|
|
|
version: projectUpdate.version,
|
2021-07-13 11:04:42 +00:00
|
|
|
projectHistoryId,
|
2020-05-06 10:09:33 +00:00
|
|
|
}
|
2022-08-25 12:01:39 +00:00
|
|
|
projectUpdate[entityType] = entityId
|
|
|
|
if (source != null) {
|
|
|
|
projectUpdate.meta.source = source
|
|
|
|
if (source !== 'editor') {
|
|
|
|
projectUpdate.meta.type = 'external'
|
|
|
|
}
|
|
|
|
}
|
2018-03-09 14:14:14 +00:00
|
|
|
|
2021-09-30 08:28:32 +00:00
|
|
|
logger.debug(
|
2022-08-25 12:01:39 +00:00
|
|
|
{ projectId, projectUpdate },
|
2020-05-06 10:09:33 +00:00
|
|
|
'queue rename operation to project-history'
|
|
|
|
)
|
|
|
|
const jsonUpdate = JSON.stringify(projectUpdate)
|
2018-03-09 14:14:14 +00:00
|
|
|
|
2022-08-25 12:01:39 +00:00
|
|
|
ProjectHistoryRedisManager.queueOps(projectId, jsonUpdate, callback)
|
2020-05-06 10:09:33 +00:00
|
|
|
},
|
2018-03-09 14:14:14 +00:00
|
|
|
|
2020-05-06 10:09:33 +00:00
|
|
|
queueAddEntity(
|
2022-08-25 12:01:39 +00:00
|
|
|
projectId,
|
2020-05-06 10:09:33 +00:00
|
|
|
projectHistoryId,
|
2022-08-25 12:01:39 +00:00
|
|
|
entityType,
|
|
|
|
entityId,
|
|
|
|
userId,
|
2020-05-06 10:09:33 +00:00
|
|
|
projectUpdate,
|
2022-08-25 12:01:39 +00:00
|
|
|
source,
|
2020-05-06 10:09:33 +00:00
|
|
|
callback
|
|
|
|
) {
|
|
|
|
projectUpdate = {
|
|
|
|
pathname: projectUpdate.pathname,
|
|
|
|
docLines: projectUpdate.docLines,
|
|
|
|
url: projectUpdate.url,
|
|
|
|
meta: {
|
2022-08-25 12:01:39 +00:00
|
|
|
user_id: userId,
|
2021-07-13 11:04:42 +00:00
|
|
|
ts: new Date(),
|
2020-05-06 10:09:33 +00:00
|
|
|
},
|
|
|
|
version: projectUpdate.version,
|
2021-07-13 11:04:42 +00:00
|
|
|
projectHistoryId,
|
2020-05-06 10:09:33 +00:00
|
|
|
}
|
2022-08-25 12:01:39 +00:00
|
|
|
projectUpdate[entityType] = entityId
|
|
|
|
if (source != null) {
|
|
|
|
projectUpdate.meta.source = source
|
|
|
|
if (source !== 'editor') {
|
|
|
|
projectUpdate.meta.type = 'external'
|
|
|
|
}
|
|
|
|
}
|
2018-03-09 14:14:14 +00:00
|
|
|
|
2021-09-30 08:28:32 +00:00
|
|
|
logger.debug(
|
2022-08-25 12:01:39 +00:00
|
|
|
{ projectId, projectUpdate },
|
2020-05-06 10:09:33 +00:00
|
|
|
'queue add operation to project-history'
|
|
|
|
)
|
|
|
|
const jsonUpdate = JSON.stringify(projectUpdate)
|
2018-03-09 14:14:14 +00:00
|
|
|
|
2022-08-25 12:01:39 +00:00
|
|
|
ProjectHistoryRedisManager.queueOps(projectId, jsonUpdate, callback)
|
2020-05-06 10:09:33 +00:00
|
|
|
},
|
2018-03-09 14:14:14 +00:00
|
|
|
|
2020-05-06 10:09:33 +00:00
|
|
|
queueResyncProjectStructure(
|
2022-08-25 12:01:39 +00:00
|
|
|
projectId,
|
2020-05-06 10:09:33 +00:00
|
|
|
projectHistoryId,
|
|
|
|
docs,
|
|
|
|
files,
|
|
|
|
callback
|
|
|
|
) {
|
2022-08-25 12:01:39 +00:00
|
|
|
logger.debug({ projectId, docs, files }, 'queue project structure resync')
|
2020-05-06 10:09:33 +00:00
|
|
|
const projectUpdate = {
|
|
|
|
resyncProjectStructure: { docs, files },
|
|
|
|
projectHistoryId,
|
|
|
|
meta: {
|
2021-07-13 11:04:42 +00:00
|
|
|
ts: new Date(),
|
|
|
|
},
|
2020-05-06 10:09:33 +00:00
|
|
|
}
|
|
|
|
const jsonUpdate = JSON.stringify(projectUpdate)
|
2022-08-25 12:01:39 +00:00
|
|
|
ProjectHistoryRedisManager.queueOps(projectId, jsonUpdate, callback)
|
2020-05-06 10:09:33 +00:00
|
|
|
},
|
2018-03-09 14:14:14 +00:00
|
|
|
|
2020-05-06 10:09:33 +00:00
|
|
|
queueResyncDocContent(
|
2022-08-25 12:01:39 +00:00
|
|
|
projectId,
|
2020-05-06 10:09:33 +00:00
|
|
|
projectHistoryId,
|
2022-08-25 12:01:39 +00:00
|
|
|
docId,
|
2020-05-06 10:09:33 +00:00
|
|
|
lines,
|
|
|
|
version,
|
|
|
|
pathname,
|
|
|
|
callback
|
|
|
|
) {
|
2021-09-30 08:28:32 +00:00
|
|
|
logger.debug(
|
2022-08-25 12:01:39 +00:00
|
|
|
{ projectId, docId, lines, version, pathname },
|
2020-05-06 10:09:33 +00:00
|
|
|
'queue doc content resync'
|
|
|
|
)
|
|
|
|
const projectUpdate = {
|
|
|
|
resyncDocContent: {
|
|
|
|
content: lines.join('\n'),
|
2021-07-13 11:04:42 +00:00
|
|
|
version,
|
2020-05-06 10:09:33 +00:00
|
|
|
},
|
|
|
|
projectHistoryId,
|
|
|
|
path: pathname,
|
2022-08-25 12:01:39 +00:00
|
|
|
doc: docId,
|
2020-05-06 10:09:33 +00:00
|
|
|
meta: {
|
2021-07-13 11:04:42 +00:00
|
|
|
ts: new Date(),
|
|
|
|
},
|
2020-05-06 10:09:33 +00:00
|
|
|
}
|
|
|
|
const jsonUpdate = JSON.stringify(projectUpdate)
|
2021-11-25 09:25:28 +00:00
|
|
|
// Do an optimised size check on the docLines using the serialised
|
|
|
|
// project update length as an upper bound
|
|
|
|
const sizeBound = jsonUpdate.length
|
|
|
|
if (docIsTooLarge(sizeBound, lines, Settings.max_doc_length)) {
|
|
|
|
const err = new Error(
|
|
|
|
'blocking resync doc content insert into project history queue: doc is too large'
|
|
|
|
)
|
2022-08-25 12:01:39 +00:00
|
|
|
logger.error({ projectId, docId, err, docSize: sizeBound }, err.message)
|
2021-11-25 09:25:28 +00:00
|
|
|
return callback(err)
|
|
|
|
}
|
2022-08-25 12:01:39 +00:00
|
|
|
ProjectHistoryRedisManager.queueOps(projectId, jsonUpdate, callback)
|
2021-07-13 11:04:42 +00:00
|
|
|
},
|
2020-05-06 10:09:33 +00:00
|
|
|
}
|
2024-01-30 15:35:54 +00:00
|
|
|
|
|
|
|
module.exports = ProjectHistoryRedisManager
|
|
|
|
module.exports.promises = promisifyAll(ProjectHistoryRedisManager)
|