Migración de MongoDB con Node y Monk

Recientemente, uno de nuestros principales usuarios se quejó de que no se podía acceder a su cuenta de Storify. Revisamos la base de datos de producción y parece ser que la cuenta podría haber sido comprometida y eliminada de manera maliciosa por alguien que usa las credenciales de la cuenta del usuario. Gracias al excelente servicio de MongoHQ, tuvimos una base de datos de respaldo en menos de 15 minutos.
Había dos opciones para continuar con la migración:

  1. Script de shell Mongo
  2. Programa Node.js

Debido a que la eliminación de la cuenta de usuario de Storify implica la eliminación de todos los objetos relacionados (identidades, relaciones (seguidores, suscripciones), Me gusta, historias), hemos decidido continuar con la última opción. Funcionó perfectamente, y aquí hay una versión simplificada que puede usar como modelo para la migración de MongoDB (también en gist.github.com/4516139).

Restauración de registros de MongoDB

Carguemos todos los módulos que necesitamos:Monk, Progress, Async y MongoDB:

var async = require('async');
var ProgressBar = require('progress');
var monk = require('monk');
var ObjectId=require('mongodb').ObjectID;

Por cierto, creado por LeanBoost, Monk es una pequeña capa que proporciona mejoras de usabilidad simples pero sustanciales para el uso de MongoDB dentro de Node.JS.

Monk toma la cadena de conexión en el siguiente formato:

username:password@dbhost:port/database

Entonces podemos crear los siguientes objetos:

var dest = monk('localhost:27017/storify_localhost');
var backup = monk('localhost:27017/storify_backup');

Necesitamos saber el ID del objeto que queremos restaurar:

var userId = ObjectId(YOUR-OBJECT-ID); 

Esta es una práctica restauración función que podemos reutilizar para restaurar objetos de colecciones relacionadas especificando la consulta (para obtener más información sobre las consultas de MongoDB, vaya a la publicación Querying 20M-Record MongoDB Collection. Para llamarla, simplemente pase el nombre de la colección como una cadena, por ejemplo, "stories" y una consulta que asocia objetos de esta colección con su objeto principal, por ejemplo, {userId:user.id} . La barra de progreso es necesaria para mostrarnos buenas imágenes en la 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();
    }
  });
}

Ahora podemos usar async para llamar a restaurar función mencionada anteriormente:

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);
});

El código completo está disponible en gist.github.com/4516139 y aquí:

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);
});
           

Para iniciarlo, ejecute npm install/update y cambie los valores de la base de datos codificados.