mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge branch 'master' into sk-fix-references-full-index
This commit is contained in:
commit
06c0b45ef7
25 changed files with 323 additions and 79 deletions
|
@ -43,7 +43,6 @@ Unit tests can be run in the `test_unit` container defined in `docker-compose.te
|
||||||
The makefile contains a short cut to run these:
|
The makefile contains a short cut to run these:
|
||||||
|
|
||||||
```
|
```
|
||||||
make install # Only needs running once, or when npm packages are updated
|
|
||||||
make unit_test
|
make unit_test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,13 @@ UnsupportedExportRecordsError = (message) ->
|
||||||
return error
|
return error
|
||||||
UnsupportedExportRecordsError.prototype.__proto___ = Error.prototype
|
UnsupportedExportRecordsError.prototype.__proto___ = Error.prototype
|
||||||
|
|
||||||
|
V1HistoryNotSyncedError = (message) ->
|
||||||
|
error = new Error(message)
|
||||||
|
error.name = "V1HistoryNotSyncedError"
|
||||||
|
error.__proto__ = V1HistoryNotSyncedError.prototype
|
||||||
|
return error
|
||||||
|
V1HistoryNotSyncedError.prototype.__proto___ = Error.prototype
|
||||||
|
|
||||||
ProjectHistoryDisabledError = (message) ->
|
ProjectHistoryDisabledError = (message) ->
|
||||||
error = new Error(message)
|
error = new Error(message)
|
||||||
error.name = "ProjectHistoryDisabledError "
|
error.name = "ProjectHistoryDisabledError "
|
||||||
|
@ -62,4 +69,5 @@ module.exports = Errors =
|
||||||
UnsupportedFileTypeError: UnsupportedFileTypeError
|
UnsupportedFileTypeError: UnsupportedFileTypeError
|
||||||
UnsupportedBrandError: UnsupportedBrandError
|
UnsupportedBrandError: UnsupportedBrandError
|
||||||
UnsupportedExportRecordsError: UnsupportedExportRecordsError
|
UnsupportedExportRecordsError: UnsupportedExportRecordsError
|
||||||
|
V1HistoryNotSyncedError: V1HistoryNotSyncedError
|
||||||
ProjectHistoryDisabledError: ProjectHistoryDisabledError
|
ProjectHistoryDisabledError: ProjectHistoryDisabledError
|
||||||
|
|
|
@ -299,6 +299,8 @@ module.exports = ProjectController =
|
||||||
autoPairDelimiters: user.ace.autoPairDelimiters
|
autoPairDelimiters: user.ace.autoPairDelimiters
|
||||||
pdfViewer : user.ace.pdfViewer
|
pdfViewer : user.ace.pdfViewer
|
||||||
syntaxValidation: user.ace.syntaxValidation
|
syntaxValidation: user.ace.syntaxValidation
|
||||||
|
fontFamily: user.ace.fontFamily
|
||||||
|
lineHeight: user.ace.lineHeight
|
||||||
}
|
}
|
||||||
trackChangesState: project.track_changes
|
trackChangesState: project.track_changes
|
||||||
privilegeLevel: privilegeLevel
|
privilegeLevel: privilegeLevel
|
||||||
|
@ -311,6 +313,7 @@ module.exports = ProjectController =
|
||||||
maxDocLength: Settings.max_doc_length
|
maxDocLength: Settings.max_doc_length
|
||||||
useV2History: !!project.overleaf?.history?.display
|
useV2History: !!project.overleaf?.history?.display
|
||||||
showRichText: req.query?.rt == 'true'
|
showRichText: req.query?.rt == 'true'
|
||||||
|
showPublishModal: req.query?.pm == 'true'
|
||||||
timer.done()
|
timer.done()
|
||||||
|
|
||||||
_buildProjectList: (allProjects, v1Projects = [])->
|
_buildProjectList: (allProjects, v1Projects = [])->
|
||||||
|
|
|
@ -16,38 +16,42 @@ AnalyticsManger = require("../Analytics/AnalyticsManager")
|
||||||
|
|
||||||
module.exports = ProjectCreationHandler =
|
module.exports = ProjectCreationHandler =
|
||||||
|
|
||||||
createBlankProject : (owner_id, projectName, projectHistoryId, callback = (error, project) ->)->
|
createBlankProject : (owner_id, projectName, attributes, callback = (error, project) ->)->
|
||||||
metrics.inc("project-creation")
|
metrics.inc("project-creation")
|
||||||
if arguments.length == 3
|
if arguments.length == 3
|
||||||
callback = projectHistoryId
|
callback = attributes
|
||||||
projectHistoryId = null
|
attributes = null
|
||||||
|
|
||||||
ProjectDetailsHandler.validateProjectName projectName, (error) ->
|
ProjectDetailsHandler.validateProjectName projectName, (error) ->
|
||||||
return callback(error) if error?
|
return callback(error) if error?
|
||||||
logger.log owner_id:owner_id, projectName:projectName, "creating blank project"
|
logger.log owner_id:owner_id, projectName:projectName, "creating blank project"
|
||||||
if projectHistoryId?
|
if attributes?
|
||||||
ProjectCreationHandler._createBlankProject owner_id, projectName, projectHistoryId, (error, project) ->
|
ProjectCreationHandler._createBlankProject owner_id, projectName, attributes, (error, project) ->
|
||||||
return callback(error) if error?
|
return callback(error) if error?
|
||||||
AnalyticsManger.recordEvent(
|
AnalyticsManger.recordEvent(
|
||||||
owner_id, 'project-imported', { projectId: project._id, projectHistoryId: projectHistoryId }
|
owner_id, 'project-imported', { projectId: project._id, attributes: attributes }
|
||||||
)
|
)
|
||||||
callback(error, project)
|
callback(error, project)
|
||||||
else
|
else
|
||||||
HistoryManager.initializeProject (error, history) ->
|
HistoryManager.initializeProject (error, history) ->
|
||||||
return callback(error) if error?
|
return callback(error) if error?
|
||||||
ProjectCreationHandler._createBlankProject owner_id, projectName, history?.overleaf_id, (error, project) ->
|
attributes = overleaf: history: id: history?.overleaf_id
|
||||||
|
ProjectCreationHandler._createBlankProject owner_id, projectName, attributes, (error, project) ->
|
||||||
return callback(error) if error?
|
return callback(error) if error?
|
||||||
AnalyticsManger.recordEvent(
|
AnalyticsManger.recordEvent(
|
||||||
owner_id, 'project-created', { projectId: project._id }
|
owner_id, 'project-created', { projectId: project._id }
|
||||||
)
|
)
|
||||||
callback(error, project)
|
callback(error, project)
|
||||||
|
|
||||||
_createBlankProject : (owner_id, projectName, projectHistoryId, callback = (error, project) ->)->
|
_createBlankProject : (owner_id, projectName, attributes, callback = (error, project) ->)->
|
||||||
rootFolder = new Folder {'name':'rootFolder'}
|
rootFolder = new Folder {'name':'rootFolder'}
|
||||||
project = new Project
|
|
||||||
owner_ref : new ObjectId(owner_id)
|
attributes.owner_ref = new ObjectId(owner_id)
|
||||||
name : projectName
|
attributes.name = projectName
|
||||||
project.overleaf.history.id = projectHistoryId
|
project = new Project attributes
|
||||||
|
|
||||||
|
Object.assign(project, attributes)
|
||||||
|
|
||||||
if Settings.apis?.project_history?.displayHistoryForNewProjects
|
if Settings.apis?.project_history?.displayHistoryForNewProjects
|
||||||
project.overleaf.history.display = true
|
project.overleaf.history.display = true
|
||||||
if Settings.currentImageName?
|
if Settings.currentImageName?
|
||||||
|
|
|
@ -405,14 +405,41 @@ module.exports = ProjectEntityUpdateHandler = self =
|
||||||
DocumentUpdaterHandler.resyncProjectHistory project_id, projectHistoryId, docs, files, callback
|
DocumentUpdaterHandler.resyncProjectHistory project_id, projectHistoryId, docs, files, callback
|
||||||
|
|
||||||
_cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) ->
|
_cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) ->
|
||||||
|
self._updateProjectStructureWithDeletedEntity project, entity, entityType, path, userId, (error) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
if(entityType.indexOf("file") != -1)
|
||||||
|
self._cleanUpFile project, entity, path, userId, callback
|
||||||
|
else if (entityType.indexOf("doc") != -1)
|
||||||
|
self._cleanUpDoc project, entity, path, userId, callback
|
||||||
|
else if (entityType.indexOf("folder") != -1)
|
||||||
|
self._cleanUpFolder project, entity, path, userId, callback
|
||||||
|
else
|
||||||
|
callback()
|
||||||
|
|
||||||
|
# Note: the _cleanUpEntity code and _updateProjectStructureWithDeletedEntity
|
||||||
|
# methods both need to recursively iterate over the entities in folder.
|
||||||
|
# These are currently using separate implementations of the recursion. In
|
||||||
|
# future, these could be simplified using a common project entity iterator.
|
||||||
|
_updateProjectStructureWithDeletedEntity: (project, entity, entityType, entityPath, userId, callback = (error) ->) ->
|
||||||
|
# compute the changes to the project structure
|
||||||
if(entityType.indexOf("file") != -1)
|
if(entityType.indexOf("file") != -1)
|
||||||
self._cleanUpFile project, entity, path, userId, callback
|
changes = oldFiles: [ {file: entity, path: entityPath} ]
|
||||||
else if (entityType.indexOf("doc") != -1)
|
else if (entityType.indexOf("doc") != -1)
|
||||||
self._cleanUpDoc project, entity, path, userId, callback
|
changes = oldDocs: [ {doc: entity, path: entityPath} ]
|
||||||
else if (entityType.indexOf("folder") != -1)
|
else if (entityType.indexOf("folder") != -1)
|
||||||
self._cleanUpFolder project, entity, path, userId, callback
|
changes = {oldDocs: [], oldFiles: []}
|
||||||
else
|
_recurseFolder = (folder, folderPath) ->
|
||||||
callback()
|
for doc in folder.docs
|
||||||
|
changes.oldDocs.push {doc, path: path.join(folderPath, doc.name)}
|
||||||
|
for file in folder.fileRefs
|
||||||
|
changes.oldFiles.push {file, path: path.join(folderPath, file.name)}
|
||||||
|
for childFolder in folder.folders
|
||||||
|
_recurseFolder(childFolder, path.join(folderPath, childFolder.name))
|
||||||
|
_recurseFolder entity, entityPath
|
||||||
|
# now send the project structure changes to the docupdater
|
||||||
|
project_id = project._id.toString()
|
||||||
|
projectHistoryId = project.overleaf?.history?.id
|
||||||
|
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
|
||||||
|
|
||||||
_cleanUpDoc: (project, doc, path, userId, callback = (error) ->) ->
|
_cleanUpDoc: (project, doc, path, userId, callback = (error) ->) ->
|
||||||
project_id = project._id.toString()
|
project_id = project._id.toString()
|
||||||
|
@ -429,21 +456,10 @@ module.exports = ProjectEntityUpdateHandler = self =
|
||||||
return callback(error) if error?
|
return callback(error) if error?
|
||||||
DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) ->
|
DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) ->
|
||||||
return callback(error) if error?
|
return callback(error) if error?
|
||||||
DocstoreManager.deleteDoc project_id, doc_id, (error) ->
|
DocstoreManager.deleteDoc project_id, doc_id, callback
|
||||||
return callback(error) if error?
|
|
||||||
changes = oldDocs: [ {doc, path} ]
|
|
||||||
projectHistoryId = project.overleaf?.history?.id
|
|
||||||
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
|
|
||||||
|
|
||||||
_cleanUpFile: (project, file, path, userId, callback = (error) ->) ->
|
_cleanUpFile: (project, file, path, userId, callback = (error) ->) ->
|
||||||
ProjectEntityMongoUpdateHandler._insertDeletedFileReference project._id, file, (error) ->
|
ProjectEntityMongoUpdateHandler._insertDeletedFileReference project._id, file, callback
|
||||||
return callback(error) if error?
|
|
||||||
project_id = project._id.toString()
|
|
||||||
projectHistoryId = project.overleaf?.history?.id
|
|
||||||
changes = oldFiles: [ {file, path} ]
|
|
||||||
# we are now keeping a copy of every file versio so we no longer delete
|
|
||||||
# the file from the filestore
|
|
||||||
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
|
|
||||||
|
|
||||||
_cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) ->
|
_cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) ->
|
||||||
jobs = []
|
jobs = []
|
||||||
|
|
|
@ -81,6 +81,11 @@ module.exports = UserController =
|
||||||
user.ace.pdfViewer = req.body.pdfViewer
|
user.ace.pdfViewer = req.body.pdfViewer
|
||||||
if req.body.syntaxValidation?
|
if req.body.syntaxValidation?
|
||||||
user.ace.syntaxValidation = req.body.syntaxValidation
|
user.ace.syntaxValidation = req.body.syntaxValidation
|
||||||
|
if req.body.fontFamily?
|
||||||
|
user.ace.fontFamily = req.body.fontFamily
|
||||||
|
if req.body.lineHeight?
|
||||||
|
user.ace.lineHeight = req.body.lineHeight
|
||||||
|
|
||||||
user.save (err)->
|
user.save (err)->
|
||||||
newEmail = req.body.email?.trim().toLowerCase()
|
newEmail = req.body.email?.trim().toLowerCase()
|
||||||
if !newEmail? or newEmail == user.email or req.externalAuthenticationSystemUsed()
|
if !newEmail? or newEmail == user.email or req.externalAuthenticationSystemUsed()
|
||||||
|
|
|
@ -183,6 +183,9 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
|
||||||
# Don't include the query string parameters, otherwise Google
|
# Don't include the query string parameters, otherwise Google
|
||||||
# treats ?nocdn=true as the canonical version
|
# treats ?nocdn=true as the canonical version
|
||||||
res.locals.currentUrl = Url.parse(req.originalUrl).pathname
|
res.locals.currentUrl = Url.parse(req.originalUrl).pathname
|
||||||
|
res.locals.capitalize = (string) ->
|
||||||
|
return "" if string.length == 0
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||||
next()
|
next()
|
||||||
|
|
||||||
webRouter.use (req, res, next)->
|
webRouter.use (req, res, next)->
|
||||||
|
@ -321,5 +324,7 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
|
||||||
chatMessageBorderLightness : if isOl then "40%" else "70%"
|
chatMessageBorderLightness : if isOl then "40%" else "70%"
|
||||||
chatMessageBgSaturation : if isOl then "85%" else "60%"
|
chatMessageBgSaturation : if isOl then "85%" else "60%"
|
||||||
chatMessageBgLightness : if isOl then "40%" else "97%"
|
chatMessageBgLightness : if isOl then "40%" else "97%"
|
||||||
|
defaultFontFamily : if isOl then 'lucida' else 'monaco'
|
||||||
|
defaultLineHeight : if isOl then 'normal' else 'compact'
|
||||||
renderAnnouncements : !isOl
|
renderAnnouncements : !isOl
|
||||||
next()
|
next()
|
||||||
|
|
|
@ -14,9 +14,9 @@ module.exports = Features =
|
||||||
return Settings.enableGithubSync
|
return Settings.enableGithubSync
|
||||||
when 'v1-return-message'
|
when 'v1-return-message'
|
||||||
return Settings.accountMerge? and Settings.overleaf?
|
return Settings.accountMerge? and Settings.overleaf?
|
||||||
when 'publish-modal'
|
|
||||||
return Settings.showPublishModal
|
|
||||||
when 'custom-togglers'
|
when 'custom-togglers'
|
||||||
return Settings.overleaf?
|
return Settings.overleaf?
|
||||||
|
when 'templates'
|
||||||
|
return !Settings.overleaf?
|
||||||
else
|
else
|
||||||
throw new Error("unknown feature: #{feature}")
|
throw new Error("unknown feature: #{feature}")
|
||||||
|
|
|
@ -3,20 +3,37 @@ Settings = require('settings-sharelatex')
|
||||||
RedisWrapper = require("./RedisWrapper")
|
RedisWrapper = require("./RedisWrapper")
|
||||||
rclient = RedisWrapper.client("lock")
|
rclient = RedisWrapper.client("lock")
|
||||||
logger = require "logger-sharelatex"
|
logger = require "logger-sharelatex"
|
||||||
|
os = require "os"
|
||||||
|
crypto = require "crypto"
|
||||||
|
|
||||||
|
HOST = os.hostname()
|
||||||
|
PID = process.pid
|
||||||
|
RND = crypto.randomBytes(4).toString('hex')
|
||||||
|
COUNT = 0
|
||||||
|
|
||||||
module.exports = LockManager =
|
module.exports = LockManager =
|
||||||
LOCK_TEST_INTERVAL: 50 # 50ms between each test of the lock
|
LOCK_TEST_INTERVAL: 50 # 50ms between each test of the lock
|
||||||
|
MAX_TEST_INTERVAL: 1000 # back off to 1s between each test of the lock
|
||||||
MAX_LOCK_WAIT_TIME: 10000 # 10s maximum time to spend trying to get the lock
|
MAX_LOCK_WAIT_TIME: 10000 # 10s maximum time to spend trying to get the lock
|
||||||
REDIS_LOCK_EXPIRY: 30 # seconds. Time until lock auto expires in redis
|
REDIS_LOCK_EXPIRY: 30 # seconds. Time until lock auto expires in redis
|
||||||
SLOW_EXECUTION_THRESHOLD: 5000 # 5s, if execution takes longer than this then log
|
SLOW_EXECUTION_THRESHOLD: 5000 # 5s, if execution takes longer than this then log
|
||||||
|
|
||||||
|
# Use a signed lock value as described in
|
||||||
|
# http://redis.io/topics/distlock#correct-implementation-with-a-single-instance
|
||||||
|
# to prevent accidental unlocking by multiple processes
|
||||||
|
randomLock : () ->
|
||||||
|
time = Date.now()
|
||||||
|
return "locked:host=#{HOST}:pid=#{PID}:random=#{RND}:time=#{time}:count=#{COUNT++}"
|
||||||
|
|
||||||
|
unlockScript: 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end'
|
||||||
|
|
||||||
runWithLock: (namespace, id, runner = ( (releaseLock = (error) ->) -> ), callback = ( (error) -> )) ->
|
runWithLock: (namespace, id, runner = ( (releaseLock = (error) ->) -> ), callback = ( (error) -> )) ->
|
||||||
# This error is defined here so we get a useful stacktrace
|
# This error is defined here so we get a useful stacktrace
|
||||||
slowExecutionError = new Error "slow execution during lock"
|
slowExecutionError = new Error "slow execution during lock"
|
||||||
|
|
||||||
timer = new metrics.Timer("lock.#{namespace}")
|
timer = new metrics.Timer("lock.#{namespace}")
|
||||||
key = "lock:web:#{namespace}:#{id}"
|
key = "lock:web:#{namespace}:#{id}"
|
||||||
LockManager._getLock key, namespace, (error) ->
|
LockManager._getLock key, namespace, (error, lockValue) ->
|
||||||
return callback(error) if error?
|
return callback(error) if error?
|
||||||
|
|
||||||
# The lock can expire in redis but the process carry on. This setTimout call
|
# The lock can expire in redis but the process carry on. This setTimout call
|
||||||
|
@ -27,7 +44,7 @@ module.exports = LockManager =
|
||||||
exceededLockTimeout = setTimeout countIfExceededLockTimeout, LockManager.REDIS_LOCK_EXPIRY * 1000
|
exceededLockTimeout = setTimeout countIfExceededLockTimeout, LockManager.REDIS_LOCK_EXPIRY * 1000
|
||||||
|
|
||||||
runner (error1, values...) ->
|
runner (error1, values...) ->
|
||||||
LockManager._releaseLock key, (error2) ->
|
LockManager._releaseLock key, lockValue, (error2) ->
|
||||||
clearTimeout exceededLockTimeout
|
clearTimeout exceededLockTimeout
|
||||||
|
|
||||||
timeTaken = new Date - timer.start
|
timeTaken = new Date - timer.start
|
||||||
|
@ -39,19 +56,21 @@ module.exports = LockManager =
|
||||||
return callback(error) if error?
|
return callback(error) if error?
|
||||||
callback null, values...
|
callback null, values...
|
||||||
|
|
||||||
_tryLock : (key, namespace, callback = (err, isFree)->)->
|
_tryLock : (key, namespace, callback = (err, isFree, lockValue)->)->
|
||||||
rclient.set key, "locked", "EX", LockManager.REDIS_LOCK_EXPIRY, "NX", (err, gotLock)->
|
lockValue = LockManager.randomLock()
|
||||||
|
rclient.set key, lockValue, "EX", LockManager.REDIS_LOCK_EXPIRY, "NX", (err, gotLock)->
|
||||||
return callback(err) if err?
|
return callback(err) if err?
|
||||||
if gotLock == "OK"
|
if gotLock == "OK"
|
||||||
metrics.inc "lock.#{namespace}.try.success"
|
metrics.inc "lock.#{namespace}.try.success"
|
||||||
callback err, true
|
callback err, true, lockValue
|
||||||
else
|
else
|
||||||
metrics.inc "lock.#{namespace}.try.failed"
|
metrics.inc "lock.#{namespace}.try.failed"
|
||||||
logger.log key: key, redis_response: gotLock, "lock is locked"
|
logger.log key: key, redis_response: gotLock, "lock is locked"
|
||||||
callback err, false
|
callback err, false
|
||||||
|
|
||||||
_getLock: (key, namespace, callback = (error) ->) ->
|
_getLock: (key, namespace, callback = (error, lockValue) ->) ->
|
||||||
startTime = Date.now()
|
startTime = Date.now()
|
||||||
|
testInterval = LockManager.LOCK_TEST_INTERVAL
|
||||||
attempts = 0
|
attempts = 0
|
||||||
do attempt = () ->
|
do attempt = () ->
|
||||||
if Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME
|
if Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME
|
||||||
|
@ -59,13 +78,23 @@ module.exports = LockManager =
|
||||||
return callback(new Error("Timeout"))
|
return callback(new Error("Timeout"))
|
||||||
|
|
||||||
attempts += 1
|
attempts += 1
|
||||||
LockManager._tryLock key, namespace, (error, gotLock) ->
|
LockManager._tryLock key, namespace, (error, gotLock, lockValue) ->
|
||||||
return callback(error) if error?
|
return callback(error) if error?
|
||||||
if gotLock
|
if gotLock
|
||||||
metrics.gauge "lock.#{namespace}.get.success.tries", attempts
|
metrics.gauge "lock.#{namespace}.get.success.tries", attempts
|
||||||
callback(null)
|
callback(null, lockValue)
|
||||||
else
|
else
|
||||||
setTimeout attempt, LockManager.LOCK_TEST_INTERVAL
|
setTimeout attempt, testInterval
|
||||||
|
# back off when the lock is taken to avoid overloading
|
||||||
|
testInterval = Math.min(testInterval * 2, LockManager.MAX_TEST_INTERVAL)
|
||||||
|
|
||||||
_releaseLock: (key, callback)->
|
_releaseLock: (key, lockValue, callback)->
|
||||||
rclient.del key, callback
|
rclient.eval LockManager.unlockScript, 1, key, lockValue, (err, result) ->
|
||||||
|
if err?
|
||||||
|
return callback(err)
|
||||||
|
else if result? and result isnt 1 # successful unlock should release exactly one key
|
||||||
|
logger.error {key:key, lockValue:lockValue, redis_err:err, redis_result:result}, "unlocking error"
|
||||||
|
metrics.inc "unlock-error"
|
||||||
|
return callback(new Error("tried to release timed out lock"))
|
||||||
|
else
|
||||||
|
callback(null,result)
|
||||||
|
|
|
@ -20,14 +20,16 @@ UserSchema = new Schema
|
||||||
loginCount : {type : Number, default: 0}
|
loginCount : {type : Number, default: 0}
|
||||||
holdingAccount : {type : Boolean, default: false}
|
holdingAccount : {type : Boolean, default: false}
|
||||||
ace : {
|
ace : {
|
||||||
mode : {type : String, default: 'none'}
|
mode : {type : String, default: 'none'}
|
||||||
theme : {type : String, default: 'textmate'}
|
theme : {type : String, default: 'textmate'}
|
||||||
fontSize : {type : Number, default:'12'}
|
fontSize : {type : Number, default:'12'}
|
||||||
autoComplete: {type : Boolean, default: true}
|
autoComplete : {type : Boolean, default: true}
|
||||||
autoPairDelimiters: {type : Boolean, default: true}
|
autoPairDelimiters : {type : Boolean, default: true}
|
||||||
spellCheckLanguage : {type : String, default: "en"}
|
spellCheckLanguage : {type : String, default: "en"}
|
||||||
pdfViewer : {type : String, default: "pdfjs"}
|
pdfViewer : {type : String, default: "pdfjs"}
|
||||||
syntaxValidation : {type : Boolean}
|
syntaxValidation : {type : Boolean}
|
||||||
|
fontFamily : {type : String}
|
||||||
|
lineHeight : {type : String}
|
||||||
}
|
}
|
||||||
features : {
|
features : {
|
||||||
collaborators: { type:Number, default: Settings.defaultFeatures.collaborators }
|
collaborators: { type:Number, default: Settings.defaultFeatures.collaborators }
|
||||||
|
|
|
@ -154,7 +154,10 @@ block requirejs
|
||||||
},
|
},
|
||||||
"ace/ext-language_tools": {
|
"ace/ext-language_tools": {
|
||||||
"deps": ["ace/ace"]
|
"deps": ["ace/ace"]
|
||||||
}
|
},
|
||||||
|
"ace/keybinding-vim": {
|
||||||
|
"deps": ["ace/ace"]
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"config":{
|
"config":{
|
||||||
"moment":{
|
"moment":{
|
||||||
|
|
|
@ -58,6 +58,7 @@ div.full-size(
|
||||||
read-only="!permissions.write",
|
read-only="!permissions.write",
|
||||||
file-name="editor.open_doc_name",
|
file-name="editor.open_doc_name",
|
||||||
on-ctrl-enter="recompileViaKey",
|
on-ctrl-enter="recompileViaKey",
|
||||||
|
on-save="recompileViaKey",
|
||||||
on-ctrl-j="toggleReviewPanel",
|
on-ctrl-j="toggleReviewPanel",
|
||||||
on-ctrl-shift-c="addNewCommentFromKbdShortcut",
|
on-ctrl-shift-c="addNewCommentFromKbdShortcut",
|
||||||
on-ctrl-shift-a="toggleTrackChangesFromKbdShortcut",
|
on-ctrl-shift-a="toggleTrackChangesFromKbdShortcut",
|
||||||
|
@ -68,6 +69,8 @@ div.full-size(
|
||||||
track-changes= "editor.trackChanges",
|
track-changes= "editor.trackChanges",
|
||||||
doc-id="editor.open_doc_id"
|
doc-id="editor.open_doc_id"
|
||||||
renderer-data="reviewPanel.rendererData"
|
renderer-data="reviewPanel.rendererData"
|
||||||
|
font-family="settings.fontFamily || ui.defaultFontFamily"
|
||||||
|
line-height="settings.lineHeight || ui.defaultLineHeight"
|
||||||
)
|
)
|
||||||
|
|
||||||
!= moduleIncludes('editor:body', locals)
|
!= moduleIncludes('editor:body', locals)
|
||||||
|
|
|
@ -150,6 +150,26 @@ aside#left-menu.full-size(
|
||||||
each size in ['10','11','12','13','14','16','20','24']
|
each size in ['10','11','12','13','14','16','20','24']
|
||||||
option(value=size) #{size}px
|
option(value=size) #{size}px
|
||||||
|
|
||||||
|
.form-controls
|
||||||
|
label(for="fontFamily") #{translate("font_family")}
|
||||||
|
select(
|
||||||
|
name="fontFamily"
|
||||||
|
ng-model="settings.fontFamily"
|
||||||
|
)
|
||||||
|
option(value="", disabled) #{translate("default")}
|
||||||
|
each fontFamily in ['monaco', 'lucida']
|
||||||
|
option(value=fontFamily) #{capitalize(fontFamily)}
|
||||||
|
|
||||||
|
.form-controls
|
||||||
|
label(for="lineHeight") #{translate("line_height")}
|
||||||
|
select(
|
||||||
|
name="lineHeight"
|
||||||
|
ng-model="settings.lineHeight"
|
||||||
|
)
|
||||||
|
option(value="", disabled) #{translate("default")}
|
||||||
|
each lineHeight in ['compact', 'normal', 'wide']
|
||||||
|
option(value=lineHeight) #{translate(lineHeight)}
|
||||||
|
|
||||||
.form-controls
|
.form-controls
|
||||||
label(for="pdfViewer") #{translate("pdf_viewer")}
|
label(for="pdfViewer") #{translate("pdf_viewer")}
|
||||||
select(
|
select(
|
||||||
|
|
|
@ -32,14 +32,16 @@
|
||||||
ng-click="downloadSelectedProjects()"
|
ng-click="downloadSelectedProjects()"
|
||||||
)
|
)
|
||||||
i.fa.fa-cloud-download
|
i.fa.fa-cloud-download
|
||||||
|
- var archiveButtonString = settings.overleaf ? translate("archive") : translate("delete")
|
||||||
|
- var archiveButtonIcon = settings.overleaf ? "fa-inbox" : "fa-trash-o"
|
||||||
a.btn.btn-default(
|
a.btn.btn-default(
|
||||||
href,
|
href,
|
||||||
tooltip=translate('delete'),
|
tooltip=`{{ isArchiveableProjectSelected ? '${archiveButtonString}' : '${translate("leave")}' }}`,
|
||||||
tooltip-placement="bottom",
|
tooltip-placement="bottom",
|
||||||
tooltip-append-to-body="true",
|
tooltip-append-to-body="true",
|
||||||
ng-click="openArchiveProjectsModal()"
|
ng-click="openArchiveProjectsModal()"
|
||||||
)
|
)
|
||||||
i.fa.fa-trash-o
|
i.fa(ng-class=`isArchiveableProjectSelected ? '${archiveButtonIcon}' : 'fa-sign-out'`)
|
||||||
|
|
||||||
.btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown)
|
.btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown)
|
||||||
a.btn.btn-default.dropdown-toggle(
|
a.btn.btn-default.dropdown-toggle(
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
li(ng-class="{active: (filter == 'shared')}", ng-click="filterProjects('shared')")
|
li(ng-class="{active: (filter == 'shared')}", ng-click="filterProjects('shared')")
|
||||||
a(href) #{translate("shared_with_you")}
|
a(href) #{translate("shared_with_you")}
|
||||||
li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')")
|
li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')")
|
||||||
a(href) #{translate("deleted_projects")}
|
a(href) #{settings.overleaf ? translate("archived_projects") : translate("deleted_projects")}
|
||||||
if isShowingV1Projects
|
if isShowingV1Projects
|
||||||
li(ng-class="{active: (filter == 'v1')}", ng-click="filterProjects('v1')")
|
li(ng-class="{active: (filter == 'v1')}", ng-click="filterProjects('v1')")
|
||||||
a(href) #{translate("v1_projects")}
|
a(href) #{translate("v1_projects")}
|
||||||
|
|
|
@ -80,6 +80,8 @@ define [
|
||||||
miniReviewPanelVisible: false
|
miniReviewPanelVisible: false
|
||||||
chatResizerSizeOpen: window.uiConfig.chatResizerSizeOpen
|
chatResizerSizeOpen: window.uiConfig.chatResizerSizeOpen
|
||||||
chatResizerSizeClosed: window.uiConfig.chatResizerSizeClosed
|
chatResizerSizeClosed: window.uiConfig.chatResizerSizeClosed
|
||||||
|
defaultFontFamily: window.uiConfig.defaultFontFamily
|
||||||
|
defaultLineHeight: window.uiConfig.defaultLineHeight
|
||||||
}
|
}
|
||||||
$scope.user = window.user
|
$scope.user = window.user
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ define [
|
||||||
"ace/ace"
|
"ace/ace"
|
||||||
"ace/ext-searchbox"
|
"ace/ext-searchbox"
|
||||||
"ace/ext-modelist"
|
"ace/ext-modelist"
|
||||||
|
"ace/keybinding-vim"
|
||||||
"ide/editor/directives/aceEditor/undo/UndoManager"
|
"ide/editor/directives/aceEditor/undo/UndoManager"
|
||||||
"ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager"
|
"ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager"
|
||||||
"ide/editor/directives/aceEditor/spell-check/SpellCheckManager"
|
"ide/editor/directives/aceEditor/spell-check/SpellCheckManager"
|
||||||
|
@ -14,9 +15,10 @@ define [
|
||||||
"ide/graphics/services/graphics"
|
"ide/graphics/services/graphics"
|
||||||
"ide/preamble/services/preamble"
|
"ide/preamble/services/preamble"
|
||||||
"ide/files/services/files"
|
"ide/files/services/files"
|
||||||
], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager) ->
|
], (App, Ace, SearchBox, Vim, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager) ->
|
||||||
EditSession = ace.require('ace/edit_session').EditSession
|
EditSession = ace.require('ace/edit_session').EditSession
|
||||||
ModeList = ace.require('ace/ext/modelist')
|
ModeList = ace.require('ace/ext/modelist')
|
||||||
|
Vim = ace.require('ace/keyboard/vim').Vim
|
||||||
|
|
||||||
# set the path for ace workers if using a CDN (from editor.pug)
|
# set the path for ace workers if using a CDN (from editor.pug)
|
||||||
if window.aceWorkerPath != ""
|
if window.aceWorkerPath != ""
|
||||||
|
@ -60,6 +62,7 @@ define [
|
||||||
onCtrlJ: "=" # Toggle the review panel
|
onCtrlJ: "=" # Toggle the review panel
|
||||||
onCtrlShiftC: "=" # Add a new comment
|
onCtrlShiftC: "=" # Add a new comment
|
||||||
onCtrlShiftA: "=" # Toggle track-changes on/off
|
onCtrlShiftA: "=" # Toggle track-changes on/off
|
||||||
|
onSave: "=" # Cmd/Ctrl-S or :w in Vim
|
||||||
syntaxValidation: "="
|
syntaxValidation: "="
|
||||||
reviewPanel: "="
|
reviewPanel: "="
|
||||||
eventsBridge: "="
|
eventsBridge: "="
|
||||||
|
@ -67,6 +70,8 @@ define [
|
||||||
trackChangesEnabled: "="
|
trackChangesEnabled: "="
|
||||||
docId: "="
|
docId: "="
|
||||||
rendererData: "="
|
rendererData: "="
|
||||||
|
lineHeight: "="
|
||||||
|
fontFamily: "="
|
||||||
}
|
}
|
||||||
link: (scope, element, attrs) ->
|
link: (scope, element, attrs) ->
|
||||||
# Don't freak out if we're already in an apply callback
|
# Don't freak out if we're already in an apply callback
|
||||||
|
@ -106,16 +111,26 @@ define [
|
||||||
metadataManager = new MetadataManager(scope, editor, element, metadata)
|
metadataManager = new MetadataManager(scope, editor, element, metadata)
|
||||||
autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, graphics, preamble, files)
|
autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, graphics, preamble, files)
|
||||||
|
|
||||||
# Prevert Ctrl|Cmd-S from triggering save dialog
|
scope.$watch "onSave", (callback) ->
|
||||||
editor.commands.addCommand
|
if callback?
|
||||||
name: "save",
|
Vim.defineEx 'write', 'w', callback
|
||||||
bindKey: win: "Ctrl-S", mac: "Command-S"
|
editor.commands.addCommand
|
||||||
exec: () ->
|
name: "save",
|
||||||
readOnly: true
|
bindKey: win: "Ctrl-S", mac: "Command-S"
|
||||||
|
exec: callback
|
||||||
|
readOnly: true
|
||||||
|
# Not technically 'save', but Ctrl-. recompiles in OL v1
|
||||||
|
# so maintain compatibility
|
||||||
|
editor.commands.addCommand
|
||||||
|
name: "recompile_v1",
|
||||||
|
bindKey: win: "Ctrl-.", mac: "Ctrl-."
|
||||||
|
exec: callback
|
||||||
|
readOnly: true
|
||||||
editor.commands.removeCommand "transposeletters"
|
editor.commands.removeCommand "transposeletters"
|
||||||
editor.commands.removeCommand "showSettingsMenu"
|
editor.commands.removeCommand "showSettingsMenu"
|
||||||
editor.commands.removeCommand "foldall"
|
editor.commands.removeCommand "foldall"
|
||||||
|
|
||||||
|
|
||||||
# For European keyboards, the / is above 7 so needs Shift pressing.
|
# For European keyboards, the / is above 7 so needs Shift pressing.
|
||||||
# This comes through as Command-Shift-/ on OS X, which is mapped to
|
# This comes through as Command-Shift-/ on OS X, which is mapped to
|
||||||
# toggleBlockComment.
|
# toggleBlockComment.
|
||||||
|
@ -266,6 +281,29 @@ define [
|
||||||
"font-size": value + "px"
|
"font-size": value + "px"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
scope.$watch "fontFamily", (value) ->
|
||||||
|
if value?
|
||||||
|
switch value
|
||||||
|
when 'monaco'
|
||||||
|
editor.setOption('fontFamily', '"Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", monospace')
|
||||||
|
when 'lucida'
|
||||||
|
editor.setOption('fontFamily', '"Lucida Console", monospace')
|
||||||
|
else
|
||||||
|
editor.setOption('fontFamily', null)
|
||||||
|
|
||||||
|
scope.$watch "lineHeight", (value) ->
|
||||||
|
if value?
|
||||||
|
switch value
|
||||||
|
when 'compact'
|
||||||
|
editor.container.style.lineHeight = 1.33
|
||||||
|
when 'normal'
|
||||||
|
editor.container.style.lineHeight = 1.6
|
||||||
|
when 'wide'
|
||||||
|
editor.container.style.lineHeight = 2
|
||||||
|
else
|
||||||
|
editor.container.style.lineHeight = 1.6
|
||||||
|
editor.renderer.updateFontSize()
|
||||||
|
|
||||||
scope.$watch "sharejsDoc", (sharejs_doc, old_sharejs_doc) ->
|
scope.$watch "sharejsDoc", (sharejs_doc, old_sharejs_doc) ->
|
||||||
if old_sharejs_doc?
|
if old_sharejs_doc?
|
||||||
detachFromAce(old_sharejs_doc)
|
detachFromAce(old_sharejs_doc)
|
||||||
|
|
|
@ -8,6 +8,12 @@ define [
|
||||||
if $scope.settings.pdfViewer not in ["pdfjs", "native"]
|
if $scope.settings.pdfViewer not in ["pdfjs", "native"]
|
||||||
$scope.settings.pdfViewer = "pdfjs"
|
$scope.settings.pdfViewer = "pdfjs"
|
||||||
|
|
||||||
|
if $scope.settings.fontFamily? and $scope.settings.fontFamily not in ["monaco", "lucida"]
|
||||||
|
delete $scope.settings.fontFamily
|
||||||
|
|
||||||
|
if $scope.settings.lineHeight? and $scope.settings.lineHeight not in ["compact", "normal", "wide"]
|
||||||
|
delete $scope.settings.lineHeight
|
||||||
|
|
||||||
$scope.fontSizeAsStr = (newVal) ->
|
$scope.fontSizeAsStr = (newVal) ->
|
||||||
if newVal?
|
if newVal?
|
||||||
$scope.settings.fontSize = newVal
|
$scope.settings.fontSize = newVal
|
||||||
|
@ -41,6 +47,14 @@ define [
|
||||||
if syntaxValidation != oldSyntaxValidation
|
if syntaxValidation != oldSyntaxValidation
|
||||||
settings.saveSettings({syntaxValidation: syntaxValidation})
|
settings.saveSettings({syntaxValidation: syntaxValidation})
|
||||||
|
|
||||||
|
$scope.$watch "settings.fontFamily", (fontFamily, oldFontFamily) =>
|
||||||
|
if fontFamily != oldFontFamily
|
||||||
|
settings.saveSettings({fontFamily: fontFamily})
|
||||||
|
|
||||||
|
$scope.$watch "settings.lineHeight", (lineHeight, oldLineHeight) =>
|
||||||
|
if lineHeight != oldLineHeight
|
||||||
|
settings.saveSettings({lineHeight: lineHeight})
|
||||||
|
|
||||||
$scope.$watch "project.spellCheckLanguage", (language, oldLanguage) =>
|
$scope.$watch "project.spellCheckLanguage", (language, oldLanguage) =>
|
||||||
return if @ignoreUpdates
|
return if @ignoreUpdates
|
||||||
if oldLanguage? and language != oldLanguage
|
if oldLanguage? and language != oldLanguage
|
||||||
|
|
|
@ -8,6 +8,7 @@ define [
|
||||||
$scope.notifications = window.data.notifications
|
$scope.notifications = window.data.notifications
|
||||||
$scope.allSelected = false
|
$scope.allSelected = false
|
||||||
$scope.selectedProjects = []
|
$scope.selectedProjects = []
|
||||||
|
$scope.isArchiveableProjectSelected = false
|
||||||
$scope.filter = "all"
|
$scope.filter = "all"
|
||||||
$scope.predicate = "lastUpdated"
|
$scope.predicate = "lastUpdated"
|
||||||
$scope.nUntagged = 0
|
$scope.nUntagged = 0
|
||||||
|
@ -85,6 +86,8 @@ define [
|
||||||
|
|
||||||
$scope.updateSelectedProjects = () ->
|
$scope.updateSelectedProjects = () ->
|
||||||
$scope.selectedProjects = $scope.projects.filter (project) -> project.selected
|
$scope.selectedProjects = $scope.projects.filter (project) -> project.selected
|
||||||
|
$scope.isArchiveableProjectSelected = $scope.selectedProjects.some (project) ->
|
||||||
|
window.user_id == project.owner._id
|
||||||
|
|
||||||
$scope.getSelectedProjects = () ->
|
$scope.getSelectedProjects = () ->
|
||||||
$scope.selectedProjects
|
$scope.selectedProjects
|
||||||
|
|
71
services/web/public/js/ace-1.2.5/theme-overleaf.js
Normal file
71
services/web/public/js/ace-1.2.5/theme-overleaf.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
ace.define("ace/theme/overleaf",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
exports.isDark = false;
|
||||||
|
exports.cssClass = "ace-overleaf";
|
||||||
|
exports.cssText = ".ace-overleaf .ace_gutter {\
|
||||||
|
background: #f0f0f0;\
|
||||||
|
color: #333;\
|
||||||
|
}\
|
||||||
|
.ace-overleaf .ace_print-margin {\
|
||||||
|
width: 1px;\
|
||||||
|
background: #e8e8e8;\
|
||||||
|
}\
|
||||||
|
.ace-overleaf {\
|
||||||
|
background-color: #FFFFFF;\
|
||||||
|
color: black;\
|
||||||
|
}\
|
||||||
|
.ace-overleaf .ace_cursor {\
|
||||||
|
color: black;\
|
||||||
|
}\
|
||||||
|
.ace-overleaf .ace_marker-layer .ace_selection {\
|
||||||
|
background: rgb(181, 213, 255);\
|
||||||
|
}\
|
||||||
|
.ace-overleaf.ace_multiselect .ace_selection.ace_start {\
|
||||||
|
box-shadow: 0 0 3px 0px white;\
|
||||||
|
}\
|
||||||
|
.ace-overleaf .ace_marker-layer .ace_step {\
|
||||||
|
background: rgb(252, 255, 0);\
|
||||||
|
}\
|
||||||
|
.ace-overleaf .ace_marker-layer .ace_bracket {\
|
||||||
|
border: 1px solid #5A5CAD;\
|
||||||
|
}\
|
||||||
|
.ace-overleaf .ace_marker-layer .ace_active-line {\
|
||||||
|
background: rgba(0, 0, 0, 0.07);\
|
||||||
|
}\
|
||||||
|
.ace-overleaf .ace_gutter-active-line {\
|
||||||
|
background-color: #dcdcdc;\
|
||||||
|
}\
|
||||||
|
.ace-overleaf .ace_marker-layer .ace_selected-word {\
|
||||||
|
background: rgb(250, 250, 255);\
|
||||||
|
border: 1px solid rgb(200, 200, 250);\
|
||||||
|
}\
|
||||||
|
.ace-overleaf .ace_fold {\
|
||||||
|
background-color: #6B72E6;\
|
||||||
|
}\
|
||||||
|
.ace-overleaf .ace_comment {\
|
||||||
|
color: #0080FF;\
|
||||||
|
font-style: italic;\
|
||||||
|
}\
|
||||||
|
.ace-overleaf .ace_storage,\
|
||||||
|
.ace-overleaf .ace_keyword {\
|
||||||
|
color: #3F7F7F;\
|
||||||
|
}\
|
||||||
|
.ace-overleaf .ace_variable,\
|
||||||
|
.ace-overleaf .ace_string {\
|
||||||
|
color: #5A5CAD;\
|
||||||
|
}\
|
||||||
|
";
|
||||||
|
exports.$id = "ace/theme/overleaf";
|
||||||
|
|
||||||
|
var dom = require("../lib/dom");
|
||||||
|
dom.importCssString(exports.cssText, exports.cssClass);
|
||||||
|
});
|
||||||
|
(function() {
|
||||||
|
ace.require(["ace/theme/overleaf"], function(m) {
|
||||||
|
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||||
|
module.exports = m;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
|
@ -22,6 +22,7 @@ describe "ProjectStructureMongoLock", ->
|
||||||
before (done) ->
|
before (done) ->
|
||||||
# We want to instantly fail if the lock is taken
|
# We want to instantly fail if the lock is taken
|
||||||
LockManager.MAX_LOCK_WAIT_TIME = 1
|
LockManager.MAX_LOCK_WAIT_TIME = 1
|
||||||
|
@lockValue = "lock-value"
|
||||||
userDetails =
|
userDetails =
|
||||||
holdingAccount:false,
|
holdingAccount:false,
|
||||||
email: 'test@example.com'
|
email: 'test@example.com'
|
||||||
|
@ -33,11 +34,13 @@ describe "ProjectStructureMongoLock", ->
|
||||||
@locked_project = project
|
@locked_project = project
|
||||||
namespace = ProjectEntityMongoUpdateHandler.LOCK_NAMESPACE
|
namespace = ProjectEntityMongoUpdateHandler.LOCK_NAMESPACE
|
||||||
@lock_key = "lock:web:#{namespace}:#{project._id}"
|
@lock_key = "lock:web:#{namespace}:#{project._id}"
|
||||||
LockManager._getLock @lock_key, namespace, done
|
LockManager._getLock @lock_key, namespace, (err, lockValue) =>
|
||||||
|
@lockValue = lockValue
|
||||||
|
done()
|
||||||
return
|
return
|
||||||
|
|
||||||
after (done) ->
|
after (done) ->
|
||||||
LockManager._releaseLock @lock_key, done
|
LockManager._releaseLock @lock_key, @lockValue, done
|
||||||
|
|
||||||
describe 'interacting with the locked project', ->
|
describe 'interacting with the locked project', ->
|
||||||
LOCKING_UPDATE_METHODS = ['addDoc', 'addFile', 'mkdirp', 'moveEntity', 'renameEntity', 'addFolder']
|
LOCKING_UPDATE_METHODS = ['addDoc', 'addFile', 'mkdirp', 'moveEntity', 'renameEntity', 'addFolder']
|
||||||
|
|
|
@ -98,7 +98,11 @@ describe 'ProjectCreationHandler', ->
|
||||||
|
|
||||||
it "should set the overleaf id if overleaf id provided", (done)->
|
it "should set the overleaf id if overleaf id provided", (done)->
|
||||||
overleaf_id = 2345
|
overleaf_id = 2345
|
||||||
@handler.createBlankProject ownerId, projectName, overleaf_id, (err, project)->
|
attributes =
|
||||||
|
overleaf:
|
||||||
|
history:
|
||||||
|
id: overleaf_id
|
||||||
|
@handler.createBlankProject ownerId, projectName, attributes, (err, project)->
|
||||||
project.overleaf.history.id.should.equal overleaf_id
|
project.overleaf.history.id.should.equal overleaf_id
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
|
|
@ -872,6 +872,12 @@ describe 'ProjectEntityUpdateHandler', ->
|
||||||
.calledWith(@project, @entity, @path, userId)
|
.calledWith(@project, @entity, @path, userId)
|
||||||
.should.equal true
|
.should.equal true
|
||||||
|
|
||||||
|
it "should should send the update to the doc updater", ->
|
||||||
|
oldDocs = [ doc: @entity, path: @path ]
|
||||||
|
@DocumentUpdaterHandler.updateProjectStructure
|
||||||
|
.calledWith(project_id, projectHistoryId, userId, {oldDocs})
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
describe "a folder", ->
|
describe "a folder", ->
|
||||||
beforeEach (done) ->
|
beforeEach (done) ->
|
||||||
@folder =
|
@folder =
|
||||||
|
@ -905,6 +911,13 @@ describe 'ProjectEntityUpdateHandler', ->
|
||||||
.calledWith(@project, @doc2, "/folder/doc-name-2", userId)
|
.calledWith(@project, @doc2, "/folder/doc-name-2", userId)
|
||||||
.should.equal true
|
.should.equal true
|
||||||
|
|
||||||
|
it "should should send one update to the doc updater for all docs and files", ->
|
||||||
|
oldFiles = [ {file: @file2, path: "/folder/file-name-2"}, {file: @file1, path: "/folder/subfolder/file-name-1"} ]
|
||||||
|
oldDocs = [ {doc: @doc2, path: "/folder/doc-name-2"}, { doc: @doc1, path: "/folder/subfolder/doc-name-1"} ]
|
||||||
|
@DocumentUpdaterHandler.updateProjectStructure
|
||||||
|
.calledWith(project_id, projectHistoryId, userId, {oldFiles, oldDocs})
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
describe "_cleanUpDoc", ->
|
describe "_cleanUpDoc", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@doc =
|
@doc =
|
||||||
|
@ -941,12 +954,6 @@ describe 'ProjectEntityUpdateHandler', ->
|
||||||
.calledWith(project_id, @doc._id.toString())
|
.calledWith(project_id, @doc._id.toString())
|
||||||
.should.equal true
|
.should.equal true
|
||||||
|
|
||||||
it "should should send the update to the doc updater", ->
|
|
||||||
oldDocs = [ doc: @doc, path: @path ]
|
|
||||||
@DocumentUpdaterHandler.updateProjectStructure
|
|
||||||
.calledWith(project_id, projectHistoryId, userId, {oldDocs})
|
|
||||||
.should.equal true
|
|
||||||
|
|
||||||
it "should call the callback", ->
|
it "should call the callback", ->
|
||||||
@callback.called.should.equal true
|
@callback.called.should.equal true
|
||||||
|
|
||||||
|
|
|
@ -3,23 +3,25 @@ assert = require('assert')
|
||||||
path = require('path')
|
path = require('path')
|
||||||
modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js'
|
modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js'
|
||||||
lockKey = "lock:web:{#{5678}}"
|
lockKey = "lock:web:{#{5678}}"
|
||||||
|
lockValue = "123456"
|
||||||
SandboxedModule = require('sandboxed-module')
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
|
||||||
describe 'LockManager - releasing the lock', ()->
|
describe 'LockManager - releasing the lock', ()->
|
||||||
|
|
||||||
deleteStub = sinon.stub().callsArgWith(1)
|
deleteStub = sinon.stub().callsArgWith(4)
|
||||||
mocks =
|
mocks =
|
||||||
"logger-sharelatex": log:->
|
"logger-sharelatex": log:->
|
||||||
|
|
||||||
"./RedisWrapper":
|
"./RedisWrapper":
|
||||||
client: ()->
|
client: ()->
|
||||||
auth:->
|
auth:->
|
||||||
del:deleteStub
|
eval:deleteStub
|
||||||
|
|
||||||
LockManager = SandboxedModule.require(modulePath, requires: mocks)
|
|
||||||
|
|
||||||
|
LockManager = SandboxedModule.require(modulePath, requires: mocks)
|
||||||
|
LockManager.unlockScript = "this is the unlock script"
|
||||||
|
|
||||||
it 'should put a all data into memory', (done)->
|
it 'should put a all data into memory', (done)->
|
||||||
LockManager._releaseLock lockKey, ->
|
LockManager._releaseLock lockKey, lockValue, ->
|
||||||
deleteStub.calledWith(lockKey).should.equal true
|
deleteStub.calledWith(LockManager.unlockScript, 1, lockKey, lockValue).should.equal true
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
|
|
@ -22,10 +22,11 @@ describe 'LockManager - trying the lock', ->
|
||||||
describe "when the lock is not set", ->
|
describe "when the lock is not set", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@set.callsArgWith(5, null, "OK")
|
@set.callsArgWith(5, null, "OK")
|
||||||
|
@LockManager.randomLock = sinon.stub().returns("random-lock-value")
|
||||||
@LockManager._tryLock @key, @namespace, @callback
|
@LockManager._tryLock @key, @namespace, @callback
|
||||||
|
|
||||||
it "should set the lock key with an expiry if it is not set", ->
|
it "should set the lock key with an expiry if it is not set", ->
|
||||||
@set.calledWith(@key, "locked", "EX", 30, "NX")
|
@set.calledWith(@key, "random-lock-value", "EX", 30, "NX")
|
||||||
.should.equal true
|
.should.equal true
|
||||||
|
|
||||||
it "should return the callback with true", ->
|
it "should return the callback with true", ->
|
||||||
|
|
Loading…
Reference in a new issue