mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #18906 from overleaf/em-migrate-existing-histories-2
History ranges migration script - second attempt GitOrigin-RevId: 60a2c04e2a72e76a58e9e179fefc4186a96fde32
This commit is contained in:
parent
9f0f42a012
commit
e73fdfba63
28 changed files with 379 additions and 187 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
const { callbackify, promisify } = require('util')
|
||||||
const metrics = require('@overleaf/metrics')
|
const metrics = require('@overleaf/metrics')
|
||||||
const logger = require('@overleaf/logger')
|
const logger = require('@overleaf/logger')
|
||||||
const os = require('os')
|
const os = require('os')
|
||||||
|
@ -31,6 +32,14 @@ module.exports = class RedisWebLocker {
|
||||||
this.SLOW_EXECUTION_THRESHOLD = options.slowExecutionThreshold || 5000
|
this.SLOW_EXECUTION_THRESHOLD = options.slowExecutionThreshold || 5000
|
||||||
// read-only copy for unit tests
|
// read-only copy for unit tests
|
||||||
this.unlockScript = UNLOCK_SCRIPT
|
this.unlockScript = UNLOCK_SCRIPT
|
||||||
|
|
||||||
|
const promisifiedRunWithLock = promisify(this.runWithLock).bind(this)
|
||||||
|
this.promises = {
|
||||||
|
runWithLock(namespace, id, runner) {
|
||||||
|
const cbRunner = callbackify(runner)
|
||||||
|
return promisifiedRunWithLock(namespace, id, cbRunner)
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use a signed lock value as described in
|
// Use a signed lock value as described in
|
||||||
|
|
|
@ -384,7 +384,7 @@ const DocumentManager = {
|
||||||
return { lines, version }
|
return { lines, version }
|
||||||
},
|
},
|
||||||
|
|
||||||
async resyncDocContents(projectId, docId, path) {
|
async resyncDocContents(projectId, docId, path, opts = {}) {
|
||||||
logger.debug({ projectId, docId, path }, 'start resyncing doc contents')
|
logger.debug({ projectId, docId, path }, 'start resyncing doc contents')
|
||||||
let {
|
let {
|
||||||
lines,
|
lines,
|
||||||
|
@ -422,6 +422,10 @@ const DocumentManager = {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.historyRangesMigration) {
|
||||||
|
historyRangesSupport = opts.historyRangesMigration === 'forwards'
|
||||||
|
}
|
||||||
|
|
||||||
await ProjectHistoryRedisManager.promises.queueResyncDocContent(
|
await ProjectHistoryRedisManager.promises.queueResyncDocContent(
|
||||||
projectId,
|
projectId,
|
||||||
projectHistoryId,
|
projectHistoryId,
|
||||||
|
@ -434,6 +438,13 @@ const DocumentManager = {
|
||||||
path,
|
path,
|
||||||
historyRangesSupport
|
historyRangesSupport
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (opts.historyRangesMigration) {
|
||||||
|
await RedisManager.promises.setHistoryRangesSupportFlag(
|
||||||
|
docId,
|
||||||
|
historyRangesSupport
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getDocWithLock(projectId, docId) {
|
async getDocWithLock(projectId, docId) {
|
||||||
|
@ -547,14 +558,14 @@ const DocumentManager = {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
async resyncDocContentsWithLock(projectId, docId, path, callback) {
|
async resyncDocContentsWithLock(projectId, docId, path, opts) {
|
||||||
const UpdateManager = require('./UpdateManager')
|
const UpdateManager = require('./UpdateManager')
|
||||||
await UpdateManager.promises.lockUpdatesAndDo(
|
await UpdateManager.promises.lockUpdatesAndDo(
|
||||||
DocumentManager.resyncDocContents,
|
DocumentManager.resyncDocContents,
|
||||||
projectId,
|
projectId,
|
||||||
docId,
|
docId,
|
||||||
path,
|
path,
|
||||||
callback
|
opts
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,14 @@ const HistoryManager = {
|
||||||
|
|
||||||
MAX_PARALLEL_REQUESTS: 4,
|
MAX_PARALLEL_REQUESTS: 4,
|
||||||
|
|
||||||
resyncProjectHistory(projectId, projectHistoryId, docs, files, callback) {
|
resyncProjectHistory(
|
||||||
|
projectId,
|
||||||
|
projectHistoryId,
|
||||||
|
docs,
|
||||||
|
files,
|
||||||
|
opts,
|
||||||
|
callback
|
||||||
|
) {
|
||||||
ProjectHistoryRedisManager.queueResyncProjectStructure(
|
ProjectHistoryRedisManager.queueResyncProjectStructure(
|
||||||
projectId,
|
projectId,
|
||||||
projectHistoryId,
|
projectHistoryId,
|
||||||
|
@ -109,6 +116,7 @@ const HistoryManager = {
|
||||||
projectId,
|
projectId,
|
||||||
doc.doc,
|
doc.doc,
|
||||||
doc.path,
|
doc.path,
|
||||||
|
opts,
|
||||||
cb
|
cb
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,27 +11,6 @@ const DeleteQueueManager = require('./DeleteQueueManager')
|
||||||
const { getTotalSizeOfLines } = require('./Limits')
|
const { getTotalSizeOfLines } = require('./Limits')
|
||||||
const async = require('async')
|
const async = require('async')
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getDoc,
|
|
||||||
peekDoc,
|
|
||||||
getProjectDocsAndFlushIfOld,
|
|
||||||
clearProjectState,
|
|
||||||
setDoc,
|
|
||||||
flushDocIfLoaded,
|
|
||||||
deleteDoc,
|
|
||||||
flushProject,
|
|
||||||
deleteProject,
|
|
||||||
deleteMultipleProjects,
|
|
||||||
acceptChanges,
|
|
||||||
resolveComment,
|
|
||||||
reopenComment,
|
|
||||||
deleteComment,
|
|
||||||
updateProject,
|
|
||||||
resyncProjectHistory,
|
|
||||||
flushAllProjects,
|
|
||||||
flushQueuedProjects,
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDoc(req, res, next) {
|
function getDoc(req, res, next) {
|
||||||
let fromVersion
|
let fromVersion
|
||||||
const docId = req.params.doc_id
|
const docId = req.params.doc_id
|
||||||
|
@ -401,17 +380,24 @@ function updateProject(req, res, next) {
|
||||||
|
|
||||||
function resyncProjectHistory(req, res, next) {
|
function resyncProjectHistory(req, res, next) {
|
||||||
const projectId = req.params.project_id
|
const projectId = req.params.project_id
|
||||||
const { projectHistoryId, docs, files } = req.body
|
const { projectHistoryId, docs, files, historyRangesMigration } = req.body
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
{ projectId, docs, files },
|
{ projectId, docs, files },
|
||||||
'queuing project history resync via http'
|
'queuing project history resync via http'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const opts = {}
|
||||||
|
if (historyRangesMigration) {
|
||||||
|
opts.historyRangesMigration = historyRangesMigration
|
||||||
|
}
|
||||||
|
|
||||||
HistoryManager.resyncProjectHistory(
|
HistoryManager.resyncProjectHistory(
|
||||||
projectId,
|
projectId,
|
||||||
projectHistoryId,
|
projectHistoryId,
|
||||||
docs,
|
docs,
|
||||||
files,
|
files,
|
||||||
|
opts,
|
||||||
error => {
|
error => {
|
||||||
if (error) {
|
if (error) {
|
||||||
return next(error)
|
return next(error)
|
||||||
|
@ -456,3 +442,24 @@ function flushQueuedProjects(req, res, next) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getDoc,
|
||||||
|
peekDoc,
|
||||||
|
getProjectDocsAndFlushIfOld,
|
||||||
|
clearProjectState,
|
||||||
|
setDoc,
|
||||||
|
flushDocIfLoaded,
|
||||||
|
deleteDoc,
|
||||||
|
flushProject,
|
||||||
|
deleteProject,
|
||||||
|
deleteMultipleProjects,
|
||||||
|
acceptChanges,
|
||||||
|
resolveComment,
|
||||||
|
reopenComment,
|
||||||
|
deleteComment,
|
||||||
|
updateProject,
|
||||||
|
resyncProjectHistory,
|
||||||
|
flushAllProjects,
|
||||||
|
flushQueuedProjects,
|
||||||
|
}
|
||||||
|
|
|
@ -8,18 +8,6 @@ const Metrics = require('./Metrics')
|
||||||
const Errors = require('./Errors')
|
const Errors = require('./Errors')
|
||||||
const { promisifyAll } = require('@overleaf/promise-utils')
|
const { promisifyAll } = require('@overleaf/promise-utils')
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
flushProjectWithLocks,
|
|
||||||
flushAndDeleteProjectWithLocks,
|
|
||||||
queueFlushAndDeleteProject,
|
|
||||||
getProjectDocsTimestamps,
|
|
||||||
getProjectDocsAndFlushIfOld,
|
|
||||||
clearProjectState,
|
|
||||||
updateProjectWithLocks,
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.promises = promisifyAll(module.exports)
|
|
||||||
|
|
||||||
function flushProjectWithLocks(projectId, _callback) {
|
function flushProjectWithLocks(projectId, _callback) {
|
||||||
const timer = new Metrics.Timer('projectManager.flushProjectWithLocks')
|
const timer = new Metrics.Timer('projectManager.flushProjectWithLocks')
|
||||||
const callback = function (...args) {
|
const callback = function (...args) {
|
||||||
|
@ -339,3 +327,15 @@ function updateProjectWithLocks(
|
||||||
callback()
|
callback()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
flushProjectWithLocks,
|
||||||
|
flushAndDeleteProjectWithLocks,
|
||||||
|
queueFlushAndDeleteProject,
|
||||||
|
getProjectDocsTimestamps,
|
||||||
|
getProjectDocsAndFlushIfOld,
|
||||||
|
clearProjectState,
|
||||||
|
updateProjectWithLocks,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.promises = promisifyAll(module.exports)
|
||||||
|
|
|
@ -77,66 +77,70 @@ const RedisManager = {
|
||||||
logger.error({ err: error, docId, projectId }, error.message)
|
logger.error({ err: error, docId, projectId }, error.message)
|
||||||
return callback(error)
|
return callback(error)
|
||||||
}
|
}
|
||||||
setHistoryRangesSupportFlag(docId, historyRangesSupport, err => {
|
RedisManager.setHistoryRangesSupportFlag(
|
||||||
if (err) {
|
docId,
|
||||||
return callback(err)
|
historyRangesSupport,
|
||||||
}
|
err => {
|
||||||
// update docsInProject set before writing doc contents
|
if (err) {
|
||||||
rclient.sadd(
|
return callback(err)
|
||||||
keys.docsInProject({ project_id: projectId }),
|
}
|
||||||
docId,
|
// update docsInProject set before writing doc contents
|
||||||
error => {
|
rclient.sadd(
|
||||||
if (error) return callback(error)
|
keys.docsInProject({ project_id: projectId }),
|
||||||
|
docId,
|
||||||
|
error => {
|
||||||
|
if (error) return callback(error)
|
||||||
|
|
||||||
if (!pathname) {
|
if (!pathname) {
|
||||||
metrics.inc('pathname', 1, {
|
metrics.inc('pathname', 1, {
|
||||||
path: 'RedisManager.setDoc',
|
path: 'RedisManager.setDoc',
|
||||||
status: pathname === '' ? 'zero-length' : 'undefined',
|
status: pathname === '' ? 'zero-length' : 'undefined',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that this MULTI operation only operates on doc
|
||||||
|
// specific keys, i.e. keys that have the doc id in curly braces.
|
||||||
|
// The curly braces identify a hash key for Redis and ensures that
|
||||||
|
// the MULTI's operations are all done on the same node in a
|
||||||
|
// cluster environment.
|
||||||
|
const multi = rclient.multi()
|
||||||
|
multi.mset({
|
||||||
|
[keys.docLines({ doc_id: docId })]: docLines,
|
||||||
|
[keys.projectKey({ doc_id: docId })]: projectId,
|
||||||
|
[keys.docVersion({ doc_id: docId })]: version,
|
||||||
|
[keys.docHash({ doc_id: docId })]: docHash,
|
||||||
|
[keys.ranges({ doc_id: docId })]: ranges,
|
||||||
|
[keys.pathname({ doc_id: docId })]: pathname,
|
||||||
|
[keys.projectHistoryId({ doc_id: docId })]: projectHistoryId,
|
||||||
|
})
|
||||||
|
if (historyRangesSupport) {
|
||||||
|
multi.del(keys.resolvedCommentIds({ doc_id: docId }))
|
||||||
|
if (resolvedCommentIds.length > 0) {
|
||||||
|
multi.sadd(
|
||||||
|
keys.resolvedCommentIds({ doc_id: docId }),
|
||||||
|
...resolvedCommentIds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
multi.exec(err => {
|
||||||
|
if (err) {
|
||||||
|
callback(
|
||||||
|
OError.tag(err, 'failed to write doc to Redis in MULTI', {
|
||||||
|
previousErrors: err.previousErrors.map(e => ({
|
||||||
|
name: e.name,
|
||||||
|
message: e.message,
|
||||||
|
command: e.command,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
)
|
||||||
// Make sure that this MULTI operation only operates on doc
|
}
|
||||||
// specific keys, i.e. keys that have the doc id in curly braces.
|
)
|
||||||
// The curly braces identify a hash key for Redis and ensures that
|
|
||||||
// the MULTI's operations are all done on the same node in a
|
|
||||||
// cluster environment.
|
|
||||||
const multi = rclient.multi()
|
|
||||||
multi.mset({
|
|
||||||
[keys.docLines({ doc_id: docId })]: docLines,
|
|
||||||
[keys.projectKey({ doc_id: docId })]: projectId,
|
|
||||||
[keys.docVersion({ doc_id: docId })]: version,
|
|
||||||
[keys.docHash({ doc_id: docId })]: docHash,
|
|
||||||
[keys.ranges({ doc_id: docId })]: ranges,
|
|
||||||
[keys.pathname({ doc_id: docId })]: pathname,
|
|
||||||
[keys.projectHistoryId({ doc_id: docId })]: projectHistoryId,
|
|
||||||
})
|
|
||||||
if (historyRangesSupport) {
|
|
||||||
multi.del(keys.resolvedCommentIds({ doc_id: docId }))
|
|
||||||
if (resolvedCommentIds.length > 0) {
|
|
||||||
multi.sadd(
|
|
||||||
keys.resolvedCommentIds({ doc_id: docId }),
|
|
||||||
...resolvedCommentIds
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
multi.exec(err => {
|
|
||||||
if (err) {
|
|
||||||
callback(
|
|
||||||
OError.tag(err, 'failed to write doc to Redis in MULTI', {
|
|
||||||
previousErrors: err.previousErrors.map(e => ({
|
|
||||||
name: e.name,
|
|
||||||
message: e.message,
|
|
||||||
command: e.command,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -672,6 +676,14 @@ const RedisManager = {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setHistoryRangesSupportFlag(docId, historyRangesSupport, callback) {
|
||||||
|
if (historyRangesSupport) {
|
||||||
|
rclient.sadd(keys.historyRangesSupport(), docId, callback)
|
||||||
|
} else {
|
||||||
|
rclient.srem(keys.historyRangesSupport(), docId, callback)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_serializeRanges(ranges, callback) {
|
_serializeRanges(ranges, callback) {
|
||||||
let jsonRanges = JSON.stringify(ranges)
|
let jsonRanges = JSON.stringify(ranges)
|
||||||
if (jsonRanges && jsonRanges.length > MAX_RANGES_SIZE) {
|
if (jsonRanges && jsonRanges.length > MAX_RANGES_SIZE) {
|
||||||
|
@ -701,14 +713,6 @@ const RedisManager = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function setHistoryRangesSupportFlag(docId, historyRangesSupport, callback) {
|
|
||||||
if (historyRangesSupport) {
|
|
||||||
rclient.sadd(keys.historyRangesSupport(), docId, callback)
|
|
||||||
} else {
|
|
||||||
rclient.srem(keys.historyRangesSupport(), docId, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = RedisManager
|
module.exports = RedisManager
|
||||||
module.exports.promises = promisifyAll(RedisManager, {
|
module.exports.promises = promisifyAll(RedisManager, {
|
||||||
without: ['_deserializeRanges', '_computeHash'],
|
without: ['_deserializeRanges', '_computeHash'],
|
||||||
|
|
|
@ -1066,7 +1066,7 @@ describe('HttpController', function () {
|
||||||
|
|
||||||
describe('successfully', function () {
|
describe('successfully', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(4)
|
this.HistoryManager.resyncProjectHistory = sinon.stub().callsArgWith(5)
|
||||||
this.HttpController.resyncProjectHistory(this.req, this.res, this.next)
|
this.HttpController.resyncProjectHistory(this.req, this.res, this.next)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1076,13 +1076,14 @@ describe('HttpController', function () {
|
||||||
this.project_id,
|
this.project_id,
|
||||||
this.projectHistoryId,
|
this.projectHistoryId,
|
||||||
this.docs,
|
this.docs,
|
||||||
this.files
|
this.files,
|
||||||
|
{}
|
||||||
)
|
)
|
||||||
.should.equal(true)
|
.should.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return a successful No Content response', function () {
|
it('should return a successful No Content response', function () {
|
||||||
this.res.sendStatus.calledWith(204).should.equal(true)
|
this.res.sendStatus.should.have.been.calledWith(204)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1090,7 +1091,7 @@ describe('HttpController', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.HistoryManager.resyncProjectHistory = sinon
|
this.HistoryManager.resyncProjectHistory = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.callsArgWith(4, new Error('oops'))
|
.callsArgWith(5, new Error('oops'))
|
||||||
this.HttpController.resyncProjectHistory(this.req, this.res, this.next)
|
this.HttpController.resyncProjectHistory(this.req, this.res, this.next)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,10 @@ const SandboxedModule = require('sandboxed-module')
|
||||||
describe('ProjectManager - flushAndDeleteProject', function () {
|
describe('ProjectManager - flushAndDeleteProject', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
let Timer
|
let Timer
|
||||||
|
this.LockManager = {
|
||||||
|
getLock: sinon.stub().yields(),
|
||||||
|
releaseLock: sinon.stub().yields(),
|
||||||
|
}
|
||||||
this.ProjectManager = SandboxedModule.require(modulePath, {
|
this.ProjectManager = SandboxedModule.require(modulePath, {
|
||||||
requires: {
|
requires: {
|
||||||
'./RedisManager': (this.RedisManager = {}),
|
'./RedisManager': (this.RedisManager = {}),
|
||||||
|
@ -26,6 +30,7 @@ describe('ProjectManager - flushAndDeleteProject', function () {
|
||||||
'./HistoryManager': (this.HistoryManager = {
|
'./HistoryManager': (this.HistoryManager = {
|
||||||
flushProjectChanges: sinon.stub().callsArg(2),
|
flushProjectChanges: sinon.stub().callsArg(2),
|
||||||
}),
|
}),
|
||||||
|
'./LockManager': this.LockManager,
|
||||||
'./Metrics': (this.Metrics = {
|
'./Metrics': (this.Metrics = {
|
||||||
Timer: (Timer = (function () {
|
Timer: (Timer = (function () {
|
||||||
Timer = class Timer {
|
Timer = class Timer {
|
||||||
|
|
|
@ -19,12 +19,17 @@ const SandboxedModule = require('sandboxed-module')
|
||||||
describe('ProjectManager - flushProject', function () {
|
describe('ProjectManager - flushProject', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
let Timer
|
let Timer
|
||||||
|
this.LockManager = {
|
||||||
|
getLock: sinon.stub().yields(),
|
||||||
|
releaseLock: sinon.stub().yields(),
|
||||||
|
}
|
||||||
this.ProjectManager = SandboxedModule.require(modulePath, {
|
this.ProjectManager = SandboxedModule.require(modulePath, {
|
||||||
requires: {
|
requires: {
|
||||||
'./RedisManager': (this.RedisManager = {}),
|
'./RedisManager': (this.RedisManager = {}),
|
||||||
'./ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}),
|
'./ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}),
|
||||||
'./DocumentManager': (this.DocumentManager = {}),
|
'./DocumentManager': (this.DocumentManager = {}),
|
||||||
'./HistoryManager': (this.HistoryManager = {}),
|
'./HistoryManager': (this.HistoryManager = {}),
|
||||||
|
'./LockManager': this.LockManager,
|
||||||
'./Metrics': (this.Metrics = {
|
'./Metrics': (this.Metrics = {
|
||||||
Timer: (Timer = (function () {
|
Timer: (Timer = (function () {
|
||||||
Timer = class Timer {
|
Timer = class Timer {
|
||||||
|
|
|
@ -18,12 +18,17 @@ const Errors = require('../../../../app/js/Errors.js')
|
||||||
describe('ProjectManager - getProjectDocsAndFlushIfOld', function () {
|
describe('ProjectManager - getProjectDocsAndFlushIfOld', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
let Timer
|
let Timer
|
||||||
|
this.LockManager = {
|
||||||
|
getLock: sinon.stub().yields(),
|
||||||
|
releaseLock: sinon.stub().yields(),
|
||||||
|
}
|
||||||
this.ProjectManager = SandboxedModule.require(modulePath, {
|
this.ProjectManager = SandboxedModule.require(modulePath, {
|
||||||
requires: {
|
requires: {
|
||||||
'./RedisManager': (this.RedisManager = {}),
|
'./RedisManager': (this.RedisManager = {}),
|
||||||
'./ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}),
|
'./ProjectHistoryRedisManager': (this.ProjectHistoryRedisManager = {}),
|
||||||
'./DocumentManager': (this.DocumentManager = {}),
|
'./DocumentManager': (this.DocumentManager = {}),
|
||||||
'./HistoryManager': (this.HistoryManager = {}),
|
'./HistoryManager': (this.HistoryManager = {}),
|
||||||
|
'./LockManager': this.LockManager,
|
||||||
'./Metrics': (this.Metrics = {
|
'./Metrics': (this.Metrics = {
|
||||||
Timer: (Timer = (function () {
|
Timer: (Timer = (function () {
|
||||||
Timer = class Timer {
|
Timer = class Timer {
|
||||||
|
|
|
@ -17,6 +17,10 @@ describe('ProjectManager', function () {
|
||||||
flushProjectChangesAsync: sinon.stub(),
|
flushProjectChangesAsync: sinon.stub(),
|
||||||
shouldFlushHistoryOps: sinon.stub().returns(false),
|
shouldFlushHistoryOps: sinon.stub().returns(false),
|
||||||
}
|
}
|
||||||
|
this.LockManager = {
|
||||||
|
getLock: sinon.stub().yields(),
|
||||||
|
releaseLock: sinon.stub().yields(),
|
||||||
|
}
|
||||||
this.Metrics = {
|
this.Metrics = {
|
||||||
Timer: class Timer {},
|
Timer: class Timer {},
|
||||||
}
|
}
|
||||||
|
@ -28,6 +32,7 @@ describe('ProjectManager', function () {
|
||||||
'./ProjectHistoryRedisManager': this.ProjectHistoryRedisManager,
|
'./ProjectHistoryRedisManager': this.ProjectHistoryRedisManager,
|
||||||
'./DocumentManager': this.DocumentManager,
|
'./DocumentManager': this.DocumentManager,
|
||||||
'./HistoryManager': this.HistoryManager,
|
'./HistoryManager': this.HistoryManager,
|
||||||
|
'./LockManager': this.LockManager,
|
||||||
'./Metrics': this.Metrics,
|
'./Metrics': this.Metrics,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -258,6 +258,9 @@ export function resyncProject(req, res, next) {
|
||||||
if (req.body.origin) {
|
if (req.body.origin) {
|
||||||
options.origin = req.body.origin
|
options.origin = req.body.origin
|
||||||
}
|
}
|
||||||
|
if (req.body.historyRangesMigration) {
|
||||||
|
options.historyRangesMigration = req.body.historyRangesMigration
|
||||||
|
}
|
||||||
if (req.query.force || req.body.force) {
|
if (req.query.force || req.body.force) {
|
||||||
// this will delete the queue and clear the sync state
|
// this will delete the queue and clear the sync state
|
||||||
// use if the project is completely broken
|
// use if the project is completely broken
|
||||||
|
|
|
@ -79,6 +79,9 @@ export function initialize(app) {
|
||||||
origin: Joi.object({
|
origin: Joi.object({
|
||||||
kind: Joi.string().required(),
|
kind: Joi.string().required(),
|
||||||
}),
|
}),
|
||||||
|
historyRangesMigration: Joi.string()
|
||||||
|
.optional()
|
||||||
|
.valid('forwards', 'backwards'),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
HttpController.resyncProject
|
HttpController.resyncProject
|
||||||
|
|
|
@ -103,7 +103,11 @@ async function _startResyncWithoutLock(projectId, options) {
|
||||||
syncState.setOrigin(options.origin || { kind: 'history-resync' })
|
syncState.setOrigin(options.origin || { kind: 'history-resync' })
|
||||||
syncState.startProjectStructureSync()
|
syncState.startProjectStructureSync()
|
||||||
|
|
||||||
await WebApiManager.promises.requestResync(projectId)
|
const webOpts = {}
|
||||||
|
if (options.historyRangesMigration) {
|
||||||
|
webOpts.historyRangesMigration = options.historyRangesMigration
|
||||||
|
}
|
||||||
|
await WebApiManager.promises.requestResync(projectId, webOpts)
|
||||||
await setResyncState(projectId, syncState)
|
await setResyncState(projectId, syncState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,12 @@ async function getHistoryId(projectId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestResync(projectId) {
|
async function requestResync(projectId, opts = {}) {
|
||||||
try {
|
try {
|
||||||
|
const body = {}
|
||||||
|
if (opts.historyRangesMigration) {
|
||||||
|
body.historyRangesMigration = opts.historyRangesMigration
|
||||||
|
}
|
||||||
await fetchNothing(
|
await fetchNothing(
|
||||||
`${Settings.apis.web.url}/project/${projectId}/history/resync`,
|
`${Settings.apis.web.url}/project/${projectId}/history/resync`,
|
||||||
{
|
{
|
||||||
|
@ -44,6 +48,7 @@ async function requestResync(projectId) {
|
||||||
user: Settings.apis.web.user,
|
user: Settings.apis.web.user,
|
||||||
password: Settings.apis.web.pass,
|
password: Settings.apis.web.pass,
|
||||||
},
|
},
|
||||||
|
json: body,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -83,6 +83,10 @@ function getAllDeletedDocs(projectId, callback) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} projectId
|
||||||
|
* @param {Callback} callback
|
||||||
|
*/
|
||||||
function getAllRanges(projectId, callback) {
|
function getAllRanges(projectId, callback) {
|
||||||
const url = `${settings.apis.docstore.url}/project/${projectId}/ranges`
|
const url = `${settings.apis.docstore.url}/project/${projectId}/ranges`
|
||||||
request.get(
|
request.get(
|
||||||
|
|
|
@ -9,46 +9,6 @@ const { promisify } = require('util')
|
||||||
const { promisifyMultiResult } = require('@overleaf/promise-utils')
|
const { promisifyMultiResult } = require('@overleaf/promise-utils')
|
||||||
const ProjectGetter = require('../Project/ProjectGetter')
|
const ProjectGetter = require('../Project/ProjectGetter')
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
flushProjectToMongo,
|
|
||||||
flushMultipleProjectsToMongo,
|
|
||||||
flushProjectToMongoAndDelete,
|
|
||||||
flushDocToMongo,
|
|
||||||
deleteDoc,
|
|
||||||
getDocument,
|
|
||||||
setDocument,
|
|
||||||
getProjectDocsIfMatch,
|
|
||||||
clearProjectState,
|
|
||||||
acceptChanges,
|
|
||||||
resolveThread,
|
|
||||||
reopenThread,
|
|
||||||
deleteThread,
|
|
||||||
resyncProjectHistory,
|
|
||||||
updateProjectStructure,
|
|
||||||
promises: {
|
|
||||||
flushProjectToMongo: promisify(flushProjectToMongo),
|
|
||||||
flushMultipleProjectsToMongo: promisify(flushMultipleProjectsToMongo),
|
|
||||||
flushProjectToMongoAndDelete: promisify(flushProjectToMongoAndDelete),
|
|
||||||
flushDocToMongo: promisify(flushDocToMongo),
|
|
||||||
deleteDoc: promisify(deleteDoc),
|
|
||||||
getDocument: promisifyMultiResult(getDocument, [
|
|
||||||
'lines',
|
|
||||||
'version',
|
|
||||||
'ranges',
|
|
||||||
'ops',
|
|
||||||
]),
|
|
||||||
setDocument: promisify(setDocument),
|
|
||||||
getProjectDocsIfMatch: promisify(getProjectDocsIfMatch),
|
|
||||||
clearProjectState: promisify(clearProjectState),
|
|
||||||
acceptChanges: promisify(acceptChanges),
|
|
||||||
resolveThread: promisify(resolveThread),
|
|
||||||
reopenThread: promisify(reopenThread),
|
|
||||||
deleteThread: promisify(deleteThread),
|
|
||||||
resyncProjectHistory: promisify(resyncProjectHistory),
|
|
||||||
updateProjectStructure: promisify(updateProjectStructure),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function flushProjectToMongo(projectId, callback) {
|
function flushProjectToMongo(projectId, callback) {
|
||||||
_makeRequest(
|
_makeRequest(
|
||||||
{
|
{
|
||||||
|
@ -267,12 +227,17 @@ function resyncProjectHistory(
|
||||||
projectHistoryId,
|
projectHistoryId,
|
||||||
docs,
|
docs,
|
||||||
files,
|
files,
|
||||||
|
opts,
|
||||||
callback
|
callback
|
||||||
) {
|
) {
|
||||||
|
const body = { docs, files, projectHistoryId }
|
||||||
|
if (opts.historyRangesMigration) {
|
||||||
|
body.historyRangesMigration = opts.historyRangesMigration
|
||||||
|
}
|
||||||
_makeRequest(
|
_makeRequest(
|
||||||
{
|
{
|
||||||
path: `/project/${projectId}/history/resync`,
|
path: `/project/${projectId}/history/resync`,
|
||||||
json: { docs, files, projectHistoryId },
|
json: body,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
timeout: 6 * 60 * 1000, // allow 6 minutes for resync
|
timeout: 6 * 60 * 1000, // allow 6 minutes for resync
|
||||||
},
|
},
|
||||||
|
@ -479,3 +444,43 @@ function _getUpdates(
|
||||||
|
|
||||||
return { deletes, adds, renames }
|
return { deletes, adds, renames }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
flushProjectToMongo,
|
||||||
|
flushMultipleProjectsToMongo,
|
||||||
|
flushProjectToMongoAndDelete,
|
||||||
|
flushDocToMongo,
|
||||||
|
deleteDoc,
|
||||||
|
getDocument,
|
||||||
|
setDocument,
|
||||||
|
getProjectDocsIfMatch,
|
||||||
|
clearProjectState,
|
||||||
|
acceptChanges,
|
||||||
|
resolveThread,
|
||||||
|
reopenThread,
|
||||||
|
deleteThread,
|
||||||
|
resyncProjectHistory,
|
||||||
|
updateProjectStructure,
|
||||||
|
promises: {
|
||||||
|
flushProjectToMongo: promisify(flushProjectToMongo),
|
||||||
|
flushMultipleProjectsToMongo: promisify(flushMultipleProjectsToMongo),
|
||||||
|
flushProjectToMongoAndDelete: promisify(flushProjectToMongoAndDelete),
|
||||||
|
flushDocToMongo: promisify(flushDocToMongo),
|
||||||
|
deleteDoc: promisify(deleteDoc),
|
||||||
|
getDocument: promisifyMultiResult(getDocument, [
|
||||||
|
'lines',
|
||||||
|
'version',
|
||||||
|
'ranges',
|
||||||
|
'ops',
|
||||||
|
]),
|
||||||
|
setDocument: promisify(setDocument),
|
||||||
|
getProjectDocsIfMatch: promisify(getProjectDocsIfMatch),
|
||||||
|
clearProjectState: promisify(clearProjectState),
|
||||||
|
acceptChanges: promisify(acceptChanges),
|
||||||
|
resolveThread: promisify(resolveThread),
|
||||||
|
reopenThread: promisify(reopenThread),
|
||||||
|
deleteThread: promisify(deleteThread),
|
||||||
|
resyncProjectHistory: promisify(resyncProjectHistory),
|
||||||
|
updateProjectStructure: promisify(updateProjectStructure),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -68,15 +68,24 @@ module.exports = HistoryController = {
|
||||||
// increase timeout to 6 minutes
|
// increase timeout to 6 minutes
|
||||||
res.setTimeout(6 * 60 * 1000)
|
res.setTimeout(6 * 60 * 1000)
|
||||||
const projectId = req.params.Project_id
|
const projectId = req.params.Project_id
|
||||||
ProjectEntityUpdateHandler.resyncProjectHistory(projectId, function (err) {
|
const opts = {}
|
||||||
if (err instanceof Errors.ProjectHistoryDisabledError) {
|
const historyRangesMigration = req.body.historyRangesMigration
|
||||||
return res.sendStatus(404)
|
if (historyRangesMigration) {
|
||||||
|
opts.historyRangesMigration = historyRangesMigration
|
||||||
|
}
|
||||||
|
ProjectEntityUpdateHandler.resyncProjectHistory(
|
||||||
|
projectId,
|
||||||
|
opts,
|
||||||
|
function (err) {
|
||||||
|
if (err instanceof Errors.ProjectHistoryDisabledError) {
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
if (err) {
|
||||||
|
return next(err)
|
||||||
|
}
|
||||||
|
res.sendStatus(204)
|
||||||
}
|
}
|
||||||
if (err) {
|
)
|
||||||
return next(err)
|
|
||||||
}
|
|
||||||
res.sendStatus(204)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
restoreFileFromV2(req, res, next) {
|
restoreFileFromV2(req, res, next) {
|
||||||
|
|
|
@ -51,6 +51,9 @@ async function resyncProject(projectId, options = {}) {
|
||||||
if (options.origin) {
|
if (options.origin) {
|
||||||
body.origin = options.origin
|
body.origin = options.origin
|
||||||
}
|
}
|
||||||
|
if (options.historyRangesMigration) {
|
||||||
|
body.historyRangesMigration = options.historyRangesMigration
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await fetchNothing(
|
await fetchNothing(
|
||||||
`${settings.apis.project_history.url}/project/${projectId}/resync`,
|
`${settings.apis.project_history.url}/project/${projectId}/resync`,
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const { callbackify } = require('util')
|
||||||
|
const HistoryManager = require('../History/HistoryManager')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate a single project
|
||||||
|
*
|
||||||
|
* @param {string} projectId
|
||||||
|
* @param {"forwards" | "backwards"} direction
|
||||||
|
*/
|
||||||
|
async function migrateProject(projectId, direction = 'forwards') {
|
||||||
|
await HistoryManager.promises.flushProject(projectId)
|
||||||
|
await HistoryManager.promises.resyncProject(projectId, {
|
||||||
|
historyRangesMigration: direction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
migrateProject: callbackify(migrateProject),
|
||||||
|
promises: { migrateProject },
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ const { Project } = require('../../models/Project')
|
||||||
const ProjectEntityHandler = require('./ProjectEntityHandler')
|
const ProjectEntityHandler = require('./ProjectEntityHandler')
|
||||||
const ProjectGetter = require('./ProjectGetter')
|
const ProjectGetter = require('./ProjectGetter')
|
||||||
const ProjectLocator = require('./ProjectLocator')
|
const ProjectLocator = require('./ProjectLocator')
|
||||||
|
const ProjectOptionsHandler = require('./ProjectOptionsHandler')
|
||||||
const ProjectUpdateHandler = require('./ProjectUpdateHandler')
|
const ProjectUpdateHandler = require('./ProjectUpdateHandler')
|
||||||
const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler')
|
const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler')
|
||||||
const SafePath = require('./SafePath')
|
const SafePath = require('./SafePath')
|
||||||
|
@ -154,6 +155,8 @@ function getDocContext(projectId, docId, callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectEntityUpdateHandler = {
|
const ProjectEntityUpdateHandler = {
|
||||||
|
LOCK_NAMESPACE,
|
||||||
|
|
||||||
updateDocLines(
|
updateDocLines(
|
||||||
projectId,
|
projectId,
|
||||||
docId,
|
docId,
|
||||||
|
@ -1394,7 +1397,7 @@ const ProjectEntityUpdateHandler = {
|
||||||
// This doesn't directly update project structure but we need to take the lock
|
// This doesn't directly update project structure but we need to take the lock
|
||||||
// to prevent anything else being queued before the resync update
|
// to prevent anything else being queued before the resync update
|
||||||
resyncProjectHistory: wrapWithLock(
|
resyncProjectHistory: wrapWithLock(
|
||||||
(projectId, callback) =>
|
(projectId, opts, callback) =>
|
||||||
ProjectGetter.getProject(
|
ProjectGetter.getProject(
|
||||||
projectId,
|
projectId,
|
||||||
{ rootFolder: true, overleaf: true },
|
{ rootFolder: true, overleaf: true },
|
||||||
|
@ -1449,7 +1452,21 @@ const ProjectEntityUpdateHandler = {
|
||||||
projectHistoryId,
|
projectHistoryId,
|
||||||
docs,
|
docs,
|
||||||
files,
|
files,
|
||||||
callback
|
opts,
|
||||||
|
err => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
if (opts.historyRangesMigration) {
|
||||||
|
ProjectOptionsHandler.setHistoryRangesSupport(
|
||||||
|
projectId,
|
||||||
|
opts.historyRangesMigration === 'forwards',
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -46,7 +46,10 @@ const ProjectHistoryHandler = {
|
||||||
|
|
||||||
await ProjectHistoryHandler.setHistoryId(projectId, historyId)
|
await ProjectHistoryHandler.setHistoryId(projectId, historyId)
|
||||||
|
|
||||||
await ProjectEntityUpdateHandler.promises.resyncProjectHistory(projectId)
|
await ProjectEntityUpdateHandler.promises.resyncProjectHistory(
|
||||||
|
projectId,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
await HistoryManager.promises.flushProject(projectId)
|
await HistoryManager.promises.flushProject(projectId)
|
||||||
},
|
},
|
||||||
|
|
|
@ -64,9 +64,11 @@ const ProjectOptionsHandler = {
|
||||||
return Project.updateOne(conditions, update, {})
|
return Project.updateOne(conditions, update, {})
|
||||||
},
|
},
|
||||||
|
|
||||||
async enableHistoryRangesSupport(projectId) {
|
async setHistoryRangesSupport(projectId, enabled) {
|
||||||
const conditions = { _id: new ObjectId(projectId) }
|
const conditions = { _id: new ObjectId(projectId) }
|
||||||
const update = { $set: { 'overleaf.history.rangesSupportEnabled': true } }
|
const update = {
|
||||||
|
$set: { 'overleaf.history.rangesSupportEnabled': enabled },
|
||||||
|
}
|
||||||
// NOTE: Updating the Mongoose model with the same query doesn't work. Maybe
|
// NOTE: Updating the Mongoose model with the same query doesn't work. Maybe
|
||||||
// because rangesSupportEnabled is not part of the schema?
|
// because rangesSupportEnabled is not part of the schema?
|
||||||
return db.projects.updateOne(conditions, update)
|
return db.projects.updateOne(conditions, update)
|
||||||
|
@ -83,8 +85,8 @@ module.exports = {
|
||||||
unsetBrandVariationId: callbackify(
|
unsetBrandVariationId: callbackify(
|
||||||
ProjectOptionsHandler.unsetBrandVariationId
|
ProjectOptionsHandler.unsetBrandVariationId
|
||||||
),
|
),
|
||||||
enableHistoryRangesSupport: callbackify(
|
setHistoryRangesSupport: callbackify(
|
||||||
ProjectOptionsHandler.enableHistoryRangesSupport
|
ProjectOptionsHandler.setHistoryRangesSupport
|
||||||
),
|
),
|
||||||
promises: ProjectOptionsHandler,
|
promises: ProjectOptionsHandler,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
const settings = require('@overleaf/settings')
|
const settings = require('@overleaf/settings')
|
||||||
const RedisWrapper = require('./RedisWrapper')
|
const RedisWrapper = require('./RedisWrapper')
|
||||||
const rclient = RedisWrapper.client('lock')
|
const rclient = RedisWrapper.client('lock')
|
||||||
const { callbackify, promisify } = require('util')
|
|
||||||
|
|
||||||
const RedisWebLocker = require('@overleaf/redis-wrapper/RedisWebLocker')
|
const RedisWebLocker = require('@overleaf/redis-wrapper/RedisWebLocker')
|
||||||
|
|
||||||
|
@ -34,16 +33,4 @@ LockManager.withTimeout = function (timeout) {
|
||||||
return createLockManager(lockManagerSettingsWithTimeout)
|
return createLockManager(lockManagerSettingsWithTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// need to bind the promisified function when it is part of a class
|
|
||||||
// see https://nodejs.org/dist/latest-v16.x/docs/api/util.html#utilpromisifyoriginal
|
|
||||||
const promisifiedRunWithLock = promisify(LockManager.runWithLock).bind(
|
|
||||||
LockManager
|
|
||||||
)
|
|
||||||
LockManager.promises = {
|
|
||||||
runWithLock(namespace, id, runner) {
|
|
||||||
const cbRunner = callbackify(runner)
|
|
||||||
return promisifiedRunWithLock(namespace, id, cbRunner)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = LockManager
|
module.exports = LockManager
|
||||||
|
|
41
services/web/scripts/history/migrate_ranges_support.js
Normal file
41
services/web/scripts/history/migrate_ranges_support.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
const HistoryRangesSupportMigration = require('../../app/src/Features/History/HistoryRangesSupportMigration')
|
||||||
|
const { waitForDb } = require('../../app/src/infrastructure/mongodb')
|
||||||
|
const minimist = require('minimist')
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await waitForDb()
|
||||||
|
const { projectId, direction } = parseArgs()
|
||||||
|
await HistoryRangesSupportMigration.promises.migrateProject(
|
||||||
|
projectId,
|
||||||
|
direction
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
console.log('Usage: migrate_ranges_support.js PROJECT_ID [--backwards]')
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs() {
|
||||||
|
const args = minimist(process.argv.slice(2), {
|
||||||
|
boolean: ['backwards'],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (args._.length !== 1) {
|
||||||
|
usage()
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
direction: args.backwards ? 'backwards' : 'forwards',
|
||||||
|
projectId: args._[0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(() => {
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
|
@ -233,7 +233,7 @@ describe('HistoryController', function () {
|
||||||
describe('for a project without project-history enabled', function () {
|
describe('for a project without project-history enabled', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.project_id = 'mock-project-id'
|
this.project_id = 'mock-project-id'
|
||||||
this.req = { params: { Project_id: this.project_id } }
|
this.req = { params: { Project_id: this.project_id }, body: {} }
|
||||||
this.res = { setTimeout: sinon.stub(), sendStatus: sinon.stub() }
|
this.res = { setTimeout: sinon.stub(), sendStatus: sinon.stub() }
|
||||||
this.next = sinon.stub()
|
this.next = sinon.stub()
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ describe('HistoryController', function () {
|
||||||
describe('for a project with project-history enabled', function () {
|
describe('for a project with project-history enabled', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.project_id = 'mock-project-id'
|
this.project_id = 'mock-project-id'
|
||||||
this.req = { params: { Project_id: this.project_id } }
|
this.req = { params: { Project_id: this.project_id }, body: {} }
|
||||||
this.res = { setTimeout: sinon.stub(), sendStatus: sinon.stub() }
|
this.res = { setTimeout: sinon.stub(), sendStatus: sinon.stub() }
|
||||||
this.next = sinon.stub()
|
this.next = sinon.stub()
|
||||||
|
|
||||||
|
|
|
@ -149,6 +149,9 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
this.EditorRealTimeController = {
|
this.EditorRealTimeController = {
|
||||||
emitToRoom: sinon.stub(),
|
emitToRoom: sinon.stub(),
|
||||||
}
|
}
|
||||||
|
this.ProjectOptionsHandler = {
|
||||||
|
setHistoryRangesSupport: sinon.stub().yields(),
|
||||||
|
}
|
||||||
this.ProjectEntityUpdateHandler = SandboxedModule.require(MODULE_PATH, {
|
this.ProjectEntityUpdateHandler = SandboxedModule.require(MODULE_PATH, {
|
||||||
requires: {
|
requires: {
|
||||||
'@overleaf/settings': { validRootDocExtensions: ['tex'] },
|
'@overleaf/settings': { validRootDocExtensions: ['tex'] },
|
||||||
|
@ -167,6 +170,7 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
'./ProjectEntityHandler': this.ProjectEntityHandler,
|
'./ProjectEntityHandler': this.ProjectEntityHandler,
|
||||||
'./ProjectEntityMongoUpdateHandler':
|
'./ProjectEntityMongoUpdateHandler':
|
||||||
this.ProjectEntityMongoUpdateHandler,
|
this.ProjectEntityMongoUpdateHandler,
|
||||||
|
'./ProjectOptionsHandler': this.ProjectOptionsHandler,
|
||||||
'../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender,
|
'../ThirdPartyDataStore/TpdsUpdateSender': this.TpdsUpdateSender,
|
||||||
'../Editor/EditorRealTimeController': this.EditorRealTimeController,
|
'../Editor/EditorRealTimeController': this.EditorRealTimeController,
|
||||||
'../../infrastructure/FileWriter': this.FileWriter,
|
'../../infrastructure/FileWriter': this.FileWriter,
|
||||||
|
@ -1962,6 +1966,7 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
|
|
||||||
this.ProjectEntityUpdateHandler.resyncProjectHistory(
|
this.ProjectEntityUpdateHandler.resyncProjectHistory(
|
||||||
projectId,
|
projectId,
|
||||||
|
{},
|
||||||
this.callback
|
this.callback
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -1987,6 +1992,7 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
|
|
||||||
this.ProjectEntityUpdateHandler.resyncProjectHistory(
|
this.ProjectEntityUpdateHandler.resyncProjectHistory(
|
||||||
projectId,
|
projectId,
|
||||||
|
{},
|
||||||
this.callback
|
this.callback
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -2025,6 +2031,7 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
})
|
})
|
||||||
this.ProjectEntityUpdateHandler.resyncProjectHistory(
|
this.ProjectEntityUpdateHandler.resyncProjectHistory(
|
||||||
projectId,
|
projectId,
|
||||||
|
{},
|
||||||
this.callback
|
this.callback
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -2113,7 +2120,11 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
files: this.files,
|
files: this.files,
|
||||||
folders: [],
|
folders: [],
|
||||||
})
|
})
|
||||||
this.ProjectEntityUpdateHandler.resyncProjectHistory(projectId, done)
|
this.ProjectEntityUpdateHandler.resyncProjectHistory(
|
||||||
|
projectId,
|
||||||
|
{},
|
||||||
|
done
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renames the duplicate files', function () {
|
it('renames the duplicate files', function () {
|
||||||
|
@ -2214,7 +2225,11 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
files: this.files,
|
files: this.files,
|
||||||
folders: [],
|
folders: [],
|
||||||
})
|
})
|
||||||
this.ProjectEntityUpdateHandler.resyncProjectHistory(projectId, done)
|
this.ProjectEntityUpdateHandler.resyncProjectHistory(
|
||||||
|
projectId,
|
||||||
|
{},
|
||||||
|
done
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renames the files', function () {
|
it('renames the files', function () {
|
||||||
|
@ -2301,7 +2316,11 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
files,
|
files,
|
||||||
folders,
|
folders,
|
||||||
})
|
})
|
||||||
this.ProjectEntityUpdateHandler.resyncProjectHistory(projectId, done)
|
this.ProjectEntityUpdateHandler.resyncProjectHistory(
|
||||||
|
projectId,
|
||||||
|
{},
|
||||||
|
done
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renames the folder', function () {
|
it('renames the folder', function () {
|
||||||
|
@ -2352,7 +2371,11 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
files,
|
files,
|
||||||
folders,
|
folders,
|
||||||
})
|
})
|
||||||
this.ProjectEntityUpdateHandler.resyncProjectHistory(projectId, done)
|
this.ProjectEntityUpdateHandler.resyncProjectHistory(
|
||||||
|
projectId,
|
||||||
|
{},
|
||||||
|
done
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renames the doc', function () {
|
it('renames the doc', function () {
|
||||||
|
@ -2385,6 +2408,7 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
this.ProjectEntityHandler.getAllEntitiesFromProject.throws()
|
this.ProjectEntityHandler.getAllEntitiesFromProject.throws()
|
||||||
this.ProjectEntityUpdateHandler.resyncProjectHistory(
|
this.ProjectEntityUpdateHandler.resyncProjectHistory(
|
||||||
projectId,
|
projectId,
|
||||||
|
{},
|
||||||
this.callback
|
this.callback
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -191,7 +191,7 @@ describe('ProjectOptionsHandler', function () {
|
||||||
|
|
||||||
describe('setting the rangesSupportEnabled', function () {
|
describe('setting the rangesSupportEnabled', function () {
|
||||||
it('should perform and update on mongo', async function () {
|
it('should perform and update on mongo', async function () {
|
||||||
await this.handler.promises.enableHistoryRangesSupport(projectId)
|
await this.handler.promises.setHistoryRangesSupport(projectId, true)
|
||||||
sinon.assert.calledWith(
|
sinon.assert.calledWith(
|
||||||
this.db.projects.updateOne,
|
this.db.projects.updateOne,
|
||||||
{ _id: new ObjectId(projectId) },
|
{ _id: new ObjectId(projectId) },
|
||||||
|
@ -205,8 +205,8 @@ describe('ProjectOptionsHandler', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should be rejected', async function () {
|
it('should be rejected', async function () {
|
||||||
expect(this.handler.promises.enableHistoryRangesSupport(projectId)).to
|
expect(this.handler.promises.setHistoryRangesSupport(projectId, true))
|
||||||
.be.rejected
|
.to.be.rejected
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue