Migrar un foro vBulletin 4 a Discourse

I’m only a recent discourse convert, so after a lot of trial and error I’ve combined everything above into a full command by command list (thanks @titusca and @enigmaty).

Hopefully this will help (or at least accelerate) fellow newcomers go from start to finish. Would like to incorporate this into the first post given the updates to mysql->mariadb that I think have thrown a lot of confusion into the process.

Background:

  • 1.6 million post transfer.
  • Utilized Digital Ocean Droplet (CPU Optimized 4 vCPU/8GB)

#1 - Install Digital Ocean Discourse 1-click droplet

#2 - Finish discourse install through SSH by following prompts

Open SSH console
root
(yourrootpassword)
(enter)
(yourdomain).com
(etc…)

#3 - Login to SFTP to upload database dump

sftp root@XXX.XXX.XX.XX
y
yes
(yourrootpassword)
put db.sql /var/discourse/shared/standalone/db.sql

#4 - Login to new discourse website to setup admin account

#5 - Login to SSH - begin process

ssh root@XXX.XXX.XX.XX
cd /var/discourse
./launcher start app
docker exec -it app bash
sudo apt-get update
sudo apt-get upgrade
y

#6 - Install MariaDB (replacement for mysql)

apt-get update && apt-get install mariadb-server-10.3 libmariadbd-dev
y

#7 - Mysql Database Setup

service mysql start
mysql -u root -p
password
create database vbulletin;
exit;

#8 - Vbulletin → Mysql Database Transfer

mysql -u root -p vbulletin < /shared/db.sql
password

#9 - GEM File

echo “gem ‘mysql2’” >>Gemfile
echo “gem ‘mysql2’, require: false” >> /var/www/discourse/Gemfile
echo “gem ‘php_serialize’, require: false” >> /var/www/discourse/Gemfile
cd /var/www/discourse
su discourse -c ‘bundle install --no-deployment --without test --without development --path vendor/bundle’
(Ignore red text result)

#10 - Configure install script

vi /var/www/discourse/script/import_scripts/vbulletin.rb

#10.a - Make edits to text file as needed

DB_HOST ||= ENV[‘DB_HOST’] || “localhost”
DB_NAME ||= ENV[‘DB_NAME’] || “vbulletin”
DB_PW ||= ENV[‘DB_PW’] || “password”
DB_USER ||= ENV[‘DB_USER’] || “root”
TIMEZONE ||= ENV[‘TIMEZONE’] || “America/Los_Angeles”
TABLE_PREFIX ||= ENV[‘TABLE_PREFIX’] || “”
ATTACHMENT_DIR ||= ENV[‘ATTACHMENT_DIR’] || ‘/shared/attachments/’

#10.c - End edits

:wq

#11 - Bundle Config

bundle config set path ‘vendor/bundle’
bundle config set without ‘development:test’
bundle config unset deployment
su discourse -c ‘bundle install’

#12 - Mysql config (may be possible to do this with previous)

mysql --version
sudo mysql -u root -p
password
ALTER USER 'root'@'localhost' IDENTIFIED BY 'password';
FLUSH PRIVILEGES;
exit

#13 - Install Script

su discourse -c ‘bundle exec ruby script/import_scripts/vbulletin.rb’

Good luck!

8 Me gusta

Just wanted to leave feedback after our migration from vB4:

  • FIXED [s]Soft-Deleted posts where not properly hidden: https://github.com/discourse/discourse/pull/12057[/s]
  • [ul] + [li] and nested [LIST] were not migrated properly and the BBcode plugin doesn’t seem to handle this either → This seems to be expected: CommonMark testing started here! (Quote: Core will not implement [ul] [ol] and [li] support for BBCode cause it is a recipe for failure.) → I will need to build some RegEx magic post-fixup for this.
  • We made an initial migration using the normale importer (took > 3 days) and restarted the migration with newer DB snapshots a couple of times to keep the import “fresh” and reduce the downtime to effectively 30 minutes. This procedure worked quite well, except for everything that was edited after we initially imported the threads, posts. We need to manually rework this information now.
  • Creating Plugins for Discourse is really hard due to lack of documentation and a big picture of how the folder structure works. Though it is getting nicer and better after you understand how it works.

Questions that i have left:

  • I not not sure how the importer maps already imported posts and how to match the old vB4 post_id to the new Discourse post_id to hide those “soft-deleted” post. If someone can give me a hint that would be very welcome! Found it: import_id inside the post_custom_fields table. Nice. Now i need to write some handy script to fix this :slight_smile: → Edit: An even better way is to use the importer script, which maps all imported id’s for easy use.
2 Me gusta

Unfortunately I can’t edit my previous post :slight_smile:

I found another issue: Every attachment that is not linked into a post, will not be available to Discourse.

My draft PR for fixing this issue: FIX: vBulletin importer should import unreferenced attachments by paresy · Pull Request #12187 · discourse/discourse · GitHub

Thanks!

3 Me gusta

Just a quick followup on my issue list. I fixed the visibility problem.

Dump all affected posts from your old vBulletin database:

SELECT postid
FROM `vb4_post`
WHERE `visible` > '1'
ORDER BY postid

Make an imported_post_ids.txt file which has all the postid’s line by line

Create a new file for the fixing script:

nano script/import_scripts/fix_visibility.rb 

Content:

require_relative '../../config/environment'
require_relative 'base/lookup_container'

@lookup = ImportScripts::LookupContainer.new

broken_postids = []
broken_real_postids = []

File.foreach("imported_post_ids.txt") do |line|
  broken_postids.append(line.to_i)
end

broken_postids.each do |id|
  broken_real_postids.append(@lookup.post_id_from_imported_post_id(id))
end

broken_real_postids.each do |id|
  puts id
  Post.find(id).trash!
end

Run the script:

su discourse -c 'bundle exec ruby script/import_scripts/fix_visibility.rb'

The script will use the logic from the importer to map the imported post_id’s to the read discourse post_id’s which we want to hide.

4 Me gusta

Hola a todos,

Tengo el script funcionando en una migración de vb3. Estoy haciendo un paso a la vez y actualmente está procesando 122k usuarios a 330/minuto. Luego tendremos 2.5 millones de publicaciones para procesar.

Estamos haciendo esto en un servidor de producción. Nadie está usando el sitio de discourse, simplemente lo configuramos y está en una URL anónima. Si inicio sesión, puedo ver cómo se incrementan las notificaciones de nuevos usuarios. Probablemente sea una pregunta tonta, pero me pregunto si la migración se procesaría más rápido si suspendiéramos o deshabilitáramos el sitio en vivo de alguna manera.

1 me gusta

Eso depende de la carga y la cantidad de CPU en tu servidor de producción. Siempre puedes intentar detener el servidor web durante 5 minutos y ver si la importación va más rápido.

3 Me gusta

La importación lleva mucho tiempo. Que yo sepa, el importador masivo debería ser más rápido. Hicimos una primera importación desde una copia de seguridad en nuestra potente máquina de desarrollo y luego hicimos una incremental desde otra copia de seguridad para cambiar a Discourse con solo media hora de inactividad. Tenga cuidado con las cosas que pueden salir mal al hacer actualizaciones incrementales :slight_smile: (Ver aquí: Migrate a vBulletin 4 forum to Discourse - #132 by paresy)

paresy

3 Me gusta

Veo un núcleo ocupado que creo que es el servidor que ingiere los datos actualizados, y otro núcleo ocupado al ejecutar el script de importación. Realmente no tengo el conocimiento del dominio para saber si la competencia entre esos dos procesos por el recurso de la base de datos podría estar ralentizando el importador, y tampoco tengo el conocimiento del dominio para saber si es posible detener la ingesta mientras se deja el contenedor activo. La ingesta tiene que ocurrir de todos modos, así que supongo que lo más seguro es dejar que siga funcionando.

Un consejo para futuros lectores: veo que 27k (¡22%!) de nuestros usuarios son spambots baneados. Los depuraremos en el lado de la fuente antes de hacer la importación final.

[añadiendo] Una edición necesaria que no veo mencionada arriba:

--- a/script/import_scripts/vbulletin.rb
+++ b/script/import_scripts/vbulletin.rb
@@ -134,6 +133,7 @@ EOM
        , usertitle
        , usergroupid
        , joindate
+       , lastvisit
        , email
        , password
        , salt

Y una edición que puede ser específica de vb3:

--- a/script/import_scripts/vbulletin.rb
+++ b/script/import_scripts/vbulletin.rb
@@ -987,7 +989,7 @@ EOM
   end

   def parse_timestamp(timestamp)
-    Time.zone.at(@tz.utc_to_local(timestamp))
+    Time.zone.at(@tz.utc_to_local(Time.at(timestamp)))
   end

[añadiendo] La importación se está ejecutando en una instancia Oracle Cloud Ampere de 4 núcleos. A modo de comparación, instalé un servidor de desarrollo de Discourse localmente/nativo en un MacBook Air M1 y me sorprendió que el proceso de importación fuera significativamente más lento.

6 Me gusta

¿Estabas recibiendo errores con el script preexistente? Perdí la información de fecha y hora de todas nuestras antiguas publicaciones de vBulletin 4 debido a eso. Si esta es una solución, me encantaría saber si la reimportación sería una buena idea si todas las publicaciones han sido copiadas.

2 Me gusta

Sí, el script daría un error porque estaba alimentando un entero a una función de tiempo.

3 Me gusta

No. El script omite las publicaciones que ya han sido importadas.

3 Me gusta

Hola,

¿Descubriste cómo solucionar esto?

Nuestros dos foros principales/inferiores tienen parentid = -1 (creo que esto se debe a que convertimos de v3 en su momento).

No estoy seguro de cómo proceder, ¿simplemente los establezco en 0 si es -1 en el script de conversión? ¿Suponiendo que 0 es la categoría principal de Discourse?

En realidad, mirando el sitio de Discourse ahora; ¿parece que esos dos son los únicos que se han importado?

 importando categorías de nivel superior...
         2 / 2 (100,0%)  [211 elementos/min]  en]
 importando categorías secundarias...
 Traceback (la llamada más reciente fue desde script/import_scripts/vbulletin.rb:1003:in `<main>')
         4: from /var/www/discourse/script/import_scripts/base.rb:47:in `perform'
         3: from script/import_scripts/vbulletin.rb:84:in `execute'
         2: from script/import_scripts/vbulletin.rb:287:in `import_categories'
         1: from script/import_scripts/vbulletin.rb:287:in `each'
script/import_scripts/vbulletin.rb:289:in `block in import_categories': undefined method `[]' for nil:NilClass (NoMethodError)
1 me gusta

Probablemente. He hecho un montón de importaciones de vBulletin desde entonces. :person_shrugging:

Solo tendrás que intentarlo y ver qué pasa. Parece lo mismo que describí.

Simplemente modificaría el script para . . . hacer algo . . . si esa cosa es nula.

1 me gusta

Absolutamente, pero no sé lo suficiente sobre cómo funciona el discurso para saber a qué configurarlo.
¿Qué haría el discurso si los configuro a un número aleatorio como 0? ¿O debería encontrar un número de categoría que ya esté en la base de datos y configurarlo a ese?

No soy muy fuerte en Ruby, ¿crees que esto funcionaría?

        if categories.detect { |c| c["forumid"] == cc["parentid"] }["parentid"].nil?
          cc["parentid"] = 52
        else
          cc["parentid"] = categories.detect { |c| c["forumid"] == cc["parentid"] }["parentid"]
        end

En realidad, parece que hay muchos foros eliminados cuyo parentid ya no existe.

EDITAR
Acabo de configurarlos todos a un tema principal, y podré arreglarlo más tarde.

1 me gusta

Finalmente llegamos a la parte de importación de archivos adjuntos, llegó a alrededor del 1.9% y ahora obtenemos este error

    67406 / 3550728 (  1.9%)  Traceback (most recent call last):
        23: from script/import_scripts/vbulletin.rb:1006:in `<main>'
        22: from /var/www/discourse/script/import_scripts/base.rb:47:in `perform'
        21: from script/import_scripts/vbulletin.rb:88:in `execute'
        20: from script/import_scripts/vbulletin.rb:610:in `import_attachments'
        19: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-6.1.4.1/lib/active_record/querying.rb:22:in `find_each'
        18: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-6.1.4.1/lib/active_record/relation/batches.rb:70:in `find_each'
        17: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-6.1.4.1/lib/active_record/relation/batches.rb:137:in `find_in_batches'
        16: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-6.1.4.1/lib/active_record/relation/batches.rb:229:in `in_batches'
        15: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-6.1.4.1/lib/active_record/relation/batches.rb:229:in `loop'
        14: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-6.1.4.1/lib/active_record/relation/batches.rb:245:in `block in in_batches'
        13: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-6.1.4.1/lib/active_record/relation/batches.rb:138:in `block in find_in_batches'
        12: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-6.1.4.1/lib/active_record/relation/batches.rb:71:in `block in find_each'
        11: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-6.1.4.1/lib/active_record/relation/batches.rb:71:in `each'
        10: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-6.1.4.1/lib/active_record/relation/batches.rb:71:in `block (2 levels) in find_each'
         9: from script/import_scripts/vbulletin.rb:651:in `block in import_attachments'
         8: from script/import_scripts/vbulletin.rb:651:in `each'
         7: from script/import_scripts/vbulletin.rb:659:in `block (2 levels) in import_attachments'
         6: from /var/www/discourse/script/import_scripts/base.rb:873:in `html_for_upload'
         5: from /var/www/discourse/script/import_scripts/base/uploader.rb:40:in `html_for_upload'
         4: from /var/www/discourse/lib/upload_markdown.rb:10:in `to_markdown'
         3: from /var/www/discourse/lib/upload_markdown.rb:19:in `image_markdown'
         2: from /var/www/discourse/app/models/upload.rb:206:in `short_url'
         1: from /var/www/discourse/app/models/upload.rb:534:in `short_url_basename'
/var/www/discourse/app/models/upload.rb:270:in `base62_sha1': undefined method `hex' for nil:NilClass (NoMethodError)

undefined method `hex’ for nil:NilClass (NoMethodError)

¿Alguien tiene alguna idea de cómo solucionar esto?

¿Está intentando leer short_url_basename, y devuelve nil; ¿por lo que .hex falla?

1 me gusta

Mi suposición, sin ver el código, es que falta el archivo o tal vez hay un campo de nombre de archivo y está vacío. Probablemente pondría un puts en import_attachments y vería qué hay en el registro que está intentando importar.

1 me gusta

¡Gracias por la ayuda! Soy nuevo en Ruby, ¿sería esta la forma correcta de hacerlo?

      unless mapping[post.id].nil? || mapping[post.id].empty?
        mapping[post.id].each do |attachment_id|
          upload, filename = find_upload(post, attachment_id)
          unless upload
            fail_count += 1
            next
          end

          puts "{short_url_basename}"

          # la deduplicación de subidas internas se asegurará de que no importemos archivos adjuntos de nuevo
          html = html_for_upload(upload, filename)
          if !new_raw[html]
            new_raw += "\n\n#{html}\n\n"
          end
        end
      end

Ajá, short_url_basename es una función, así que eso no funcionará.

¿Es simplemente puts "{post}"? ¿Y mostrará todo el contenido del objeto post?

Esta parece ser la línea en la que falla en upload.rb

upload_markdown 19
"![#{@upload.original_filename}|#{@upload.width}x#{@upload.height}](#{@upload.short_url})"

upload.rb 534
"#{Upload.base62_sha1(sha1)}#{extension.present? ? ".#{extension}" : ""}"

upload.rb 270
Base62.encode(sha1.hex)

Entonces, es upload.original_filename, upload.width, upload.height o upload.short_url.

Entonces, si hago una verificación de nulo en upload_markdown, ¿no debería prevenir el error?

¿Necesita el shortURL para que funcione? ¿Podría simplemente crear mi propio shortURL aleatorio?

2 Me gusta

Creo que ahí está el problema. No encuentra la carga, por lo que devuelve nil. Quizás el archivo falte o sea inválido.

1 me gusta

Pero, ¿no lo atraparía esto entonces?

unless upload
  fail_count += 1
  next
end

¿O unless no comprueba si es nil?

¿O pasa porque creó el objeto upload, pero falta la propiedad upload.short_url en el objeto upload quizás?

1 me gusta

Lo siento. Correcto. Eso lo atraparía. Me temo que por eso este nivel de depuración no es realmente apropiado para un foro. :person_shrugging:

Sin embargo, vas por buen camino. Sigue adelante. Parece que sabes lo suficiente para resolverlo. Escribí al menos un par de importadores antes de aprender Ruby.

1 me gusta