Recently one of our top users complained that their Storify account was unaccessible. We’ve checked the production database and it appeares to be that the account might have been compromised and maliciously deleted by somebody using user’s account credentials. Thanks to a great MongoHQ service, we had a backup database in less than 15 minutes.
There were two options to proceed with the migration:
- Mongo shell script
- Node.js program
Because Storify user account deletion involves deletion of all related objects — identities, relationships (followers, subscriptions), likes, stories — we’ve decided to proceed with the latter option. It worked perfectly, and here is a simplified version which you can use as a boilerplate for MongoDB migration (also at gist.github.com/4516139).
Let’s load all the modules we need: Monk, Progress, Async, and MongoDB:
var async = require('async');
var ProgressBar = require('progress');
var monk = require('monk');
var ObjectId=require('mongodb').ObjectID;
By the way, made by LeanBoost, Monk is a tiny layer that provides simple yet substantial usability improvements for MongoDB usage within Node.JS.
Monk takes connection string in the following format:
username:password@dbhost:port/database
So we can create the following objects:
var dest = monk('localhost:27017/storify_localhost');
var backup = monk('localhost:27017/storify_backup');
We need to know the object ID which we want to restore:
var userId = ObjectId(YOUR-OBJECT-ID);
This is a handy restore function which we can reuse to restore objects from related collections by specifying query (for more on MongoDB queries go to post Querying 20M-Record MongoDB Collection. To call it, just pass a name of the collection as a string, e.g., "stories"
and a query which associates objects from this collection with your main object, e.g., {userId:user.id}
. The progress bar is needed to show us nice visuals in the terminal.
var restore = function(collection, query, callback){
console.info('restoring from ' + collection);
var q = query;
backup.get(collection).count(q, function(e, n) {
console.log('found '+n+' '+collection);
if (e) console.error(e);
var bar = new ProgressBar('[:bar] :current/:total :percent :etas', { total: n-1, width: 40 })
var tick = function(e) {
if (e) {
console.error(e);
bar.tick();
}
else {
bar.tick();
}
if (bar.complete) {
console.log();
console.log('restoring '+collection+' is completed');
callback();
}
};
if (n>0){
console.log('adding '+ n+ ' '+collection);
backup.get(collection).find(q, { stream: true }).each(function(element) {
dest.get(collection).insert(element, tick);
});
} else {
callback();
}
});
}
Now we can use async to call the restore function mentioned above:
async.series({
restoreUser: function(callback){ // import user element
backup.get('users').find({_id:userId}, { stream: true, limit: 1 }).each(function(user) {
dest.get('users').insert(user, function(e){
if (e) {
console.log(e);
}
else {
console.log('resored user: '+ user.username);
}
callback();
});
});
},
restoreIdentity: function(callback){
restore('identities',{
userid:userId
}, callback);
},
restoreStories: function(callback){
restore('stories', {authorid:userId}, callback);
}
}, function(e) {
console.log();
console.log('restoring is completed!');
process.exit(1);
});
The full code is available at gist.github.com/4516139 and here:
var async = require('async');
var ProgressBar = require('progress');
var monk = require('monk');
var ms = require('ms');
var ObjectId=require('mongodb').ObjectID;
var dest = monk('localhost:27017/storify_localhost');
var backup = monk('localhost:27017/storify_backup');
var userId = ObjectId(YOUR-OBJECT-ID); // monk should have auto casting but we need it for queries
var restore = function(collection, query, callback){
console.info('restoring from ' + collection);
var q = query;
backup.get(collection).count(q, function(e, n) {
console.log('found '+n+' '+collection);
if (e) console.error(e);
var bar = new ProgressBar('[:bar] :current/:total :percent :etas', { total: n-1, width: 40 })
var tick = function(e) {
if (e) {
console.error(e);
bar.tick();
}
else {
bar.tick();
}
if (bar.complete) {
console.log();
console.log('restoring '+collection+' is completed');
callback();
}
};
if (n>0){
console.log('adding '+ n+ ' '+collection);
backup.get(collection).find(q, { stream: true }).each(function(element) {
dest.get(collection).insert(element, tick);
});
} else {
callback();
}
});
}
async.series({
restoreUser: function(callback){ // import user element
backup.get('users').find({_id:userId}, { stream: true, limit: 1 }).each(function(user) {
dest.get('users').insert(user, function(e){
if (e) {
console.log(e);
}
else {
console.log('resored user: '+ user.username);
}
callback();
});
});
},
restoreIdentity: function(callback){
restore('identities',{
userid:userId
}, callback);
},
restoreStories: function(callback){
restore('stories', {authorid:userId}, callback);
}
}, function(e) {
console.log();
console.log('restoring is completed!');
process.exit(1);
});
To launch it, run npm install/update and change hard-coded database values.