mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #133 from overleaf/em-json-request-size
Make max JSON request size configurable and default to 8 MB
This commit is contained in:
commit
a4de5848fb
5 changed files with 808 additions and 906 deletions
|
@ -1,11 +1,3 @@
|
|||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const Metrics = require('metrics-sharelatex')
|
||||
Metrics.initialize('doc-updater')
|
||||
|
||||
|
@ -16,7 +8,7 @@ logger.initialize('document-updater')
|
|||
|
||||
logger.logger.addSerializers(require('./app/js/LoggerSerializers'))
|
||||
|
||||
if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) {
|
||||
if (Settings.sentry != null && Settings.sentry.dsn != null) {
|
||||
logger.initializeErrorReporting(Settings.sentry.dsn)
|
||||
}
|
||||
|
||||
|
@ -39,21 +31,21 @@ Metrics.event_loop.monitor(logger, 100)
|
|||
|
||||
const app = express()
|
||||
app.use(Metrics.http.monitor(logger))
|
||||
app.use(bodyParser.json({ limit: Settings.max_doc_length + 64 * 1024 }))
|
||||
app.use(bodyParser.json({ limit: Settings.maxJsonRequestSize }))
|
||||
Metrics.injectMetricsRoute(app)
|
||||
|
||||
DispatchManager.createAndStartDispatchers(Settings.dispatcherCount || 10)
|
||||
|
||||
app.param('project_id', function (req, res, next, projectId) {
|
||||
if (projectId != null ? projectId.match(/^[0-9a-f]{24}$/) : undefined) {
|
||||
app.param('project_id', (req, res, next, projectId) => {
|
||||
if (projectId != null && projectId.match(/^[0-9a-f]{24}$/)) {
|
||||
return next()
|
||||
} else {
|
||||
return next(new Error('invalid project id'))
|
||||
}
|
||||
})
|
||||
|
||||
app.param('doc_id', function (req, res, next, docId) {
|
||||
if (docId != null ? docId.match(/^[0-9a-f]{24}$/) : undefined) {
|
||||
app.param('doc_id', (req, res, next, docId) => {
|
||||
if (docId != null && docId.match(/^[0-9a-f]{24}$/)) {
|
||||
return next()
|
||||
} else {
|
||||
return next(new Error('invalid doc id'))
|
||||
|
@ -99,18 +91,18 @@ app.delete(
|
|||
app.get('/flush_all_projects', HttpController.flushAllProjects)
|
||||
app.get('/flush_queued_projects', HttpController.flushQueuedProjects)
|
||||
|
||||
app.get('/total', function (req, res, next) {
|
||||
app.get('/total', (req, res, next) => {
|
||||
const timer = new Metrics.Timer('http.allDocList')
|
||||
return RedisManager.getCountOfDocsInMemory(function (err, count) {
|
||||
RedisManager.getCountOfDocsInMemory((err, count) => {
|
||||
if (err) {
|
||||
return next(err)
|
||||
}
|
||||
timer.done()
|
||||
return res.send({ total: count })
|
||||
res.send({ total: count })
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/status', function (req, res) {
|
||||
app.get('/status', (req, res) => {
|
||||
if (Settings.shuttingDown) {
|
||||
return res.sendStatus(503) // Service unavailable
|
||||
} else {
|
||||
|
@ -121,67 +113,70 @@ app.get('/status', function (req, res) {
|
|||
const pubsubClient = require('redis-sharelatex').createClient(
|
||||
Settings.redis.pubsub
|
||||
)
|
||||
app.get('/health_check/redis', (req, res, next) =>
|
||||
pubsubClient.healthCheck(function (error) {
|
||||
if (error != null) {
|
||||
app.get('/health_check/redis', (req, res, next) => {
|
||||
pubsubClient.healthCheck((error) => {
|
||||
if (error) {
|
||||
logger.err({ err: error }, 'failed redis health check')
|
||||
return res.sendStatus(500)
|
||||
} else {
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const docUpdaterRedisClient = require('redis-sharelatex').createClient(
|
||||
Settings.redis.documentupdater
|
||||
)
|
||||
app.get('/health_check/redis_cluster', (req, res, next) =>
|
||||
docUpdaterRedisClient.healthCheck(function (error) {
|
||||
if (error != null) {
|
||||
app.get('/health_check/redis_cluster', (req, res, next) => {
|
||||
docUpdaterRedisClient.healthCheck((error) => {
|
||||
if (error) {
|
||||
logger.err({ err: error }, 'failed redis cluster health check')
|
||||
return res.sendStatus(500)
|
||||
} else {
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
app.get('/health_check', (req, res, next) =>
|
||||
app.get('/health_check', (req, res, next) => {
|
||||
async.series(
|
||||
[
|
||||
(cb) =>
|
||||
pubsubClient.healthCheck(function (error) {
|
||||
if (error != null) {
|
||||
(cb) => {
|
||||
pubsubClient.healthCheck((error) => {
|
||||
if (error) {
|
||||
logger.err({ err: error }, 'failed redis health check')
|
||||
}
|
||||
return cb(error)
|
||||
}),
|
||||
(cb) =>
|
||||
docUpdaterRedisClient.healthCheck(function (error) {
|
||||
if (error != null) {
|
||||
cb(error)
|
||||
})
|
||||
},
|
||||
(cb) => {
|
||||
docUpdaterRedisClient.healthCheck((error) => {
|
||||
if (error) {
|
||||
logger.err({ err: error }, 'failed redis cluster health check')
|
||||
}
|
||||
return cb(error)
|
||||
}),
|
||||
(cb) =>
|
||||
mongojs.healthCheck(function (error) {
|
||||
if (error != null) {
|
||||
cb(error)
|
||||
})
|
||||
},
|
||||
(cb) => {
|
||||
mongojs.healthCheck((error) => {
|
||||
if (error) {
|
||||
logger.err({ err: error }, 'failed mongo health check')
|
||||
}
|
||||
return cb(error)
|
||||
cb(error)
|
||||
})
|
||||
}
|
||||
],
|
||||
function (error) {
|
||||
if (error != null) {
|
||||
(error) => {
|
||||
if (error) {
|
||||
return res.sendStatus(500)
|
||||
} else {
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
app.use(function (error, req, res, next) {
|
||||
app.use((error, req, res, next) => {
|
||||
if (error instanceof Errors.NotFoundError) {
|
||||
return res.sendStatus(404)
|
||||
} else if (error instanceof Errors.OpRangeNotAvailableError) {
|
||||
|
@ -194,45 +189,41 @@ app.use(function (error, req, res, next) {
|
|||
}
|
||||
})
|
||||
|
||||
const shutdownCleanly = (signal) =>
|
||||
function () {
|
||||
logger.log({ signal }, 'received interrupt, cleaning up')
|
||||
Settings.shuttingDown = true
|
||||
return setTimeout(function () {
|
||||
logger.log({ signal }, 'shutting down')
|
||||
return process.exit()
|
||||
}, 10000)
|
||||
}
|
||||
const shutdownCleanly = (signal) => () => {
|
||||
logger.log({ signal }, 'received interrupt, cleaning up')
|
||||
Settings.shuttingDown = true
|
||||
setTimeout(() => {
|
||||
logger.log({ signal }, 'shutting down')
|
||||
process.exit()
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
const watchForEvent = (eventName) =>
|
||||
docUpdaterRedisClient.on(
|
||||
eventName,
|
||||
(e) => console.log(`redis event: ${eventName} ${e}`) // eslint-disable-line no-console
|
||||
)
|
||||
const watchForEvent = (eventName) => {
|
||||
docUpdaterRedisClient.on(eventName, (e) => {
|
||||
console.log(`redis event: ${eventName} ${e}`) // eslint-disable-line no-console
|
||||
})
|
||||
}
|
||||
|
||||
const events = ['connect', 'ready', 'error', 'close', 'reconnecting', 'end']
|
||||
for (const eventName of Array.from(events)) {
|
||||
for (const eventName of events) {
|
||||
watchForEvent(eventName)
|
||||
}
|
||||
|
||||
const port =
|
||||
__guard__(
|
||||
Settings.internal != null ? Settings.internal.documentupdater : undefined,
|
||||
(x) => x.port
|
||||
) ||
|
||||
__guard__(
|
||||
Settings.apis != null ? Settings.apis.documentupdater : undefined,
|
||||
(x1) => x1.port
|
||||
) ||
|
||||
Settings.internal.documentupdater.port ||
|
||||
(Settings.api &&
|
||||
Settings.api.documentupdater &&
|
||||
Settings.api.documentupdater.port) ||
|
||||
3003
|
||||
const host = Settings.internal.documentupdater.host || 'localhost'
|
||||
|
||||
if (!module.parent) {
|
||||
// Called directly
|
||||
app.listen(port, host, function () {
|
||||
app.listen(port, host, () => {
|
||||
logger.info(`Document-updater starting up, listening on ${host}:${port}`)
|
||||
if (Settings.continuousBackgroundFlush) {
|
||||
logger.info('Starting continuous background flush')
|
||||
return DeleteQueueManager.startBackgroundFlush()
|
||||
DeleteQueueManager.startBackgroundFlush()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -250,9 +241,3 @@ for (const signal of [
|
|||
]) {
|
||||
process.on(signal, shutdownCleanly(signal))
|
||||
}
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
||||
|
|
|
@ -1,481 +1,414 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
handle-callback-err,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
let HttpController
|
||||
const DocumentManager = require('./DocumentManager')
|
||||
const HistoryManager = require('./HistoryManager')
|
||||
const ProjectManager = require('./ProjectManager')
|
||||
const Errors = require('./Errors')
|
||||
const logger = require('logger-sharelatex')
|
||||
const Settings = require('settings-sharelatex')
|
||||
const Metrics = require('./Metrics')
|
||||
const ProjectFlusher = require('./ProjectFlusher')
|
||||
const DeleteQueueManager = require('./DeleteQueueManager')
|
||||
const async = require('async')
|
||||
|
||||
const TWO_MEGABYTES = 2 * 1024 * 1024
|
||||
module.exports = {
|
||||
getDoc,
|
||||
getProjectDocsAndFlushIfOld,
|
||||
clearProjectState,
|
||||
setDoc,
|
||||
flushDocIfLoaded,
|
||||
deleteDoc,
|
||||
flushProject,
|
||||
deleteProject,
|
||||
deleteMultipleProjects,
|
||||
acceptChanges,
|
||||
deleteComment,
|
||||
updateProject,
|
||||
resyncProjectHistory,
|
||||
flushAllProjects,
|
||||
flushQueuedProjects
|
||||
}
|
||||
|
||||
module.exports = HttpController = {
|
||||
getDoc(req, res, next) {
|
||||
let fromVersion
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
const { doc_id } = req.params
|
||||
const { project_id } = req.params
|
||||
logger.log({ project_id, doc_id }, 'getting doc via http')
|
||||
const timer = new Metrics.Timer('http.getDoc')
|
||||
function getDoc(req, res, next) {
|
||||
let fromVersion
|
||||
const docId = req.params.doc_id
|
||||
const projectId = req.params.project_id
|
||||
logger.log({ projectId, docId }, 'getting doc via http')
|
||||
const timer = new Metrics.Timer('http.getDoc')
|
||||
|
||||
if ((req.query != null ? req.query.fromVersion : undefined) != null) {
|
||||
fromVersion = parseInt(req.query.fromVersion, 10)
|
||||
} else {
|
||||
fromVersion = -1
|
||||
}
|
||||
if (req.query.fromVersion != null) {
|
||||
fromVersion = parseInt(req.query.fromVersion, 10)
|
||||
} else {
|
||||
fromVersion = -1
|
||||
}
|
||||
|
||||
return DocumentManager.getDocAndRecentOpsWithLock(
|
||||
project_id,
|
||||
doc_id,
|
||||
fromVersion,
|
||||
function (error, lines, version, ops, ranges, pathname) {
|
||||
timer.done()
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ project_id, doc_id }, 'got doc via http')
|
||||
if (lines == null || version == null) {
|
||||
return next(new Errors.NotFoundError('document not found'))
|
||||
}
|
||||
return res.json({
|
||||
id: doc_id,
|
||||
lines,
|
||||
version,
|
||||
ops,
|
||||
ranges,
|
||||
pathname
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
_getTotalSizeOfLines(lines) {
|
||||
let size = 0
|
||||
for (const line of Array.from(lines)) {
|
||||
size += line.length + 1
|
||||
}
|
||||
return size
|
||||
},
|
||||
|
||||
getProjectDocsAndFlushIfOld(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
const { project_id } = req.params
|
||||
const projectStateHash = req.query != null ? req.query.state : undefined
|
||||
// exclude is string of existing docs "id:version,id:version,..."
|
||||
const excludeItems =
|
||||
__guard__(req.query != null ? req.query.exclude : undefined, (x) =>
|
||||
x.split(',')
|
||||
) || []
|
||||
logger.log({ project_id, exclude: excludeItems }, 'getting docs via http')
|
||||
const timer = new Metrics.Timer('http.getAllDocs')
|
||||
const excludeVersions = {}
|
||||
for (const item of Array.from(excludeItems)) {
|
||||
const [id, version] = Array.from(
|
||||
item != null ? item.split(':') : undefined
|
||||
)
|
||||
excludeVersions[id] = version
|
||||
}
|
||||
logger.log(
|
||||
{ project_id, projectStateHash, excludeVersions },
|
||||
'excluding versions'
|
||||
)
|
||||
return ProjectManager.getProjectDocsAndFlushIfOld(
|
||||
project_id,
|
||||
projectStateHash,
|
||||
excludeVersions,
|
||||
function (error, result) {
|
||||
timer.done()
|
||||
if (error instanceof Errors.ProjectStateChangedError) {
|
||||
return res.sendStatus(409) // conflict
|
||||
} else if (error != null) {
|
||||
return next(error)
|
||||
} else {
|
||||
logger.log(
|
||||
{
|
||||
project_id,
|
||||
result: Array.from(result).map((doc) => `${doc._id}:${doc.v}`)
|
||||
},
|
||||
'got docs via http'
|
||||
)
|
||||
return res.send(result)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
clearProjectState(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
const { project_id } = req.params
|
||||
const timer = new Metrics.Timer('http.clearProjectState')
|
||||
logger.log({ project_id }, 'clearing project state via http')
|
||||
return ProjectManager.clearProjectState(project_id, function (error) {
|
||||
DocumentManager.getDocAndRecentOpsWithLock(
|
||||
projectId,
|
||||
docId,
|
||||
fromVersion,
|
||||
(error, lines, version, ops, ranges, pathname) => {
|
||||
timer.done()
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
} else {
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
setDoc(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
const { doc_id } = req.params
|
||||
const { project_id } = req.params
|
||||
const { lines, source, user_id, undoing } = req.body
|
||||
const lineSize = HttpController._getTotalSizeOfLines(lines)
|
||||
if (lineSize > TWO_MEGABYTES) {
|
||||
logger.log(
|
||||
{ project_id, doc_id, source, lineSize, user_id },
|
||||
'document too large, returning 406 response'
|
||||
)
|
||||
return res.sendStatus(406)
|
||||
}
|
||||
logger.log(
|
||||
{ project_id, doc_id, lines, source, user_id, undoing },
|
||||
'setting doc via http'
|
||||
)
|
||||
const timer = new Metrics.Timer('http.setDoc')
|
||||
return DocumentManager.setDocWithLock(
|
||||
project_id,
|
||||
doc_id,
|
||||
lines,
|
||||
source,
|
||||
user_id,
|
||||
undoing,
|
||||
function (error) {
|
||||
timer.done()
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ project_id, doc_id }, 'set doc via http')
|
||||
return res.sendStatus(204)
|
||||
}
|
||||
)
|
||||
}, // No Content
|
||||
|
||||
flushDocIfLoaded(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
const { doc_id } = req.params
|
||||
const { project_id } = req.params
|
||||
logger.log({ project_id, doc_id }, 'flushing doc via http')
|
||||
const timer = new Metrics.Timer('http.flushDoc')
|
||||
return DocumentManager.flushDocIfLoadedWithLock(
|
||||
project_id,
|
||||
doc_id,
|
||||
function (error) {
|
||||
timer.done()
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ project_id, doc_id }, 'flushed doc via http')
|
||||
return res.sendStatus(204)
|
||||
}
|
||||
)
|
||||
}, // No Content
|
||||
|
||||
deleteDoc(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
const { doc_id } = req.params
|
||||
const { project_id } = req.params
|
||||
const ignoreFlushErrors = req.query.ignore_flush_errors === 'true'
|
||||
const timer = new Metrics.Timer('http.deleteDoc')
|
||||
logger.log({ project_id, doc_id }, 'deleting doc via http')
|
||||
return DocumentManager.flushAndDeleteDocWithLock(
|
||||
project_id,
|
||||
doc_id,
|
||||
{ ignoreFlushErrors },
|
||||
function (error) {
|
||||
timer.done()
|
||||
// There is no harm in flushing project history if the previous call
|
||||
// failed and sometimes it is required
|
||||
HistoryManager.flushProjectChangesAsync(project_id)
|
||||
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ project_id, doc_id }, 'deleted doc via http')
|
||||
return res.sendStatus(204)
|
||||
}
|
||||
)
|
||||
}, // No Content
|
||||
|
||||
flushProject(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
const { project_id } = req.params
|
||||
logger.log({ project_id }, 'flushing project via http')
|
||||
const timer = new Metrics.Timer('http.flushProject')
|
||||
return ProjectManager.flushProjectWithLocks(project_id, function (error) {
|
||||
timer.done()
|
||||
if (error != null) {
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ project_id }, 'flushed project via http')
|
||||
return res.sendStatus(204)
|
||||
})
|
||||
}, // No Content
|
||||
|
||||
deleteProject(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
logger.log({ projectId, docId }, 'got doc via http')
|
||||
if (lines == null || version == null) {
|
||||
return next(new Errors.NotFoundError('document not found'))
|
||||
}
|
||||
res.json({
|
||||
id: docId,
|
||||
lines,
|
||||
version,
|
||||
ops,
|
||||
ranges,
|
||||
pathname
|
||||
})
|
||||
}
|
||||
const { project_id } = req.params
|
||||
logger.log({ project_id }, 'deleting project via http')
|
||||
const options = {}
|
||||
if (req.query != null ? req.query.background : undefined) {
|
||||
options.background = true
|
||||
} // allow non-urgent flushes to be queued
|
||||
if (req.query != null ? req.query.shutdown : undefined) {
|
||||
options.skip_history_flush = true
|
||||
} // don't flush history when realtime shuts down
|
||||
if (req.query != null ? req.query.background : undefined) {
|
||||
return ProjectManager.queueFlushAndDeleteProject(project_id, function (
|
||||
error
|
||||
) {
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ project_id }, 'queue delete of project via http')
|
||||
return res.sendStatus(204)
|
||||
}) // No Content
|
||||
)
|
||||
}
|
||||
|
||||
function _getTotalSizeOfLines(lines) {
|
||||
let size = 0
|
||||
for (const line of lines) {
|
||||
size += line.length + 1
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
function getProjectDocsAndFlushIfOld(req, res, next) {
|
||||
const projectId = req.params.project_id
|
||||
const projectStateHash = req.query.state
|
||||
// exclude is string of existing docs "id:version,id:version,..."
|
||||
const excludeItems =
|
||||
req.query.exclude != null ? req.query.exclude.split(',') : []
|
||||
logger.log({ projectId, exclude: excludeItems }, 'getting docs via http')
|
||||
const timer = new Metrics.Timer('http.getAllDocs')
|
||||
const excludeVersions = {}
|
||||
for (const item of excludeItems) {
|
||||
const [id, version] = item.split(':')
|
||||
excludeVersions[id] = version
|
||||
}
|
||||
logger.log(
|
||||
{ projectId, projectStateHash, excludeVersions },
|
||||
'excluding versions'
|
||||
)
|
||||
ProjectManager.getProjectDocsAndFlushIfOld(
|
||||
projectId,
|
||||
projectStateHash,
|
||||
excludeVersions,
|
||||
(error, result) => {
|
||||
timer.done()
|
||||
if (error instanceof Errors.ProjectStateChangedError) {
|
||||
res.sendStatus(409) // conflict
|
||||
} else if (error) {
|
||||
next(error)
|
||||
} else {
|
||||
logger.log(
|
||||
{
|
||||
projectId,
|
||||
result: result.map((doc) => `${doc._id}:${doc.v}`)
|
||||
},
|
||||
'got docs via http'
|
||||
)
|
||||
res.send(result)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function clearProjectState(req, res, next) {
|
||||
const projectId = req.params.project_id
|
||||
const timer = new Metrics.Timer('http.clearProjectState')
|
||||
logger.log({ projectId }, 'clearing project state via http')
|
||||
ProjectManager.clearProjectState(projectId, (error) => {
|
||||
timer.done()
|
||||
if (error) {
|
||||
next(error)
|
||||
} else {
|
||||
const timer = new Metrics.Timer('http.deleteProject')
|
||||
return ProjectManager.flushAndDeleteProjectWithLocks(
|
||||
project_id,
|
||||
options,
|
||||
function (error) {
|
||||
timer.done()
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ project_id }, 'deleted project via http')
|
||||
return res.sendStatus(204)
|
||||
}
|
||||
)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
}, // No Content
|
||||
})
|
||||
}
|
||||
|
||||
deleteMultipleProjects(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
const project_ids =
|
||||
(req.body != null ? req.body.project_ids : undefined) || []
|
||||
logger.log({ project_ids }, 'deleting multiple projects via http')
|
||||
return async.eachSeries(
|
||||
project_ids,
|
||||
function (project_id, cb) {
|
||||
logger.log({ project_id }, 'queue delete of project via http')
|
||||
return ProjectManager.queueFlushAndDeleteProject(project_id, cb)
|
||||
},
|
||||
function (error) {
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
return res.sendStatus(204)
|
||||
}
|
||||
)
|
||||
}, // No Content
|
||||
|
||||
acceptChanges(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
const { project_id, doc_id } = req.params
|
||||
let change_ids = req.body != null ? req.body.change_ids : undefined
|
||||
if (change_ids == null) {
|
||||
change_ids = [req.params.change_id]
|
||||
}
|
||||
function setDoc(req, res, next) {
|
||||
const docId = req.params.doc_id
|
||||
const projectId = req.params.project_id
|
||||
const { lines, source, user_id: userId, undoing } = req.body
|
||||
const lineSize = _getTotalSizeOfLines(lines)
|
||||
if (lineSize > Settings.max_doc_length) {
|
||||
logger.log(
|
||||
{ project_id, doc_id },
|
||||
`accepting ${change_ids.length} changes via http`
|
||||
{ projectId, docId, source, lineSize, userId },
|
||||
'document too large, returning 406 response'
|
||||
)
|
||||
const timer = new Metrics.Timer('http.acceptChanges')
|
||||
return DocumentManager.acceptChangesWithLock(
|
||||
project_id,
|
||||
doc_id,
|
||||
change_ids,
|
||||
function (error) {
|
||||
return res.sendStatus(406)
|
||||
}
|
||||
logger.log(
|
||||
{ projectId, docId, lines, source, userId, undoing },
|
||||
'setting doc via http'
|
||||
)
|
||||
const timer = new Metrics.Timer('http.setDoc')
|
||||
DocumentManager.setDocWithLock(
|
||||
projectId,
|
||||
docId,
|
||||
lines,
|
||||
source,
|
||||
userId,
|
||||
undoing,
|
||||
(error) => {
|
||||
timer.done()
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ projectId, docId }, 'set doc via http')
|
||||
res.sendStatus(204) // No Content
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function flushDocIfLoaded(req, res, next) {
|
||||
const docId = req.params.doc_id
|
||||
const projectId = req.params.project_id
|
||||
logger.log({ projectId, docId }, 'flushing doc via http')
|
||||
const timer = new Metrics.Timer('http.flushDoc')
|
||||
DocumentManager.flushDocIfLoadedWithLock(projectId, docId, (error) => {
|
||||
timer.done()
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ projectId, docId }, 'flushed doc via http')
|
||||
res.sendStatus(204) // No Content
|
||||
})
|
||||
}
|
||||
|
||||
function deleteDoc(req, res, next) {
|
||||
const docId = req.params.doc_id
|
||||
const projectId = req.params.project_id
|
||||
const ignoreFlushErrors = req.query.ignore_flush_errors === 'true'
|
||||
const timer = new Metrics.Timer('http.deleteDoc')
|
||||
logger.log({ projectId, docId }, 'deleting doc via http')
|
||||
DocumentManager.flushAndDeleteDocWithLock(
|
||||
projectId,
|
||||
docId,
|
||||
{ ignoreFlushErrors },
|
||||
(error) => {
|
||||
timer.done()
|
||||
// There is no harm in flushing project history if the previous call
|
||||
// failed and sometimes it is required
|
||||
HistoryManager.flushProjectChangesAsync(projectId)
|
||||
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ projectId, docId }, 'deleted doc via http')
|
||||
res.sendStatus(204) // No Content
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function flushProject(req, res, next) {
|
||||
const projectId = req.params.project_id
|
||||
logger.log({ projectId }, 'flushing project via http')
|
||||
const timer = new Metrics.Timer('http.flushProject')
|
||||
ProjectManager.flushProjectWithLocks(projectId, (error) => {
|
||||
timer.done()
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ projectId }, 'flushed project via http')
|
||||
res.sendStatus(204) // No Content
|
||||
})
|
||||
}
|
||||
|
||||
function deleteProject(req, res, next) {
|
||||
const projectId = req.params.project_id
|
||||
logger.log({ projectId }, 'deleting project via http')
|
||||
const options = {}
|
||||
if (req.query.background) {
|
||||
options.background = true
|
||||
} // allow non-urgent flushes to be queued
|
||||
if (req.query.shutdown) {
|
||||
options.skip_history_flush = true
|
||||
} // don't flush history when realtime shuts down
|
||||
if (req.query.background) {
|
||||
ProjectManager.queueFlushAndDeleteProject(projectId, (error) => {
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ projectId }, 'queue delete of project via http')
|
||||
res.sendStatus(204)
|
||||
}) // No Content
|
||||
} else {
|
||||
const timer = new Metrics.Timer('http.deleteProject')
|
||||
ProjectManager.flushAndDeleteProjectWithLocks(
|
||||
projectId,
|
||||
options,
|
||||
(error) => {
|
||||
timer.done()
|
||||
if (error != null) {
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log(
|
||||
{ project_id, doc_id },
|
||||
`accepted ${change_ids.length} changes via http`
|
||||
)
|
||||
return res.sendStatus(204)
|
||||
logger.log({ projectId }, 'deleted project via http')
|
||||
res.sendStatus(204) // No Content
|
||||
}
|
||||
)
|
||||
}, // No Content
|
||||
|
||||
deleteComment(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
const { project_id, doc_id, comment_id } = req.params
|
||||
logger.log({ project_id, doc_id, comment_id }, 'deleting comment via http')
|
||||
const timer = new Metrics.Timer('http.deleteComment')
|
||||
return DocumentManager.deleteCommentWithLock(
|
||||
project_id,
|
||||
doc_id,
|
||||
comment_id,
|
||||
function (error) {
|
||||
timer.done()
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log(
|
||||
{ project_id, doc_id, comment_id },
|
||||
'deleted comment via http'
|
||||
)
|
||||
return res.sendStatus(204)
|
||||
}
|
||||
)
|
||||
}, // No Content
|
||||
|
||||
updateProject(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
const timer = new Metrics.Timer('http.updateProject')
|
||||
const { project_id } = req.params
|
||||
const {
|
||||
projectHistoryId,
|
||||
userId,
|
||||
docUpdates,
|
||||
fileUpdates,
|
||||
version
|
||||
} = req.body
|
||||
logger.log(
|
||||
{ project_id, docUpdates, fileUpdates, version },
|
||||
'updating project via http'
|
||||
)
|
||||
|
||||
return ProjectManager.updateProjectWithLocks(
|
||||
project_id,
|
||||
projectHistoryId,
|
||||
userId,
|
||||
docUpdates,
|
||||
fileUpdates,
|
||||
version,
|
||||
function (error) {
|
||||
timer.done()
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ project_id }, 'updated project via http')
|
||||
return res.sendStatus(204)
|
||||
}
|
||||
)
|
||||
}, // No Content
|
||||
|
||||
resyncProjectHistory(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
const { project_id } = req.params
|
||||
const { projectHistoryId, docs, files } = req.body
|
||||
|
||||
logger.log(
|
||||
{ project_id, docs, files },
|
||||
'queuing project history resync via http'
|
||||
)
|
||||
return HistoryManager.resyncProjectHistory(
|
||||
project_id,
|
||||
projectHistoryId,
|
||||
docs,
|
||||
files,
|
||||
function (error) {
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ project_id }, 'queued project history resync via http')
|
||||
return res.sendStatus(204)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
flushAllProjects(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
res.setTimeout(5 * 60 * 1000)
|
||||
const options = {
|
||||
limit: req.query.limit || 1000,
|
||||
concurrency: req.query.concurrency || 5,
|
||||
dryRun: req.query.dryRun || false
|
||||
}
|
||||
return ProjectFlusher.flushAllProjects(options, function (
|
||||
err,
|
||||
project_ids
|
||||
) {
|
||||
if (err != null) {
|
||||
logger.err({ err }, 'error bulk flushing projects')
|
||||
return res.sendStatus(500)
|
||||
} else {
|
||||
return res.send(project_ids)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
flushQueuedProjects(req, res, next) {
|
||||
if (next == null) {
|
||||
next = function (error) {}
|
||||
}
|
||||
res.setTimeout(10 * 60 * 1000)
|
||||
const options = {
|
||||
limit: req.query.limit || 1000,
|
||||
timeout: 5 * 60 * 1000,
|
||||
min_delete_age: req.query.min_delete_age || 5 * 60 * 1000
|
||||
}
|
||||
return DeleteQueueManager.flushAndDeleteOldProjects(options, function (
|
||||
err,
|
||||
flushed
|
||||
) {
|
||||
if (err != null) {
|
||||
logger.err({ err }, 'error flushing old projects')
|
||||
return res.sendStatus(500)
|
||||
} else {
|
||||
logger.log({ flushed }, 'flush of queued projects completed')
|
||||
return res.send({ flushed })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
function deleteMultipleProjects(req, res, next) {
|
||||
const projectIds = req.body.project_ids || []
|
||||
logger.log({ projectIds }, 'deleting multiple projects via http')
|
||||
async.eachSeries(
|
||||
projectIds,
|
||||
(projectId, cb) => {
|
||||
logger.log({ projectId }, 'queue delete of project via http')
|
||||
ProjectManager.queueFlushAndDeleteProject(projectId, cb)
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
res.sendStatus(204) // No Content
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function acceptChanges(req, res, next) {
|
||||
const { project_id: projectId, doc_id: docId } = req.params
|
||||
let changeIds = req.body.change_ids
|
||||
if (changeIds == null) {
|
||||
changeIds = [req.params.change_id]
|
||||
}
|
||||
logger.log(
|
||||
{ projectId, docId },
|
||||
`accepting ${changeIds.length} changes via http`
|
||||
)
|
||||
const timer = new Metrics.Timer('http.acceptChanges')
|
||||
DocumentManager.acceptChangesWithLock(
|
||||
projectId,
|
||||
docId,
|
||||
changeIds,
|
||||
(error) => {
|
||||
timer.done()
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log(
|
||||
{ projectId, docId },
|
||||
`accepted ${changeIds.length} changes via http`
|
||||
)
|
||||
res.sendStatus(204) // No Content
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function deleteComment(req, res, next) {
|
||||
const {
|
||||
project_id: projectId,
|
||||
doc_id: docId,
|
||||
comment_id: commentId
|
||||
} = req.params
|
||||
logger.log({ projectId, docId, commentId }, 'deleting comment via http')
|
||||
const timer = new Metrics.Timer('http.deleteComment')
|
||||
DocumentManager.deleteCommentWithLock(
|
||||
projectId,
|
||||
docId,
|
||||
commentId,
|
||||
(error) => {
|
||||
timer.done()
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ projectId, docId, commentId }, 'deleted comment via http')
|
||||
res.sendStatus(204) // No Content
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function updateProject(req, res, next) {
|
||||
const timer = new Metrics.Timer('http.updateProject')
|
||||
const projectId = req.params.project_id
|
||||
const {
|
||||
projectHistoryId,
|
||||
userId,
|
||||
docUpdates,
|
||||
fileUpdates,
|
||||
version
|
||||
} = req.body
|
||||
logger.log(
|
||||
{ projectId, docUpdates, fileUpdates, version },
|
||||
'updating project via http'
|
||||
)
|
||||
|
||||
ProjectManager.updateProjectWithLocks(
|
||||
projectId,
|
||||
projectHistoryId,
|
||||
userId,
|
||||
docUpdates,
|
||||
fileUpdates,
|
||||
version,
|
||||
(error) => {
|
||||
timer.done()
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ projectId }, 'updated project via http')
|
||||
res.sendStatus(204) // No Content
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function resyncProjectHistory(req, res, next) {
|
||||
const projectId = req.params.project_id
|
||||
const { projectHistoryId, docs, files } = req.body
|
||||
|
||||
logger.log(
|
||||
{ projectId, docs, files },
|
||||
'queuing project history resync via http'
|
||||
)
|
||||
HistoryManager.resyncProjectHistory(
|
||||
projectId,
|
||||
projectHistoryId,
|
||||
docs,
|
||||
files,
|
||||
(error) => {
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
logger.log({ projectId }, 'queued project history resync via http')
|
||||
res.sendStatus(204)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function flushAllProjects(req, res, next) {
|
||||
res.setTimeout(5 * 60 * 1000)
|
||||
const options = {
|
||||
limit: req.query.limit || 1000,
|
||||
concurrency: req.query.concurrency || 5,
|
||||
dryRun: req.query.dryRun || false
|
||||
}
|
||||
ProjectFlusher.flushAllProjects(options, (err, projectIds) => {
|
||||
if (err) {
|
||||
logger.err({ err }, 'error bulk flushing projects')
|
||||
res.sendStatus(500)
|
||||
} else {
|
||||
res.send(projectIds)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function flushQueuedProjects(req, res, next) {
|
||||
res.setTimeout(10 * 60 * 1000)
|
||||
const options = {
|
||||
limit: req.query.limit || 1000,
|
||||
timeout: 5 * 60 * 1000,
|
||||
min_delete_age: req.query.min_delete_age || 5 * 60 * 1000
|
||||
}
|
||||
DeleteQueueManager.flushAndDeleteOldProjects(options, (err, flushed) => {
|
||||
if (err) {
|
||||
logger.err({ err }, 'error flushing old projects')
|
||||
res.sendStatus(500)
|
||||
} else {
|
||||
logger.log({ flushed }, 'flush of queued projects completed')
|
||||
res.send({ flushed })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -168,6 +168,8 @@ module.exports = {
|
|||
},
|
||||
|
||||
max_doc_length: 2 * 1024 * 1024, // 2mb
|
||||
maxJsonRequestSize:
|
||||
parseInt(process.env.MAX_JSON_REQUEST_SIZE, 10) || 8 * 1024 * 1024,
|
||||
|
||||
dispatcherCount: process.env.DISPATCHER_COUNT,
|
||||
|
||||
|
|
|
@ -1,23 +1,9 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
handle-callback-err,
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const sinon = require('sinon')
|
||||
const chai = require('chai')
|
||||
chai.should()
|
||||
const { expect } = require('chai')
|
||||
const Settings = require('settings-sharelatex')
|
||||
const rclient_du = require('redis-sharelatex').createClient(
|
||||
const docUpdaterRedis = require('redis-sharelatex').createClient(
|
||||
Settings.redis.documentupdater
|
||||
)
|
||||
const Keys = Settings.redis.documentupdater.key_schema
|
||||
|
@ -50,39 +36,37 @@ describe('Setting a document', function () {
|
|||
sinon.spy(MockTrackChangesApi, 'flushDoc')
|
||||
sinon.spy(MockProjectHistoryApi, 'flushProject')
|
||||
sinon.spy(MockWebApi, 'setDocument')
|
||||
return DocUpdaterApp.ensureRunning(done)
|
||||
DocUpdaterApp.ensureRunning(done)
|
||||
})
|
||||
|
||||
after(function () {
|
||||
MockTrackChangesApi.flushDoc.restore()
|
||||
MockProjectHistoryApi.flushProject.restore()
|
||||
return MockWebApi.setDocument.restore()
|
||||
MockWebApi.setDocument.restore()
|
||||
})
|
||||
|
||||
describe('when the updated doc exists in the doc updater', function () {
|
||||
before(function (done) {
|
||||
;[this.project_id, this.doc_id] = Array.from([
|
||||
DocUpdaterClient.randomId(),
|
||||
DocUpdaterClient.randomId()
|
||||
])
|
||||
this.project_id = DocUpdaterClient.randomId()
|
||||
this.doc_id = DocUpdaterClient.randomId()
|
||||
MockWebApi.insertDoc(this.project_id, this.doc_id, {
|
||||
lines: this.lines,
|
||||
version: this.version
|
||||
})
|
||||
DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, (error) => {
|
||||
if (error != null) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
return DocUpdaterClient.sendUpdate(
|
||||
DocUpdaterClient.sendUpdate(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.update,
|
||||
(error) => {
|
||||
if (error != null) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
return setTimeout(() => {
|
||||
return DocUpdaterClient.setDocLines(
|
||||
setTimeout(() => {
|
||||
DocUpdaterClient.setDocLines(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.newLines,
|
||||
|
@ -90,29 +74,31 @@ describe('Setting a document', function () {
|
|||
this.user_id,
|
||||
false,
|
||||
(error, res, body) => {
|
||||
if (error) {
|
||||
return done(error)
|
||||
}
|
||||
this.statusCode = res.statusCode
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
)
|
||||
}, 200)
|
||||
}
|
||||
)
|
||||
})
|
||||
return null
|
||||
})
|
||||
|
||||
after(function () {
|
||||
MockTrackChangesApi.flushDoc.reset()
|
||||
MockProjectHistoryApi.flushProject.reset()
|
||||
return MockWebApi.setDocument.reset()
|
||||
MockWebApi.setDocument.reset()
|
||||
})
|
||||
|
||||
it('should return a 204 status code', function () {
|
||||
return this.statusCode.should.equal(204)
|
||||
this.statusCode.should.equal(204)
|
||||
})
|
||||
|
||||
it('should send the updated doc lines and version to the web api', function () {
|
||||
return MockWebApi.setDocument
|
||||
MockWebApi.setDocument
|
||||
.calledWith(this.project_id, this.doc_id, this.newLines)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
@ -122,11 +108,13 @@ describe('Setting a document', function () {
|
|||
this.project_id,
|
||||
this.doc_id,
|
||||
(error, res, doc) => {
|
||||
if (error) {
|
||||
return done(error)
|
||||
}
|
||||
doc.lines.should.deep.equal(this.newLines)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
)
|
||||
return null
|
||||
})
|
||||
|
||||
it('should bump the version in the doc updater', function (done) {
|
||||
|
@ -134,31 +122,33 @@ describe('Setting a document', function () {
|
|||
this.project_id,
|
||||
this.doc_id,
|
||||
(error, res, doc) => {
|
||||
if (error) {
|
||||
return done(error)
|
||||
}
|
||||
doc.version.should.equal(this.version + 2)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
)
|
||||
return null
|
||||
})
|
||||
|
||||
return it('should leave the document in redis', function (done) {
|
||||
rclient_du.get(Keys.docLines({ doc_id: this.doc_id }), (error, lines) => {
|
||||
if (error != null) {
|
||||
throw error
|
||||
it('should leave the document in redis', function (done) {
|
||||
docUpdaterRedis.get(
|
||||
Keys.docLines({ doc_id: this.doc_id }),
|
||||
(error, lines) => {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
expect(JSON.parse(lines)).to.deep.equal(this.newLines)
|
||||
done()
|
||||
}
|
||||
expect(JSON.parse(lines)).to.deep.equal(this.newLines)
|
||||
return done()
|
||||
})
|
||||
return null
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the updated doc does not exist in the doc updater', function () {
|
||||
before(function (done) {
|
||||
;[this.project_id, this.doc_id] = Array.from([
|
||||
DocUpdaterClient.randomId(),
|
||||
DocUpdaterClient.randomId()
|
||||
])
|
||||
this.project_id = DocUpdaterClient.randomId()
|
||||
this.doc_id = DocUpdaterClient.randomId()
|
||||
MockWebApi.insertDoc(this.project_id, this.doc_id, {
|
||||
lines: this.lines,
|
||||
version: this.version
|
||||
|
@ -171,114 +161,126 @@ describe('Setting a document', function () {
|
|||
this.user_id,
|
||||
false,
|
||||
(error, res, body) => {
|
||||
if (error) {
|
||||
return done(error)
|
||||
}
|
||||
this.statusCode = res.statusCode
|
||||
return setTimeout(done, 200)
|
||||
setTimeout(done, 200)
|
||||
}
|
||||
)
|
||||
return null
|
||||
})
|
||||
|
||||
after(function () {
|
||||
MockTrackChangesApi.flushDoc.reset()
|
||||
MockProjectHistoryApi.flushProject.reset()
|
||||
return MockWebApi.setDocument.reset()
|
||||
MockWebApi.setDocument.reset()
|
||||
})
|
||||
|
||||
it('should return a 204 status code', function () {
|
||||
return this.statusCode.should.equal(204)
|
||||
this.statusCode.should.equal(204)
|
||||
})
|
||||
|
||||
it('should send the updated doc lines to the web api', function () {
|
||||
return MockWebApi.setDocument
|
||||
MockWebApi.setDocument
|
||||
.calledWith(this.project_id, this.doc_id, this.newLines)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should flush track changes', function () {
|
||||
return MockTrackChangesApi.flushDoc
|
||||
.calledWith(this.doc_id)
|
||||
.should.equal(true)
|
||||
MockTrackChangesApi.flushDoc.calledWith(this.doc_id).should.equal(true)
|
||||
})
|
||||
|
||||
it('should flush project history', function () {
|
||||
return MockProjectHistoryApi.flushProject
|
||||
MockProjectHistoryApi.flushProject
|
||||
.calledWith(this.project_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
return it('should remove the document from redis', function (done) {
|
||||
rclient_du.get(Keys.docLines({ doc_id: this.doc_id }), (error, lines) => {
|
||||
if (error != null) {
|
||||
throw error
|
||||
it('should remove the document from redis', function (done) {
|
||||
docUpdaterRedis.get(
|
||||
Keys.docLines({ doc_id: this.doc_id }),
|
||||
(error, lines) => {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
expect(lines).to.not.exist
|
||||
done()
|
||||
}
|
||||
expect(lines).to.not.exist
|
||||
return done()
|
||||
})
|
||||
return null
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the updated doc is too large for the body parser', function () {
|
||||
before(function (done) {
|
||||
;[this.project_id, this.doc_id] = Array.from([
|
||||
DocUpdaterClient.randomId(),
|
||||
DocUpdaterClient.randomId()
|
||||
])
|
||||
MockWebApi.insertDoc(this.project_id, this.doc_id, {
|
||||
lines: this.lines,
|
||||
version: this.version
|
||||
})
|
||||
this.newLines = []
|
||||
while (
|
||||
JSON.stringify(this.newLines).length <
|
||||
Settings.max_doc_length + 64 * 1024
|
||||
) {
|
||||
this.newLines.push('(a long line of text)'.repeat(10000))
|
||||
}
|
||||
DocUpdaterClient.setDocLines(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.newLines,
|
||||
this.source,
|
||||
this.user_id,
|
||||
false,
|
||||
(error, res, body) => {
|
||||
this.statusCode = res.statusCode
|
||||
return setTimeout(done, 200)
|
||||
const DOC_TOO_LARGE_TEST_CASES = [
|
||||
{
|
||||
desc: 'when the updated doc is too large for the body parser',
|
||||
size: Settings.maxJsonRequestSize,
|
||||
expectedStatusCode: 413
|
||||
},
|
||||
{
|
||||
desc: 'when the updated doc is larger than the HTTP controller limit',
|
||||
size: Settings.max_doc_length,
|
||||
expectedStatusCode: 406
|
||||
}
|
||||
]
|
||||
|
||||
DOC_TOO_LARGE_TEST_CASES.forEach((testCase) => {
|
||||
describe(testCase.desc, function () {
|
||||
before(function (done) {
|
||||
this.project_id = DocUpdaterClient.randomId()
|
||||
this.doc_id = DocUpdaterClient.randomId()
|
||||
MockWebApi.insertDoc(this.project_id, this.doc_id, {
|
||||
lines: this.lines,
|
||||
version: this.version
|
||||
})
|
||||
this.newLines = []
|
||||
while (JSON.stringify(this.newLines).length <= testCase.size) {
|
||||
this.newLines.push('(a long line of text)'.repeat(10000))
|
||||
}
|
||||
)
|
||||
return null
|
||||
})
|
||||
DocUpdaterClient.setDocLines(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.newLines,
|
||||
this.source,
|
||||
this.user_id,
|
||||
false,
|
||||
(error, res, body) => {
|
||||
if (error) {
|
||||
return done(error)
|
||||
}
|
||||
this.statusCode = res.statusCode
|
||||
setTimeout(done, 200)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
after(function () {
|
||||
MockTrackChangesApi.flushDoc.reset()
|
||||
MockProjectHistoryApi.flushProject.reset()
|
||||
return MockWebApi.setDocument.reset()
|
||||
})
|
||||
after(function () {
|
||||
MockTrackChangesApi.flushDoc.reset()
|
||||
MockProjectHistoryApi.flushProject.reset()
|
||||
MockWebApi.setDocument.reset()
|
||||
})
|
||||
|
||||
it('should return a 413 status code', function () {
|
||||
return this.statusCode.should.equal(413)
|
||||
})
|
||||
it(`should return a ${testCase.expectedStatusCode} status code`, function () {
|
||||
this.statusCode.should.equal(testCase.expectedStatusCode)
|
||||
})
|
||||
|
||||
it('should not send the updated doc lines to the web api', function () {
|
||||
return MockWebApi.setDocument.called.should.equal(false)
|
||||
})
|
||||
it('should not send the updated doc lines to the web api', function () {
|
||||
MockWebApi.setDocument.called.should.equal(false)
|
||||
})
|
||||
|
||||
it('should not flush track changes', function () {
|
||||
return MockTrackChangesApi.flushDoc.called.should.equal(false)
|
||||
})
|
||||
it('should not flush track changes', function () {
|
||||
MockTrackChangesApi.flushDoc.called.should.equal(false)
|
||||
})
|
||||
|
||||
return it('should not flush project history', function () {
|
||||
return MockProjectHistoryApi.flushProject.called.should.equal(false)
|
||||
it('should not flush project history', function () {
|
||||
MockProjectHistoryApi.flushProject.called.should.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the updated doc is large but under the bodyParser and HTTPController size limit', function () {
|
||||
before(function (done) {
|
||||
;[this.project_id, this.doc_id] = Array.from([
|
||||
DocUpdaterClient.randomId(),
|
||||
DocUpdaterClient.randomId()
|
||||
])
|
||||
this.project_id = DocUpdaterClient.randomId()
|
||||
this.doc_id = DocUpdaterClient.randomId()
|
||||
MockWebApi.insertDoc(this.project_id, this.doc_id, {
|
||||
lines: this.lines,
|
||||
version: this.version
|
||||
|
@ -298,35 +300,37 @@ describe('Setting a document', function () {
|
|||
this.user_id,
|
||||
false,
|
||||
(error, res, body) => {
|
||||
if (error) {
|
||||
return done(error)
|
||||
}
|
||||
this.statusCode = res.statusCode
|
||||
return setTimeout(done, 200)
|
||||
setTimeout(done, 200)
|
||||
}
|
||||
)
|
||||
return null
|
||||
})
|
||||
|
||||
after(function () {
|
||||
MockTrackChangesApi.flushDoc.reset()
|
||||
MockProjectHistoryApi.flushProject.reset()
|
||||
return MockWebApi.setDocument.reset()
|
||||
MockWebApi.setDocument.reset()
|
||||
})
|
||||
|
||||
it('should return a 204 status code', function () {
|
||||
return this.statusCode.should.equal(204)
|
||||
this.statusCode.should.equal(204)
|
||||
})
|
||||
|
||||
return it('should send the updated doc lines to the web api', function () {
|
||||
return MockWebApi.setDocument
|
||||
it('should send the updated doc lines to the web api', function () {
|
||||
MockWebApi.setDocument
|
||||
.calledWith(this.project_id, this.doc_id, this.newLines)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
return describe('with track changes', function () {
|
||||
describe('with track changes', function () {
|
||||
before(function () {
|
||||
this.lines = ['one', 'one and a half', 'two', 'three']
|
||||
this.id_seed = '587357bd35e64f6157'
|
||||
return (this.update = {
|
||||
this.update = {
|
||||
doc: this.doc_id,
|
||||
op: [
|
||||
{
|
||||
|
@ -339,33 +343,31 @@ describe('Setting a document', function () {
|
|||
user_id: this.user_id
|
||||
},
|
||||
v: this.version
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('with the undo flag', function () {
|
||||
before(function (done) {
|
||||
;[this.project_id, this.doc_id] = Array.from([
|
||||
DocUpdaterClient.randomId(),
|
||||
DocUpdaterClient.randomId()
|
||||
])
|
||||
this.project_id = DocUpdaterClient.randomId()
|
||||
this.doc_id = DocUpdaterClient.randomId()
|
||||
MockWebApi.insertDoc(this.project_id, this.doc_id, {
|
||||
lines: this.lines,
|
||||
version: this.version
|
||||
})
|
||||
DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, (error) => {
|
||||
if (error != null) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
return DocUpdaterClient.sendUpdate(
|
||||
DocUpdaterClient.sendUpdate(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.update,
|
||||
(error) => {
|
||||
if (error != null) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
// Go back to old lines, with undo flag
|
||||
return DocUpdaterClient.setDocLines(
|
||||
DocUpdaterClient.setDocLines(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.lines,
|
||||
|
@ -373,63 +375,62 @@ describe('Setting a document', function () {
|
|||
this.user_id,
|
||||
true,
|
||||
(error, res, body) => {
|
||||
if (error) {
|
||||
return done(error)
|
||||
}
|
||||
this.statusCode = res.statusCode
|
||||
return setTimeout(done, 200)
|
||||
setTimeout(done, 200)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
return null
|
||||
})
|
||||
|
||||
after(function () {
|
||||
MockTrackChangesApi.flushDoc.reset()
|
||||
MockProjectHistoryApi.flushProject.reset()
|
||||
return MockWebApi.setDocument.reset()
|
||||
MockWebApi.setDocument.reset()
|
||||
})
|
||||
|
||||
return it('should undo the tracked changes', function (done) {
|
||||
it('should undo the tracked changes', function (done) {
|
||||
DocUpdaterClient.getDoc(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
(error, res, data) => {
|
||||
if (error != null) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
const { ranges } = data
|
||||
expect(ranges.changes).to.be.undefined
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
)
|
||||
return null
|
||||
})
|
||||
})
|
||||
|
||||
return describe('without the undo flag', function () {
|
||||
describe('without the undo flag', function () {
|
||||
before(function (done) {
|
||||
;[this.project_id, this.doc_id] = Array.from([
|
||||
DocUpdaterClient.randomId(),
|
||||
DocUpdaterClient.randomId()
|
||||
])
|
||||
this.project_id = DocUpdaterClient.randomId()
|
||||
this.doc_id = DocUpdaterClient.randomId()
|
||||
MockWebApi.insertDoc(this.project_id, this.doc_id, {
|
||||
lines: this.lines,
|
||||
version: this.version
|
||||
})
|
||||
DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, (error) => {
|
||||
if (error != null) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
return DocUpdaterClient.sendUpdate(
|
||||
DocUpdaterClient.sendUpdate(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.update,
|
||||
(error) => {
|
||||
if (error != null) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
// Go back to old lines, without undo flag
|
||||
return DocUpdaterClient.setDocLines(
|
||||
DocUpdaterClient.setDocLines(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.lines,
|
||||
|
@ -437,36 +438,37 @@ describe('Setting a document', function () {
|
|||
this.user_id,
|
||||
false,
|
||||
(error, res, body) => {
|
||||
if (error) {
|
||||
return done(error)
|
||||
}
|
||||
this.statusCode = res.statusCode
|
||||
return setTimeout(done, 200)
|
||||
setTimeout(done, 200)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
return null
|
||||
})
|
||||
|
||||
after(function () {
|
||||
MockTrackChangesApi.flushDoc.reset()
|
||||
MockProjectHistoryApi.flushProject.reset()
|
||||
return MockWebApi.setDocument.reset()
|
||||
MockWebApi.setDocument.reset()
|
||||
})
|
||||
|
||||
return it('should not undo the tracked changes', function (done) {
|
||||
it('should not undo the tracked changes', function (done) {
|
||||
DocUpdaterClient.getDoc(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
(error, res, data) => {
|
||||
if (error != null) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
const { ranges } = data
|
||||
expect(ranges.changes.length).to.equal(1)
|
||||
return done()
|
||||
done()
|
||||
}
|
||||
)
|
||||
return null
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue