Migrar un foro XenForo a Discourse

En el foro que transferí, una vez se instaló xengallery, por lo que tuve que cambiar lo siguiente, ya que la tabla xfgallery ya no existía.

  def get_xf_sql(type, id)
    case type
    when :gallery
      return "SELECT NULL WHERE 1=0;"
    when :attachment
      <<-SQL
        SELECT a.attachment_id, a.data_id, d.filename, d.file_hash, d.user_id
        FROM #{TABLE_PREFIX}attachment AS a
        INNER JOIN #{TABLE_PREFIX}attachment_data d ON a.data_id = d.data_id
        WHERE attachment_id = #{id}
        AND content_type = 'post'
      SQL
    end
  end
1 me gusta

Puedes intentar ejecutar el script de nuevo y ver si termina.

¿Tu sistema está en una sola partición?

El paso de instalación de mysql-server ahora está obsoleto. Necesita instalar mariadb-server.

sudo apt-get install mariadb-server mariadb-client libmariadb-dev-compat libmariadb-dev

Pude seguir el resto de los pasos e importar la base de datos XF, por lo que alguien debería actualizar la guía.

Ahora estoy atascado en el siguiente paso y necesito ayuda.

echo "gem 'mysql2'" >> Gemfile
bundle install --no-deployment

Ejecutar lo anterior me da el siguiente error. Revisé el Gemfile y solo contiene esta línea: gem ‘mysql2’

Este Gemfile no incluye una fuente global explícita.
No usar una fuente global explícita puede resultar en que se genere un lockfile diferente dependiendo de las gemas que hayas instalado localmente antes de ejecutar bundler.
En su lugar, define una fuente global en tu Gemfile de esta manera: source "https://rubygems.org".
No se pudo encontrar la gema 'mysql2' en las gemas instaladas localmente.
root@ip-172-566-459-13-app:/#

Ok, así que logré pasar al siguiente paso. Alguien arriba publicó que necesitamos estar en la carpeta /var/www/discourse en el contenedor y luego agregar el gem.

Ahora, en el paso final

RAILS_ENV=production bundle exec ruby script/import_scripts/xenforo.rb

Estoy recibiendo este error. ¿Qué podría estar haciendo mal?

`/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/activerecord-7.2.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:63:in “rescue in new_client”: No pudimos encontrar tu base de datos: discourse.
Las configuraciones de base de datos disponibles se pueden encontrar en config/database.yml. (ActiveRecord::NoDatabaseError) Para resolver este error:

  • ¿No creaste la base de datos o la eliminaste? Para crear la base de datos, ejecuta:
    bin/rails db:create
  • ¿Ha cambiado el nombre de la base de datos? Verifica que config/database.yml contenga el nombre de base de datos correcto.`

Lo resolví: Estaba ejecutando como usuario root, tuve que cambiar al usuario ‘discourse’. La importación ha comenzado.

4 Me gusta

Recogí un servidor razonablemente bueno con 4 CPU y 16 GB de RAM. A la velocidad a la que se están migrando las publicaciones, me llevará 9 días solo para migrar las publicaciones. Los usuarios tardaron 2,5 horas en migrarse. Es seguro decir que esto no me servirá tal como está, pero al menos puedo pasar algunos meses familiarizándome hasta que encuentre una solución para esta migración masiva.

PD:
En el script de migración, veo que los correos electrónicos duplicados no se importan. ¿Cuáles son las diferentes formas en que se determina un duplicado? Noté que xyz@gmail.com se trata igual que xyz+1@gmail.com y xy.z@gmail.com

¿Hay algún otro patrón también?

He intentado hacer migraciones en VPS con especificaciones similares a mi ordenador personal, pero por alguna razón siempre fue mucho, mucho más lento que usar mi ordenador.

Hoy en día, siempre hago mis migraciones localmente. ¿Cuántas publicaciones tienes?

1 me gusta

2.5 millones de publicaciones.
Intentaré la migración local en una Mac M1 para comparar.

1 me gusta

Eso es prácticamente todo. La comprobación de unicidad se realiza en la versión normalizada y en minúsculas de la dirección de correo electrónico proporcionada.

Normalizamos eliminando todos los puntos e ignorando todo lo que sigue a + en el nombre de usuario.

3 Me gusta

La velocidad de una sola CPU es el factor importante.

En mis máquinas, una tasa de 800-1000 usuarios o publicaciones/minuto es bastante típica.

Tenga en cuenta que cuando realice la importación final, solo importará los usuarios y las publicaciones que aún no se hayan importado, por lo que no llevará mucho tiempo.

Desactive la configuración del sitio Normalizar correos electrónicos (desactivado era el valor predeterminado hasta hace poco). Probablemente debería desactivarse en esta función aquí:

Puede ponerlo en su versión personalizada del script de xenforo con SiteSetting.normalize_emails=false. No estoy seguro de qué sucedió con esos usuarios de correo electrónico duplicados; hay dos cosas obvias que hacer: darles una dirección de correo electrónico falsa o omitir su importación. ¿Parece que les da unas falsas? (Y existe una probabilidad bastante buena de que, de hecho, sean usuarios falsos de todos modos). Si los omitió, ejecutar el script nuevamente los importará.

3 Me gusta

Sí, en mi portátil, está procesando las cosas mucho más rápido a 1000 elementos por minuto. Eso es aproximadamente 2 veces más rápido que en el servidor. Aun así, eso son unos 3 días.

Revisé los correos electrónicos omitidos y parece que está haciendo un buen trabajo ignorando esas cuentas. Simplemente las fusionaré antes de la importación final. Apenas 20 casos de este tipo.

Ten en cuenta que cuando hagas la importación final, solo importará los usuarios y las publicaciones que aún no se hayan importado, por lo que no llevará mucho tiempo.

Gracias por señalar esto. Lo observé yo mismo y parece que esto es lo que salvará el día cuando haga la importación final. Entonces, ¿hago una copia de seguridad y la restauro en D-3 y luego otra copia de seguridad y la restauro con el nuevo archivo de copia de seguridad de la base de datos en el Día 0? ¿Es correcto?

1 me gusta

¿Esas copias de seguridad y restauraciones son en el sitio de Xenforo, o tienes algún sitio de Discourse en vivo al que vas a importar los datos de Xenforo?

Mientras no realices cambios en el script que requieran volver a importar datos, y lo que tengas ahora en tu portátil sea lo que quieres en tu servidor de Discourse, entonces puedes seguir obteniendo nuevas volcados de la base de datos de Xenforo y importándolos (para probar, ver cuánto tiempo lleva, etc.) y luego, el día del cambio, congelas el sitio de Xenforo, obtienes esa base de datos, ejecutas el script una vez más y lo subes a tu servidor de Discourse.

Si ya tienes datos en tu sitio de Discourse que quieres conservar, las cosas son mucho más complicadas ya que necesitarás congelar ese sitio, luego obtener los datos de Xenforo y luego proceder como se describe arriba.

1 me gusta

Será una instalación limpia de Discourse, lo que lo hace sencillo.

Tengo bastante tiempo libre, ya que quiero probar las migraciones varias veces, familiarizarme a fondo con Discourse, configurar todos los complementos como quiero y quizás también ensuciarme las manos con la personalización de algunos complementos.

Lo que has explicado me quita un gran peso de encima, ya que pensaba que también tendría que averiguar las importaciones masivas.

2 Me gusta

He vuelto con una consulta, ¿el script de importación genera algún registro? Mi importación de prueba está atascada en el 98,2 % desde hace unas horas.

Otra cosa que me di cuenta es que, si reinicio la migración, tarda unos 30 segundos en omitir un lote de 1000 publicaciones. Por lo tanto, la velocidad efectiva es ahora de 2000 elementos por minuto. No es una mejora significativa con respecto a las 1000 publicaciones por minuto de la primera importación, ya que incluso en la última importación el día del corte, llevará aproximadamente un día. 23 horas de las cuales solo se omitirán elementos ya importados.

Solo lo que ves.

Probablemente deberías detenerla y volver a iniciarla.

Sí, omitirá todos los datos que ya se hayan importado. Y lo hace mucho más rápido que 2000 publicaciones/minuto. Sospecho que lo verás cuando la reinicies ahora.

Eso es, reinicié y luego hice la publicación anterior. Son 2000 publicaciones/minuto. Para estar seguro, lo intenté de nuevo.

1 me gusta

Así que logré importar los avatares y los archivos adjuntos. Copié estas carpetas.

/internal_data/attachments
/data/avatars

Para responder a mi pregunta, los avatares y los archivos adjuntos se finalizan una vez importados. Si un usuario cambia su avatar después de que se importe su ID, no se importará/actualizará porque esa publicación o usuario se omitirá en la segunda ejecución.

Ahora solo necesito averiguar la importación de conversaciones (también se puede omitir, pero es bueno tenerla) y las redirecciones permanentes.

@Fajfi - Gracias por tu contribución al script de importación. Funcionó a la perfección para los avatares y los archivos adjuntos. Todavía se está ejecutando y aún no he llegado a la parte de “likes”.

Se ha corregido la importación de conversaciones. Se pudieron importar más de medio millón de mensajes de XF2.3 a Discourse. Se ha enviado una PR en caso de que alguien esté interesado.

----EDIT----

Se ha enviado otra PR con una corrección para la importación de “likes”. Es sorprendente que nadie haya migrado de XF2.1+ a Discourse hasta ahora. Los “likes” fueron renombrados a “reactions” en 2019 cuando se lanzó XF2.1.

6 Me gusta

Tuve algunos problemas con las adjunciones que no se importaban.

Esto se manifestaba con el [attach]número[/attach] en las publicaciones.

Creo que esto se debe a los nombres de archivo hash que a veces se desincronizan con el registro de la base de datos.

Cambié esta parte del código:

def import_xf_attachment(data_id, file_hash, owner_id, original_filename)
    matching_files = Dir.glob(File.join(ATTACHMENT_DIR, "**", "#{data_id}-*.data"))
    filename = matching_files.first

    upload = nil
    if filename && File.exist?(filename)
      path = Pathname.new(filename)
      new_path = path.dirname.join(original_filename)
      FileUtils.cp path, new_path
      upload = @uploader.create_upload owner_id, new_path, original_filename
      FileUtils.rm(new_path) if File.exist?(new_path)
    else
      STDERR.puts "No se pudo encontrar el archivo para data_id #{data_id}. Omitiendo."
    end
    upload
  end

Parece funcionar en mi importación de XenForo 2.3 a Discourse.

2 Me gusta

Aún no he obtenido un buen resultado. Falló después de importar 3/4 de millón de registros y se quejó de que XFMG no estaba presente.

Tendré que reunir más entusiasmo para que funcione.

He propuesto una arquitectura limpia para el nuevo servicio. Pero “limpio” no siempre significa “simple”.]/i) do
username = $1
imported_post_id = $2

  if imported_post_id.present?
    topic_mapping = topic_lookup_from_imported_post_id(imported_post_id.to_i)
    if topic_mapping
      "\n[quote=\"#{username}, post:#{topic_mapping[:post_number]}, topic:#{topic_mapping[:topic_id]}\"]\n"
    else
      "\n[quote=\"#{username}\"]\n"
    end
  else
    "\n[quote=\"#{username}\"]\n"
  end
end

s.gsub!(%r{\[/QUOTE\]}i, "\n[/quote]\n")
s.gsub!(%r{\[HEADING=1\](.+?)\[/HEADING\]}i) { "\n# #{$1}\n" }
s.gsub!(%r{\[HEADING=2\](.+?)\[/HEADING\]}i) { "\n## #{$1}\n" }
s.gsub!(%r{\[HEADING=3\](.+?)\[/HEADING\]}i) { "\n### #{$1}\n" }

s.gsub!(%r{\[SPOILER="?([^\]]*?)"?\](.*?)\[/SPOILER\]}im) do
  title = $1.presence || "Spoiler"
  content = $2
  "\n[details=\"#{title}\"]\n#{content}\n[/details]\n"
end

s.gsub!(%r{\[CODE="?([a-zA-Z0-9_\-+]*)"?\](.*?)\[/CODE\]}im) do
  lang = $1.presence || ""
  code = $2
  "\n```#{lang}\n#{code}\n```\n"
end

s.gsub!(%r{[USER=\d+]@?(.+?)[/USER]}i, ‘@\1’)
s.gsub!(%r{[MEDIA=youtube](.+?)[/MEDIA]}i, ‘https://www.youtube.com/watch?v=\1’)
s.gsub!(%r{[MEDIA=[^]]+](.+?)[/MEDIA]}i, ‘\1’)

# Eliminar etiquetas BBCode presentacionales que Markdown no utiliza
s.gsub!(%r{\[/?(?:LEFT|RIGHT|CENTER|JUSTIFY|FONT|SIZE|COLOR|INDENT)(?:=[^\]]+)?\]}i, '')

# Procesar y reemplazar las etiquetas [ATTACH] con subidas de Discourse
s = process_xf_attachments("post", s, import_id) if import_id.to_i > 0

s.strip

end

=========================================================================

11. Resolución de adjuntos y manejo de archivos

=========================================================================

def process_xf_attachments(content_type, text, content_id)
sql = "
SELECT a.attachment_id, a.data_id, d.filename, d.file_hash, d.user_id
FROM #{TABLE_PREFIX}attachment a
INNER JOIN #{TABLE_PREFIX}attachment_data d ON a.data_id = d.data_id
WHERE a.content_type = ‘#{@client.escape(content_type.to_s)}’
AND a.content_id = #{content_id.to_i}
"
attachments = mysql_query(sql).to_a
return text if attachments.empty?

embedded_ids = Set.new

attachments.each do |att|
  att_id = att[:attachment_id]
  upload = import_xf_attachment_file(att[:data_id], att[:file_hash], att[:user_id], att[:filename])
  next unless upload&.persisted?

  html = @uploader.html_for_upload(upload, att[:filename])

  # Coincidir con [ATTACH]123[/ATTACH] estándar así como con variantes de atributos de XF 2.3 como [ATTACH type="full" alt="..."]123[/ATTACH]
  pattern = %r{\[ATTACH[^\]]*\]\s*#{att_id}\s*\[/ATTACH\]}i
  if text.match?(pattern)
    text.gsub!(pattern, "\n#{html}\n")
    embedded_ids.add(att_id)
  end
end

# Agregar cualquier adjunto que no haya sido colocado en línea en el cuerpo del texto
unattached = attachments.reject { |a| embedded_ids.include?(a[:attachment_id]) }
if unattached.any?
  text += "\n\n"
  unattached.each do |att|
    upload = import_xf_attachment_file(att[:data_id], att[:file_hash], att[:user_id], att[:filename])
    next unless upload&.persisted?
    html = @uploader.html_for_upload(upload, att[:filename])
    text += "#{html}\n"
  end
end

# Pasa final de limpieza: purgar cualquier marca [ATTACH] huérfana o ausente
text.gsub!(%r{\[ATTACH[^\]]*\]\s*\d*\s*\[/ATTACH\]}i, '')

text

end

def import_xf_attachment_file(data_id, file_hash, owner_id, original_filename)
group_id = data_id.to_i / 1000

exact_name   = "#{data_id}-#{file_hash}.data"
path_grouped = Pathname.new(File.join(ATTACHMENT_DIR, group_id.to_s, exact_name))
path_flat    = Pathname.new(File.join(ATTACHMENT_DIR, exact_name))

path = if File.exist?(path_grouped)
         path_grouped
       elsif File.exist?(path_flat)
         path_flat
       else
         glob_grouped = Dir.glob(File.join(ATTACHMENT_DIR, group_id.to_s, "#{data_id}-*.data")).first
         glob_flat    = Dir.glob(File.join(ATTACHMENT_DIR, "#{data_id}-*.data")).first
         found = glob_grouped || glob_flat
         if found
           Pathname.new(found)
         else
           log_missing_file(:attachment,
             data_id: data_id,
             filename: original_filename,
             db_hash: file_hash,
             expected_grouped: path_grouped.to_s,
             expected_flat: path_flat.to_s
           )
           return nil
         end
       end

discourse_user_id = user_id_from_imported_user_id(owner_id) || Discourse::SYSTEM_USER_ID

temp_path = path.dirname.join(original_filename)
FileUtils.cp(path, temp_path)

upload = create_upload(discourse_user_id, temp_path, original_filename)
FileUtils.rm(temp_path) if File.exist?(temp_path)

upload

rescue StandardError => e
STDERR.puts “Failed to process attachment data_id #{data_id}: #{e.message}”
nil
end

def mysql_query(sql)
@client.query(sql)
end
end

ImportScripts::XenForo23.new.perform


He tenido algunos usuarios que no pudieron importarse debido a direcciones de correo electrónico extrañas creadas por spambots. Por ejemplo, correos como Peter.Today.Rain.Import.Something@Dave.Gmail.com.

Actualizaré / editaré mi código de importación anterior una vez que se incorporen los campos personalizados, ya que estos son importantes para mí.

.

![image|690x275](upload://9Dj6MAdiJpSiak5nKQFy59y70Yg.jpeg)

![image|690x268](upload://tjfkDVWhujr6qavpYTAcEIVdaCL.jpeg)

![image|690x386](upload://okHsZo8MARU83hyECp3L94rmwr9.png)
4 Me gusta