Migrar un foro vBulletin 4 a Discourse

Soy solo un converso reciente de Discourse, así que tras mucho ensayo y error he combinado todo lo anterior en una lista completa de comandos paso a paso (gracias a @titusca y @enigmaty).

Espero que esto ayude (o al menos acelere) a que otros recién llegados pasen del inicio al final. Me gustaría incorporar esto en el primer puesto, dado las actualizaciones de mysql->mariadb que, creo, han generado mucha confusión en el proceso.

Antecedentes:

  • Transferencia de 1,6 millones de publicaciones.
  • Se utilizó un Droplet de Digital Ocean (CPU Optimizada: 4 vCPU/8GB)

#1 - Instalar el Droplet de Discourse de un clic de Digital Ocean

#2 - Finalizar la instalación de Discourse mediante SSH siguiendo las indicaciones

Abrir consola SSH
root
(tu_contraseña_root)
(enter)
(tudominio).com
(etc…)

#3 - Iniciar sesión en SFTP para subir el volcado de la base de datos

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

#4 - Iniciar sesión en el nuevo sitio web de Discourse para configurar la cuenta de administrador

#5 - Iniciar sesión en SSH - comenzar el proceso

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 - Instalar MariaDB (reemplazo de mysql)

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

#7 - Configuración de la base de datos Mysql

service mysql start
mysql -u root -p
contraseña
create database vbulletin;
exit;

#8 - Transferencia de base de datos Vbulletin → Mysql

mysql -u root -p vbulletin < /shared/db.sql
contraseña

#9 - Archivo GEM

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’
(Ignorar el resultado en texto rojo)

#10 - Configurar el script de instalación

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

#10.a - Realizar las ediciones necesarias en el archivo de texto

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 - Finalizar ediciones

:wq

#11 - Configuración de Bundle

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

#12 - Configuración de Mysql (quizás sea posible hacerlo con el paso anterior)

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

#13 - Script de instalación

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

¡Buena suerte!

8 Me gusta

Solo quería dejar comentarios tras nuestra migración desde vB4:

  • [s]FIXED: Los mensajes eliminados suavemente no se ocultaban correctamente: https://github.com/discourse/discourse/pull/12057[/s]
  • [ul] + [li] y [LIST] anidados no se migraron correctamente y el plugin BBcode tampoco parece manejar esto bien → Esto parece ser lo esperado: CommonMark testing started here! (Cita: El núcleo no implementará soporte para [ul], [ol] y [li] en BBcode porque es una receta para el fracaso.) → Tendré que crear alguna magia con expresiones regulares para una corrección posterior.
  • Realizamos una migración inicial usando el importador normal (tomó > 3 días) y reiniciamos la migración varias veces con nuevas instantáneas de la base de datos para mantener la importación “actualizada” y reducir el tiempo de inactividad a efectivamente 30 minutos. Este procedimiento funcionó bastante bien, excepto por todo lo que se editó después de importar inicialmente los hilos y mensajes. Ahora necesitamos reorganizar manualmente esta información.
  • Crear plugins para Discourse es realmente difícil debido a la falta de documentación y una visión general de cómo funciona la estructura de carpetas. Aunque se vuelve más agradable y mejor una vez que entiendes cómo funciona.

Preguntas que me quedan:

  • No estoy seguro de cómo el importador mapea los mensajes ya importados ni de cómo emparejar el post_id antiguo de vB4 con el nuevo post_id de Discourse para ocultar esos mensajes “eliminados suavemente”. Si alguien puede darme una pista, ¡sería muy bienvenido! Lo encontré: import_id dentro de la tabla post_custom_fields. Genial. Ahora necesito escribir un script práctico para arreglar esto :slight_smile: → Edición: Una forma aún mejor es usar el script del importador, que mapea todos los IDs importados para facilitar su uso.
2 Me gusta

Desafortunadamente, no puedo editar mi publicación anterior :slight_smile:

Encontré otro problema: Cada archivo adjunto que no está vinculado en una publicación no estará disponible para Discourse.

Mi PR de borrador para solucionar este problema: FIX: vBulletin importer should import unreferenced attachments by paresy · Pull Request #12187 · discourse/discourse · GitHub

¡Gracias!

3 Me gusta

Solo un breve seguimiento sobre mi lista de problemas. He solucionado el problema de visibilidad.

Volcar todos los posts afectados de tu base de datos antigua de vBulletin:

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

Crear un archivo imported_post_ids.txt que contenga todos los postid, uno por línea.

Crear un nuevo archivo para el script de corrección:

nano script/import_scripts/fix_visibility.rb

Contenido:

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

Ejecutar el script:

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

El script utilizará la lógica del importador para mapear los postid importados a los postid legibles de Discourse que queremos ocultar.

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