Merge pull request #6 from heukirne/s3-archive
Add S3 archive track changes feature
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
MongoManager = require "./MongoManager"
|
||||
MongoAWS = require "./MongoAWS"
|
||||
LockManager = require "./LockManager"
|
||||
DocstoreHandler = require "./DocstoreHandler"
|
||||
logger = require "logger-sharelatex"
|
||||
_ = require "underscore"
|
||||
async = require "async"
|
||||
settings = require("settings-sharelatex")
|
||||
|
||||
# increase lock timeouts because archiving can be slow
|
||||
LockManager.LOCK_TEST_INTERVAL = 500 # 500ms between each test of the lock
|
||||
LockManager.MAX_LOCK_WAIT_TIME = 30000 # 30s maximum time to spend trying to get the lock
|
||||
LockManager.LOCK_TTL = 30 # seconds
|
||||
|
||||
module.exports = DocArchiveManager =
|
||||
|
||||
archiveAllDocsChanges: (project_id, callback = (error, docs) ->) ->
|
||||
DocstoreHandler.getAllDocs project_id, (error, docs) ->
|
||||
if error?
|
||||
return callback(error)
|
||||
else if !docs?
|
||||
return callback new Error("No docs for project #{project_id}")
|
||||
jobs = _.map docs, (doc) ->
|
||||
(cb)-> DocArchiveManager.archiveDocChangesWithLock project_id, doc._id, cb
|
||||
async.series jobs, callback
|
||||
|
||||
archiveDocChangesWithLock: (project_id, doc_id, callback = (error) ->) ->
|
||||
job = (releaseLock) ->
|
||||
DocArchiveManager.archiveDocChanges project_id, doc_id, releaseLock
|
||||
LockManager.runWithLock("HistoryLock:#{doc_id}", job, callback)
|
||||
|
||||
archiveDocChanges: (project_id, doc_id, callback)->
|
||||
MongoManager.getDocChangesCount doc_id, (error, count) ->
|
||||
return callback(error) if error?
|
||||
if count == 0
|
||||
logger.log {project_id, doc_id}, "document history is empty, not archiving"
|
||||
return callback()
|
||||
else
|
||||
MongoManager.getLastCompressedUpdate doc_id, (error, update) ->
|
||||
return callback(error) if error?
|
||||
MongoAWS.archiveDocHistory project_id, doc_id, (error) ->
|
||||
return callback(error) if error?
|
||||
logger.log doc_id:doc_id, project_id:project_id, "exported document to S3"
|
||||
MongoManager.markDocHistoryAsArchived doc_id, update, (error) ->
|
||||
return callback(error) if error?
|
||||
callback()
|
||||
|
||||
unArchiveAllDocsChanges: (project_id, callback = (error, docs) ->) ->
|
||||
DocstoreHandler.getAllDocs project_id, (error, docs) ->
|
||||
if error?
|
||||
return callback(error)
|
||||
else if !docs?
|
||||
return callback new Error("No docs for project #{project_id}")
|
||||
jobs = _.map docs, (doc) ->
|
||||
(cb)-> DocArchiveManager.unArchiveDocChangesWithLock project_id, doc._id, cb
|
||||
async.parallelLimit jobs, 4, callback
|
||||
|
||||
unArchiveDocChangesWithLock: (project_id, doc_id, callback = (error) ->) ->
|
||||
job = (releaseLock) ->
|
||||
DocArchiveManager.unArchiveDocChanges project_id, doc_id, releaseLock
|
||||
LockManager.runWithLock("HistoryLock:#{doc_id}", job, callback)
|
||||
|
||||
unArchiveDocChanges: (project_id, doc_id, callback)->
|
||||
MongoManager.getArchivedDocChanges doc_id, (error, count) ->
|
||||
return callback(error) if error?
|
||||
if count == 0
|
||||
return callback()
|
||||
else
|
||||
MongoAWS.unArchiveDocHistory project_id, doc_id, (error) ->
|
||||
return callback(error) if error?
|
||||
logger.log doc_id:doc_id, project_id:project_id, "imported document from S3"
|
||||
MongoManager.markDocHistoryAsUnarchived doc_id, (error) ->
|
||||
return callback(error) if error?
|
||||
callback()
|
||||
@@ -0,0 +1,21 @@
|
||||
request = require("request").defaults(jar: false)
|
||||
logger = require "logger-sharelatex"
|
||||
settings = require "settings-sharelatex"
|
||||
|
||||
module.exports = DocstoreHandler =
|
||||
|
||||
getAllDocs: (project_id, callback = (error) ->) ->
|
||||
logger.log project_id: project_id, "getting all docs for project in docstore api"
|
||||
url = "#{settings.apis.docstore.url}/project/#{project_id}/doc"
|
||||
request.get {
|
||||
url: url
|
||||
json: true
|
||||
}, (error, res, docs) ->
|
||||
return callback(error) if error?
|
||||
logger.log {error, res, docs: if docs?.length then docs.map (d) -> d._id else []}, "docstore response"
|
||||
if 200 <= res.statusCode < 300
|
||||
callback(null, docs)
|
||||
else
|
||||
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
|
||||
logger.error err: error, project_id: project_id, "error getting all docs from docstore"
|
||||
callback(error)
|
||||
@@ -3,6 +3,7 @@ DiffManager = require "./DiffManager"
|
||||
PackManager = require "./PackManager"
|
||||
RestoreManager = require "./RestoreManager"
|
||||
logger = require "logger-sharelatex"
|
||||
DocArchiveManager = require "./DocArchiveManager"
|
||||
|
||||
module.exports = HttpController =
|
||||
flushDoc: (req, res, next = (error) ->) ->
|
||||
@@ -66,3 +67,17 @@ module.exports = HttpController =
|
||||
RestoreManager.restoreToBeforeVersion project_id, doc_id, version, user_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.send 204
|
||||
|
||||
archiveProject: (req, res, next = (error) ->) ->
|
||||
project_id = req.params.project_id
|
||||
logger.log project_id: project_id, "archiving all track changes to s3"
|
||||
DocArchiveManager.archiveAllDocsChanges project_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.send 204
|
||||
|
||||
unArchiveProject: (req, res, next = (error) ->) ->
|
||||
project_id = req.params.project_id
|
||||
logger.log project_id: project_id, "unarchiving all track changes from s3"
|
||||
DocArchiveManager.unArchiveAllDocsChanges project_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.send 204
|
||||
@@ -0,0 +1,105 @@
|
||||
settings = require "settings-sharelatex"
|
||||
logger = require "logger-sharelatex"
|
||||
AWS = require 'aws-sdk'
|
||||
S3S = require 's3-streams'
|
||||
{db, ObjectId} = require "./mongojs"
|
||||
JSONStream = require "JSONStream"
|
||||
ReadlineStream = require "readline-stream"
|
||||
|
||||
module.exports = MongoAWS =
|
||||
|
||||
MAX_SIZE: 1024*1024 # almost max size
|
||||
MAX_COUNT: 1024 # almost max count
|
||||
|
||||
archiveDocHistory: (project_id, doc_id, _callback = (error) ->) ->
|
||||
|
||||
callback = (args...) ->
|
||||
_callback(args...)
|
||||
_callback = () ->
|
||||
|
||||
query = {
|
||||
doc_id: ObjectId(doc_id)
|
||||
expiresAt: {$exists : false}
|
||||
}
|
||||
|
||||
AWS.config.update {
|
||||
accessKeyId: settings.filestore.s3.key
|
||||
secretAccessKey: settings.filestore.s3.secret
|
||||
}
|
||||
|
||||
upload = S3S.WriteStream new AWS.S3(), {
|
||||
"Bucket": settings.filestore.stores.user_files,
|
||||
"Key": project_id+"/changes-"+doc_id
|
||||
}
|
||||
|
||||
db.docHistory.find(query)
|
||||
.on 'error', (err) ->
|
||||
callback(err)
|
||||
.pipe JSONStream.stringify()
|
||||
.pipe upload
|
||||
.on 'error', (err) ->
|
||||
callback(err)
|
||||
.on 'finish', () ->
|
||||
return callback(null)
|
||||
|
||||
unArchiveDocHistory: (project_id, doc_id, _callback = (error) ->) ->
|
||||
|
||||
callback = (args...) ->
|
||||
_callback(args...)
|
||||
_callback = () ->
|
||||
|
||||
AWS.config.update {
|
||||
accessKeyId: settings.filestore.s3.key
|
||||
secretAccessKey: settings.filestore.s3.secret
|
||||
}
|
||||
|
||||
download = S3S.ReadStream new AWS.S3(), {
|
||||
"Bucket": settings.filestore.stores.user_files,
|
||||
"Key": project_id+"/changes-"+doc_id
|
||||
}, {
|
||||
encoding: "utf8"
|
||||
}
|
||||
|
||||
lineStream = new ReadlineStream();
|
||||
ops = []
|
||||
sz = 0
|
||||
|
||||
download
|
||||
.on 'open', (obj) ->
|
||||
return 1
|
||||
.on 'error', (err) ->
|
||||
callback(err)
|
||||
.pipe lineStream
|
||||
.on 'data', (line) ->
|
||||
if line.length > 2
|
||||
ops.push(JSON.parse(line))
|
||||
sz += line.length
|
||||
if ops.length >= MongoAWS.MAX_COUNT || sz >= MongoAWS.MAX_SIZE
|
||||
download.pause()
|
||||
MongoAWS.handleBulk ops.slice(0), sz, () ->
|
||||
download.resume()
|
||||
ops.splice(0,ops.length)
|
||||
sz = 0
|
||||
.on 'end', () ->
|
||||
MongoAWS.handleBulk ops, sz, callback
|
||||
.on 'error', (err) ->
|
||||
return callback(err)
|
||||
|
||||
handleBulk: (ops, size, cb) ->
|
||||
bulk = db.docHistory.initializeUnorderedBulkOp();
|
||||
|
||||
for op in ops
|
||||
op._id = ObjectId(op._id)
|
||||
op.doc_id = ObjectId(op.doc_id)
|
||||
op.project_id = ObjectId(op.project_id)
|
||||
bulk.find({_id:op._id}).upsert().updateOne(op)
|
||||
|
||||
if ops.length > 0
|
||||
bulk.execute (err, result) ->
|
||||
if err?
|
||||
logger.error err:err, "error bulking ReadlineStream"
|
||||
else
|
||||
logger.log count:ops.length, result:result, size: size, "bulked ReadlineStream"
|
||||
cb(err)
|
||||
else
|
||||
cb()
|
||||
@@ -0,0 +1,123 @@
|
||||
settings = require "settings-sharelatex"
|
||||
child_process = require "child_process"
|
||||
mongoUri = require "mongo-uri";
|
||||
logger = require "logger-sharelatex"
|
||||
AWS = require 'aws-sdk'
|
||||
fs = require 'fs'
|
||||
S3S = require 's3-streams'
|
||||
|
||||
module.exports = MongoAWSexternal =
|
||||
|
||||
archiveDocHistory: (project_id, doc_id, callback = (error) ->) ->
|
||||
MongoAWS.mongoExportDocHistory doc_id, (error, filepath) ->
|
||||
MongoAWS.s3upStream project_id, doc_id, filepath, callback
|
||||
#delete temp file?
|
||||
|
||||
|
||||
unArchiveDocHistory: (project_id, doc_id, callback = (error) ->) ->
|
||||
MongoAWS.s3downStream project_id, doc_id, (error, filepath) ->
|
||||
if error == null
|
||||
MongoAWS.mongoImportDocHistory filepath, callback
|
||||
#delete temp file?
|
||||
else
|
||||
callback
|
||||
|
||||
mongoExportDocHistory: (doc_id, callback = (error, filepath) ->) ->
|
||||
uriData = mongoUri.parse(settings.mongo.url);
|
||||
filepath = settings.path.dumpFolder + '/' + doc_id + '.jsonUp'
|
||||
|
||||
args = []
|
||||
args.push '-h'
|
||||
args.push uriData.hosts[0]
|
||||
args.push '-d'
|
||||
args.push uriData.database
|
||||
args.push '-c'
|
||||
args.push 'docHistory'
|
||||
args.push '-q'
|
||||
args.push "{doc_id: ObjectId('#{doc_id}') , expiresAt: {$exists : false} }"
|
||||
args.push '-o'
|
||||
args.push filepath
|
||||
|
||||
proc = child_process.spawn "mongoexport", args
|
||||
|
||||
proc.on "error", callback
|
||||
|
||||
stderr = ""
|
||||
proc.stderr.on "data", (chunk) -> stderr += chunk.toString()
|
||||
|
||||
proc.on "close", (code) ->
|
||||
if code == 0
|
||||
return callback(null,filepath)
|
||||
else
|
||||
return callback(new Error("mongodump failed: #{stderr}"),null)
|
||||
|
||||
mongoImportDocHistory: (filepath, callback = (error) ->) ->
|
||||
|
||||
uriData = mongoUri.parse(settings.mongo.url);
|
||||
|
||||
args = []
|
||||
args.push '-h'
|
||||
args.push uriData.hosts[0]
|
||||
args.push '-d'
|
||||
args.push uriData.database
|
||||
args.push '-c'
|
||||
args.push 'docHistory'
|
||||
args.push '--file'
|
||||
args.push filepath
|
||||
|
||||
proc = child_process.spawn "mongoimport", args
|
||||
|
||||
proc.on "error", callback
|
||||
|
||||
stderr = ""
|
||||
proc.stderr.on "data", (chunk) -> stderr += chunk.toString()
|
||||
|
||||
proc.on "close", (code) ->
|
||||
if code == 0
|
||||
return callback(null,filepath)
|
||||
else
|
||||
return callback(new Error("mongodump failed: #{stderr}"),null)
|
||||
|
||||
s3upStream: (project_id, doc_id, filepath, callback = (error) ->) ->
|
||||
|
||||
AWS.config.update {
|
||||
accessKeyId: settings.filestore.s3.key
|
||||
secretAccessKey: settings.filestore.s3.secret
|
||||
}
|
||||
|
||||
upload = S3S.WriteStream new AWS.S3(), {
|
||||
"Bucket": settings.filestore.stores.user_files,
|
||||
"Key": project_id+"/changes-"+doc_id
|
||||
}
|
||||
|
||||
fs.createReadStream(filepath)
|
||||
.on 'open', (obj) ->
|
||||
return 1
|
||||
.pipe(upload)
|
||||
.on 'finish', () ->
|
||||
return callback(null)
|
||||
.on 'error', (err) ->
|
||||
return callback(err)
|
||||
|
||||
s3downStream: (project_id, doc_id, callback = (error, filepath) ->) ->
|
||||
|
||||
filepath = settings.path.dumpFolder + '/' + doc_id + '.jsonDown'
|
||||
|
||||
AWS.config.update {
|
||||
accessKeyId: settings.filestore.s3.key
|
||||
secretAccessKey: settings.filestore.s3.secret
|
||||
}
|
||||
|
||||
download = S3S.ReadStream new AWS.S3(), {
|
||||
"Bucket": settings.filestore.stores.user_files,
|
||||
"Key": project_id+"/changes-"+doc_id
|
||||
}
|
||||
|
||||
download
|
||||
.on 'open', (obj) ->
|
||||
return 1
|
||||
.pipe(fs.createWriteStream(filepath))
|
||||
.on 'finish', () ->
|
||||
return callback(null, filepath)
|
||||
.on 'error', (err) ->
|
||||
return callback(err, null)
|
||||
@@ -1,6 +1,7 @@
|
||||
{db, ObjectId} = require "./mongojs"
|
||||
PackManager = require "./PackManager"
|
||||
async = require "async"
|
||||
logger = require "logger-sharelatex"
|
||||
|
||||
module.exports = MongoManager =
|
||||
getLastCompressedUpdate: (doc_id, callback = (error, update) ->) ->
|
||||
@@ -47,6 +48,7 @@ module.exports = MongoManager =
|
||||
|
||||
|
||||
insertCompressedUpdate: (project_id, doc_id, update, temporary, callback = (error) ->) ->
|
||||
inS3 = update.inS3?
|
||||
update = {
|
||||
doc_id: ObjectId(doc_id.toString())
|
||||
project_id: ObjectId(project_id.toString())
|
||||
@@ -54,6 +56,9 @@ module.exports = MongoManager =
|
||||
meta: update.meta
|
||||
v: update.v
|
||||
}
|
||||
if inS3
|
||||
update.inS3 = true
|
||||
|
||||
if temporary
|
||||
seconds = 1000
|
||||
minutes = 60 * seconds
|
||||
@@ -126,3 +131,20 @@ module.exports = MongoManager =
|
||||
# For finding documents which need packing
|
||||
db.docHistoryStats.ensureIndex { doc_id: 1 }, { background: true }
|
||||
db.docHistoryStats.ensureIndex { updates: -1, doc_id: 1 }, { background: true }
|
||||
|
||||
getDocChangesCount: (doc_id, callback)->
|
||||
db.docHistory.count { doc_id : ObjectId(doc_id.toString()), inS3 : { $exists : false }}, {}, callback
|
||||
|
||||
getArchivedDocChanges: (doc_id, callback)->
|
||||
db.docHistory.count { doc_id: ObjectId(doc_id.toString()) , inS3: true }, {}, callback
|
||||
|
||||
markDocHistoryAsArchived: (doc_id, update, callback)->
|
||||
db.docHistory.update { _id: update._id }, { $set : { inS3 : true } }, (error)->
|
||||
return callback(error) if error?
|
||||
db.docHistory.remove { doc_id : ObjectId(doc_id.toString()), inS3 : { $exists : false }, v: { $lt : update.v }, expiresAt: {$exists : false} }, (error)->
|
||||
return callback(error) if error?
|
||||
callback(error)
|
||||
|
||||
markDocHistoryAsUnarchived: (doc_id, callback)->
|
||||
db.docHistory.update { doc_id: ObjectId(doc_id.toString()) }, { $unset : { inS3 : true } }, { multi: true }, (error)->
|
||||
callback(error)
|
||||
|
||||
@@ -6,6 +6,8 @@ WebApiManager = require "./WebApiManager"
|
||||
UpdateTrimmer = require "./UpdateTrimmer"
|
||||
logger = require "logger-sharelatex"
|
||||
async = require "async"
|
||||
DocArchiveManager = require "./DocArchiveManager"
|
||||
_ = require "underscore"
|
||||
|
||||
module.exports = UpdatesManager =
|
||||
compressAndSaveRawUpdates: (project_id, doc_id, rawUpdates, temporary, callback = (error) ->) ->
|
||||
@@ -32,6 +34,9 @@ module.exports = UpdatesManager =
|
||||
return
|
||||
|
||||
compressedUpdates = UpdateCompressor.compressRawUpdates lastCompressedUpdate, rawUpdates
|
||||
if lastCompressedUpdate?.inS3? and not _.some(compressedUpdates, (update) -> update.inS3)
|
||||
compressedUpdates[compressedUpdates.length-1].inS3 = lastCompressedUpdate.inS3
|
||||
|
||||
MongoManager.insertCompressedUpdates project_id, doc_id, compressedUpdates, temporary,(error) ->
|
||||
return callback(error) if error?
|
||||
logger.log project_id: project_id, doc_id: doc_id, rawUpdatesLength: length, compressedUpdatesLength: compressedUpdates.length, "compressed doc updates"
|
||||
@@ -94,7 +99,17 @@ module.exports = UpdatesManager =
|
||||
getProjectUpdates: (project_id, options = {}, callback = (error, updates) ->) ->
|
||||
UpdatesManager.processUncompressedUpdatesForProject project_id, (error) ->
|
||||
return callback(error) if error?
|
||||
MongoManager.getProjectUpdates project_id, options, callback
|
||||
MongoManager.getProjectUpdates project_id, options, (error, updates) ->
|
||||
jobs = []
|
||||
for update in updates
|
||||
if update.inS3?
|
||||
do (update) ->
|
||||
jobs.push (callback) -> DocArchiveManager.unArchiveDocChanges update.project_id, update.doc_id, callback
|
||||
if jobs.length?
|
||||
async.series jobs, (err) ->
|
||||
MongoManager.getProjectUpdates project_id, options, callback
|
||||
else
|
||||
callback(error, updates)
|
||||
|
||||
getProjectUpdatesWithUserInfo: (project_id, options = {}, callback = (error, updates) ->) ->
|
||||
UpdatesManager.getProjectUpdates project_id, options, (error, updates) ->
|
||||
|
||||
Reference in New Issue
Block a user