Migrar un foro de NodeBB con MongoDB a Discourse

Como bien sabes, NodeBB admite dos backends de base de datos: Redis y MongoDB. El script de importación de Discourse los admite a ambos. En este tutorial, aprenderemos a migrar NodeBB con MongoDB como backend de base de datos. Utilizaremos NodeBB Importer con el adaptador mongo. Si tu foro de NodeBB utiliza Redis como backend, por favor sigue este tutorial que demuestra el adaptador redis.

El plan

  • Preparar el entorno de desarrollo.
  • Exportar la base de datos desde el entorno de producción.
  • Importar la base de datos de producción a una instancia de Discourse.
  • Ejecutar el script de importación.

Qué se puede migrar

  • Grupos
  • Adjuntos
  • Categorías
    • Categoría Raíz => Categoría Raíz
    • Subcategoría y Sub-subcategoría => Subcategoría
  • Temas y Publicaciones
    • tema fijado => tema fijado
    • tema bloqueado => tema cerrado
    • vistas de tema
    • upvoted_by
    • estilos, menciones, emojis y adjuntos.
  • Usuarios (con los siguientes atributos)
    • fondo del perfil
    • avatares
    • estado de prohibición
    • nombre de usuario
    • nombre
    • correo electrónico
    • administrador
    • biografía
    • grupo
    • sitio web
    • ubicación
    • fecha de ingreso

Preparación del entorno de desarrollo local

Como se mencionó en nuestro plan, primero necesitamos preparar nuestro entorno de desarrollo. Sigue una de estas guías para instalar Discourse:

:bulb: Por favor, consulta esta guía si tienes problemas para configurar el servidor de Discourse.

Luego, instala el servidor de base de datos MongoDB.

Ubuntu-18-04:

$ wget -qO - https://www.mongodb.org/static/pgp/server-4.0.asc | sudo apt-key add -
$ echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.0.list
$ sudo apt-get update
$ sudo apt-get install -y mongodb-org
$ sudo service mongod status

Para más detalles, consulta la guía oficial.

Mac OS:

$ brew tap mongodb/brew
$ brew install mongodb-community@4.0
$ brew services start mongodb-community@4.0
$ brew services status mongodb-community@4.0

Para más detalles, consulta la guía oficial.

Windows 10:

Descarga el instalador e instala MongoDB como un servicio de Windows:

https://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2008plus-ssl-4.0.12-signed.msi

Ejecuta el cmd con privilegios de administrador para verificar si el servidor mongo funciona correctamente:

"C:\Program Files\MongoDB\Server\4.0\bin\mongo.exe"

Para más detalles, consulta la guía oficial.

Este entorno será nuestro servidor de Discourse.

Exportación del volcado de la base de datos de producción:

Apaga tu foro de NodeBB (servidor de producción).

$ cd /ruta_a_nodebb
$ ./nodebb stop

Apaga tu base de datos:

$ sudo service mongodb stop

Copia de seguridad de tu BD:

$ mongodump --out ~/mi_ruta_de_copia_seguridad/

La salida del comando anterior será algo como esto:

2019-08-16T15:17:09.845+0300 done dumping admin.system.users (1 documento)
2019-08-16T15:17:09.846+0300 done dumping admin.system.version (2 documentos)
2019-08-16T15:17:09.849+0300 done dumping nodebb.sessions (10 documentos)
2019-08-16T15:17:09.850+0300 done dumping nodebb.socket.io (215 documentos)
2019-08-16T15:17:09.854+0300 done dumping nodebb.objects (1488 documentos)

Ten en cuenta que la copia de seguridad es en realidad un directorio, no solo un archivo.

:bulb: Puedes verificar el tamaño de tu BD ejecutando use miBaseDeDatos y luego db.stats().dataSize; dentro de la CLI de mongo. El valor devuelto estará en bytes.

Copia de seguridad de los activos de tu foro:

$ cd /ruta_al_directorio_raiz_de_nodebb/
$ tar -czf ./uploads.tar.gz ./public/uploads

Una vez que tengas tu BD y los activos del foro, debes copiarlos al servidor de Discourse.

Importación de la base de datos

Ahora que tenemos nuestra base de datos, podemos importarla en nuestra instancia local de MongoDB:

$ mongorestore ~/ruta_de_mi_directorio_de_copia_seguridad/

Para más opciones, consulta la guía oficial.

A continuación, necesitas extraer el archivo uploads.tar.gz para que el importador pueda importar los activos:

$ tar xvzf uploads.tar.gz

Ejecución del script de importación

Ahora que nuestra base de datos y activos están en su lugar, estamos listos para ejecutar nuestro script de importación. Antes de eso, necesitamos editar el script de importador de NodeBB para adaptarlo a nuestras necesidades.

Esta es la ruta de tu carpeta de uploads de NodeBB:

ATTACHMENT_DIR = '/ruta_absoluta/uploads'

A continuación, necesitamos indicar al importador que utilice el adaptador mongo en lugar del adaptador redis:

adapter = NodeBB::Mongo
@client = adapter.new('mongodb://127.0.0.1:27017/nodebb')

# adapter = NodeBB::Redis
# @client = adapter.new(
# host: "localhost",
# port: "6379",
# db: 14
# )

Ejecuta el importador con Discourse limpio y soporte para la gema mongo:

$ cd ~/discourse
$ echo "gem 'mongo'" >> Gemfile
$ bundle install
$ bundle exec rake db:drop db:create db:migrate
$ bundle exec ruby script/import_scripts/nodebb/nodebb.rb

El importador se conectará a la instancia de MongoDB y migrará todo a Discourse.

Una vez que el importador termine, inicia Discourse:

$ bundle exec rails server

Inicia Sidekiq para procesar los datos migrados:

$ bundle exec sidekiq

Puedes monitorear el progreso en http://localhost:3000/sidekiq/queues.

Realiza una copia de seguridad de Discourse y súbelo a tu servidor de producción de Discourse siguiendo este tutorial.

:tada:

Si tienes alguna pregunta sobre el proceso, estaré encantado de ayudarte.

¡Feliz migración! :grinning:

10 Me gusta

Great work really. While I was trying, I am getting below error. Any idea what I might be doing wrong here?

importing groups
   10 / 10 (100.0%)  [474765 items/min]    
importing top level categories...
    8 / 8 (100.0%)  [437896 items/min]    
importing child categories...
   68 / 68 (100.0%)  [774048 items/min]  
importing users
 5534 / 5534 (100.0%)  [355520 items/min]    
adding users to groups...

importing topics...
Traceback (most recent call last):
	12: from script/import_scripts/nodebb/nodebb.rb:532:in `<main>'
	11: from /home/fsuzer/discourse/script/import_scripts/base.rb:47:in `perform'
	10: from script/import_scripts/nodebb/nodebb.rb:52:in `execute'
	 9: from script/import_scripts/nodebb/nodebb.rb:336:in `import_topics'
	 8: from /home/fsuzer/discourse/script/import_scripts/base.rb:877:in `batches'
	 7: from /home/fsuzer/discourse/script/import_scripts/base.rb:877:in `loop'
	 6: from /home/fsuzer/discourse/script/import_scripts/base.rb:878:in `block in batches'
	 5: from script/import_scripts/nodebb/nodebb.rb:337:in `block in import_topics'
	 4: from /home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:71:in `topics'
	 3: from /home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:71:in `map'
	 2: from /home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:71:in `block in topics'
	 1: from /home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:79:in `topic'
/home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:100:in `post': undefined method `[]' for nil:NilClass (NoMethodError)

Some field from the topic is empty. Or perhaps the topic query returned nothing.

1 me gusta

¿Alguien puede ayudarme?

Error: No se pudo encontrar el gem ‘mongo’ en ninguna de las fuentes de gems listadas en su Gemfile.

Después de $ bundle install

Esto sucederá si omitiste lo siguiente:

Asegúrate de que tu Gemfile tenga esta línea:

gem 'mongo'

Luego ejecuta:

$ bundle install
1 me gusta

Gracias por tu ayuda. Pero esta línea ya existe en mi archivo. También realicé los pasos anteriores indicados.

No sé si esto ayuda, pero cuando ejecuto gem list mongo, no aparece en la lista.

Tu Gemfile me suena raro. Por favor, ¿puedes compartir todo el contenido?

Tu Gemfile está simplemente mal. Debería tener otro contenido, tal como este:

Además de:

gem 'mongo'

¡Hola,

¡Gracias por la guía! Cuando ejecuto el script de migración, esto es lo que obtengo cuando el script llega al punto donde importa los usuarios:

Traceback (most recent call last):
        21: from script/import_scripts/nodebb/nodebb.rb:532:in `<main>'
        20: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:47:in `perform'
        19: from script/import_scripts/nodebb/nodebb.rb:50:in `execute'
        18: from script/import_scripts/nodebb/nodebb.rb:130:in `import_users'
        17: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:262:in `create_users'
        16: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:262:in `each'
        15: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:274:in `block in create_users'
        14: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:391:in `create_user'
        13: from /home/odyslam/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.3/lib/active_support/core_ext/object/try.rb:15:in `try'
        12: from /home/odyslam/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.3/lib/active_support/core_ext/object/try.rb:15:in `public_send'
        11: from script/import_scripts/nodebb/nodebb.rb:164:in `block (2 levels) in import_users'
        10: from script/import_scripts/nodebb/nodebb.rb:211:in `import_profile_picture'
         9: from /home/odyslam/forum/discourse/lib/upload_creator.rb:45:in `create_for'
         8: from /home/odyslam/forum/discourse/lib/distributed_mutex.rb:14:in `synchronize'
         7: from /home/odyslam/forum/discourse/lib/distributed_mutex.rb:29:in `synchronize'
         6: from /home/odyslam/forum/discourse/lib/distributed_mutex.rb:29:in `synchronize'
         5: from /home/odyslam/forum/discourse/lib/distributed_mutex.rb:33:in `block in synchronize'
         4: from /home/odyslam/forum/discourse/lib/upload_creator.rb:66:in `block in create_for'
         3: from /home/odyslam/forum/discourse/lib/upload_creator.rb:381:in `optimize!'
         2: from /home/odyslam/forum/discourse/app/models/optimized_image.rb:178:in `ensure_safe_paths!'
         1: from /home/odyslam/forum/discourse/app/models/optimized_image.rb:178:in `each'
/home/odyslam/forum/discourse/app/models/optimized_image.rb:179:in `block in ensure_safe_paths!': Discourse::InvalidAccess (Discourse::InvalidAccess)

Logré sortear esto comentando la parte que sube las fotos de perfil; eso no es un problema.

Ahora obtengo el error de nil al importar los temas. :frowning:

Traceback (most recent call last):
        12: from script/import_scripts/nodebb/nodebb.rb:532:in `<main>'
        11: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:47:in `perform'
        10: from script/import_scripts/nodebb/nodebb.rb:52:in `execute'
         9: from script/import_scripts/nodebb/nodebb.rb:336:in `import_topics'
         8: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:862:in `batches'
         7: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:862:in `loop'
         6: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:863:in `block in batches'
         5: from script/import_scripts/nodebb/nodebb.rb:337:in `block in import_topics'
         4: from /home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:71:in `topics'
         3: from /home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:71:in `map'
         2: from /home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:71:in `block in topics'
         1: from /home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:79:in `topic'
/home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:100:in `post': undefined method `[]' for nil:NilClass (NoMethodError)

En caso de que alguien esté interesado en el futuro, logré realizar la migración. Aquí hay algunos comentarios sobre mi experiencia; hay un par de trampas en el proceso.

https://odyslam.com/blog/Migrating-from-Nodebb-to-Discourse/

2 Me gusta
21: from script/import_scripts/nodebb/nodebb.rb:532:in `main`
        20: from /home/nodebb/discourse/script/import_scripts/base.rb:47:in `perform`
        19: from script/import_scripts/nodebb/nodebb.rb:50:in `execute`
        18: from script/import_scripts/nodebb/nodebb.rb:130:in `import_users`
        17: from /home/nodebb/discourse/script/import_scripts/base.rb:264:in `create_users`
        16: from /home/nodebb/discourse/script/import_scripts/base.rb:264:in `each`
        15: from /home/nodebb/discourse/script/import_scripts/base.rb:276:in `block in create_users`
        14: from /home/nodebb/discourse/script/import_scripts/base.rb:393:in `create_user`
        13: from /home/nodebb/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.1.4.1/lib/active_support/core_ext/object/try.rb:15:in `try`
        12: from /home/nodebb/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.1.4.1/lib/active_support/core_ext/object/try.rb:15:in `public_send`
        11: from script/import_scripts/nodebb/nodebb.rb:164:in `block (2 levels) in import_users`
        10: from script/import_scripts/nodebb/nodebb.rb:211:in `import_profile_picture`
         9: from /home/nodebb/discourse/lib/upload_creator.rb:64:in `create_for`
         8: from /home/nodebb/discourse/lib/distributed_mutex.rb:14:in `synchronize`
         7: from /home/nodebb/discourse/lib/distributed_mutex.rb:29:in `synchronize`
         6: from /home/nodebb/discourse/lib/distributed_mutex.rb:29:in `synchronize`
         5: from /home/nodebb/discourse/lib/distributed_mutex.rb:33:in `block in synchronize`
         4: from /home/nodebb/discourse/lib/upload_creator.rb:83:in `block in create_for`
         3: from /home/nodebb/discourse/lib/upload_creator.rb:501:in `optimize!`
         2: from /home/nodebb/discourse/app/models/optimized_image.rb:181:in `ensure_safe_paths!`
         1: from /home/nodebb/discourse/app/models/optimized_image.rb:181:in `each`
/home/nodebb/discourse/app/models/optimized_image.rb:182:in `block in ensure_safe_paths!': Discourse::InvalidAccess (Discourse::InvalidAccess)

@enigmaty Estoy enfrentando el mismo error que @OdysLam @FreeWorLD.

¡Hola!
¿Alguien ha intentado migrar desde la última versión de NodeBB 4.9.x a la versión actual de desarrollo de Discourse?

/home/dev/discourse/script/import_scripts/nodebb/mongo.rb:100:in 'NodeBB::Mongo#post': undefined method '[]' for nil (NoMethodError)

      post["timestamp"] = timestamp_to_date(post["timestamp"])
                                                ^^^^^^^^^^^^^
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'block in NodeBB::Mongo#posts'
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'Array#map'
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'NodeBB::Mongo#posts'
        from script/import_scripts/nodebb/nodebb.rb:413:in 'block in ImportScripts::NodeBB#import_posts'
        from /home/dev/discourse/script/import_scripts/base.rb:943:in 'block in ImportScripts::Base#batches'
        from 内部:kernel:168:in 'Kernel#loop'
        from /home/dev/discourse/script/import_scripts/base.rb:942:in 'ImportScripts::Base#batches'
        from script/import_scripts/nodebb/nodebb.rb:412:in 'ImportScripts::NodeBB#import_posts'
        from script/import_scripts/nodebb/nodebb.rb:52:in 'ImportScripts::NodeBB#execute'
        from /home/dev/discourse/script/import_scripts/base.rb:47:in 'ImportScripts::Base#perform'
        from script/import_scripts/nodebb/nodebb.rb:568:in '<main>'

Tengo ese error mientras migro en el entorno de desarrollo.
¿Alguna idea?

¿Los scripts de migración todavía son compatibles con la Comunidad en 2026?
El campo timestamp existe y contiene datos válidos.

Gracias.

[quote=“Twissell, post:17, topic:126553”]'NodeBB::Mongo#post': undefined method '[]' for nil

  post["timestamp"] = timestamp_to_date(post["timestamp"])

[/quote]

El problema es que post es nil. Entonces no has configurado las cosas correctamente. ¿Importó usuarios?

Podemos intentarlo, pero es bastante limitado. Es probable que necesites entender cómo funcionan muchas cosas que pueden estar más allá de lo que es fácilmente compatible en un foro.

3 Me gusta
importando usuários
      182 / 182 (100.0%)  [437953 itens/min]
adicionando usuários a grupos...

importando tópicos...
      132 / 132 (100.0%)  [3567036 itens/min]
importando posts...
/home/dev/discourse/script/import_scripts/nodebb/mongo.rb:100:in `NodeBB::Mongo#post': undefined method `[]' for nil (NoMethodError)

      post["timestamp"] = timestamp_to_date(post["timestamp"])
                                                ^^^^^^^^^^^^^
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in `block in NodeBB::Mongo#posts'
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in `Array#map'
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in `NodeBB::Mongo#posts'
        from script/import_scripts/nodebb/nodebb.rb:413:in `block in ImportScripts::NodeBB#import_posts'
        from /home/dev/discourse/script/import_scripts/base.rb:943:in `block in ImportScripts::Base#batches'
        from <internal:kernel>:168:in `Kernel#loop'
        from /home/dev/discourse/script/import_scripts/base.rb:942:in `ImportScripts::Base#batches'
        from script/import_scripts/nodebb/nodebb.rb:412:in `ImportScripts::NodeBB#import_posts'
        from script/import_scripts/nodebb/nodebb.rb:52:in `ImportScripts::NodeBB#execute'
        from /home/dev/discourse/script/import_scripts/base.rb:47:in `ImportScripts::Base#perform'
        from script/import_scripts/nodebb/nodebb.rb:568:in `<main>'


Os usuários parecem ter sido importados corretamente de acordo com a saída do script, veja acima.

Desculpe, você é uma pessoa viva ou apenas um assistente de IA inteligente o suficiente?

Entonces el problema tiene que ver con que no está leyendo los datos de las publicaciones.

Mi suposición es que está aquí y por alguna razón no encuentra las publicaciones.

    def posts(offset = 0, page_size = 2000)
      post_keys = mongo.find(_key: "posts:pid").skip(offset).limit(page_size).pluck(:value)

      post_keys.map { |post_key| post(post_key) }
    end

Soy una persona viva que se ha ganado la vida dando soporte a Discourse durante una década y ha escrito una serie de importadores y ha importado más de cien foros de varias otras plataformas.

Si quieres ayuda de un asistente de IA, consulta https://ask.discourse.com/

6 Me gusta

De alguna manera logré que la función del script funcionara correctamente.

Aquí están mis cambios (con un poco de ayuda de un Claude Code :slight_smile:)

 >mongo.rb
   
   def posts(offset = 0, page_size = 1000)
      post_keys = mongo.find(_key: "posts:pid").skip(offset).limit(page_size).pluck(:value)
      post_keys
          .map { |pid| post(pid) }
          .compact  # <-- elimina cualquier resultado nulo (pids huérfanos)
      post_keys.map { |post_key| post(post_key) }
    end

    def post(id)
    post = mongo.find(_key: "post:#{id}").first
    return nil if post.nil? # <-- comprobación de nulos
    post["timestamp"] = timestamp_to_date(post["timestamp"])
    if post["upvoted_by"] = mongo.find(_key: "pid:#{id}:upvote").first
        post["upvoted_by"] = mongo.find(_key: "pid:#{id}:upvote").first[:members]
      else
        post["upvoted_by"] = []
      end

      post["pid"] = post["pid"].to_s
      post["deleted"] = post["deleted"].to_s

      post
    end
	
>nodebb.rb

 create_posts(posts, total: post_count, offset: offset) do |post|
        # omitir si la publicación es nula
		# omitir si es merged_post
        next if post.nil?
        next if @merged_posts_map[post["pid"]]

        # omitir si está eliminada
        next if post["deleted"] == "1"

        raw = post["content"]
        post_id = "p#{post["pid"]}"

        next if raw.blank?
        topic = topic_lookup_from_imported_post_id("t#{post["tid"]}")

        unless topic
          puts "Tema con id #{post["tid"]} no encontrado, omitiendo"
          next
        end	

Parece que ahora está funcionando de la manera correcta.

Aunque no sé qué tan correcto es esto desde el punto de vista de la arquitectura interna de Discourse, a primera vista parece funcionar.

Cualquier sugerencia de mejora y optimización son bienvenidas.

1 me gusta

Consejo: no dejes que falle de forma completamente silenciosa; cámbialo por esto

if post.nil?
  puts "!!! No se pudo encontrar la publicación #{id}"
  return nil
end

O terminarás preguntándote por qué faltan la mitad de tus publicaciones y después de horas de frustración resulta ser esto.

lo mismo para estos dos

        next if post.nil?
        next if post["deleted"] == "1"
5 Me gusta

Método posts reescrito en el archivo mongo.rb

def posts(offset = 0, page_size = 1000)
post_keys = mongo.find(_key: “posts:pid”).sort(score: 1).skip(offset).limit(page_size).pluck(:value)
post_keys.map { |pid| post(pid)}.compact
end

Introduzca aquí el texto de la cita

Este enfoque asegura que las publicaciones dentro de un tema se ordenen en el orden cronológico correcto. Ascendente.
Observe la llamada sort(score: 1).