Migrar um fórum vBulletin 4 para Discourse

Sou apenas um converso recente do Discourse, então, após muita tentativa e erro, combinei tudo acima em uma lista completa, comando por comando (obrigado @titusca e @enigmaty).

Espero que isso ajude (ou pelo menos acelere) outros recém-chegados a ir do início ao fim. Gostaria de incorporar isso ao primeiro post, considerando as atualizações de mysql- para mariadb que, acredito, causaram muita confusão no processo.

Contexto:

  • Transferência de 1,6 milhão de posts.
  • Utilizei um Droplet da Digital Ocean (CPU Otimizada, 4 vCPU/8GB)

#1 - Instalar o Droplet de 1 clique do Discourse da Digital Ocean

#2 - Concluir a instalação do Discourse via SSH seguindo as instruções

Abrir console SSH
root
(sua senha de root)
(enter)
(seu dominio).com
(etc…)

#3 - Fazer login no SFTP para fazer upload do dump do banco de dados

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

#4 - Fazer login no novo site do Discourse para configurar a conta de administrador

#5 - Fazer login no SSH - iniciar o processo

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 (substituto do mysql)

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

#7 - Configuração do banco de dados Mysql

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

#8 - Transferência de Vbulletin para o banco de dados Mysql

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

#9 - Arquivo 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’
(Ignore o resultado em texto vermelho)

#10 - Configurar script de instalação

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

#10.a - Fazer edições no arquivo de texto conforme necessário

DB_HOST ||= ENV[‘DB_HOST’] || “localhost”
DB_NAME ||= ENV[‘DB_NAME’] || “vbulletin”
DB_PW ||= ENV[‘DB_PW’] || “senha”
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 - Encerrar edições

:wq

#11 - Configuração do Bundle

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

#12 - Configuração do Mysql (talvez seja possível fazer isso com o anterior)

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

#13 - Script de Instalação

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

Boa sorte!

8 curtidas

Só queria deixar um feedback após nossa migração do vB4:

  • [s]FIXADO: Posts com exclusão suave não estavam sendo ocultados corretamente: https://github.com/discourse/discourse/pull/12057[/s]
  • [ul] + [li] e [LIST] aninhados não foram migrados corretamente, e o plugin BBCode também não parece lidar com isso → Isso parece ser esperado: CommonMark testing started here! (Citação: O núcleo não implementará suporte a [ul], [ol] e [li] para BBCode, pois é uma receita para o fracasso.) → Vou precisar criar algum truque com RegEx para correção pós-migração.
  • Fizemos uma migração inicial usando o importador normal (levou > 3 dias) e reiniciamos a migração com snapshots mais recentes do banco de dados algumas vezes para manter a importação “atualizada” e reduzir o tempo de inatividade para efetivamente 30 minutos. Esse procedimento funcionou muito bem, exceto para tudo que foi editado após a importação inicial dos tópicos e posts. Agora precisamos reprocessar manualmente essas informações.
  • Criar plugins para o Discourse é realmente difícil devido à falta de documentação e de uma visão geral de como a estrutura de pastas funciona. Embora fique mais agradável e melhor depois que você entende como funciona.

Perguntas que ainda tenho:

  • Não tenho certeza de como o importador mapeia os posts já importados e como associar o post_id antigo do vB4 ao novo post_id do Discourse para ocultar esses posts com exclusão suave. Se alguém puder me dar uma dica, seria muito bem-vindo! Encontrei: import_id dentro da tabela post_custom_fields. Legal. Agora preciso escrever um script prático para corrigir isso :slight_smile: → Edição: Uma maneira ainda melhor é usar o script do importador, que mapeia todos os IDs importados para facilitar o uso.
2 curtidas

Infelizmente, não consigo editar minha postagem anterior :slight_smile:

Encontrei outro problema: todo anexo que não está vinculado a uma postagem não estará disponível para o Discourse.

Meu PR de rascunho para corrigir esse problema: FIX: vBulletin importer should import unreferenced attachments by paresy · Pull Request #12187 · discourse/discourse · GitHub

Obrigado!

3 curtidas

Apenas um breve acompanhamento sobre minha lista de problemas. Eu corrigi o problema de visibilidade.

Exporte todos os posts afetados do seu banco de dados vBulletin antigo:

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

Crie um arquivo chamado imported_post_ids.txt contendo todos os postid, um por linha

Crie um novo arquivo para o script de correção:

nano script/import_scripts/fix_visibility.rb

Conteúdo:

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

Execute o script:

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

O script usará a lógica do importador para mapear os postid importados para os postid legíveis do Discourse que queremos ocultar.

4 curtidas

Olá pessoal,

Tenho o script rodando em uma migração vb3. Estou fazendo um passo de cada vez e ele está atualmente processando 122 mil usuários a 330/minuto. Depois teremos 2,5 milhões de posts para processar.

Estamos fazendo isso em um servidor de produção. Ninguém está usando o site discourse, nós apenas o configuramos e ele está em uma URL anônima. Se eu fizer login, posso ver as notificações de novos usuários aumentando. Provavelmente é uma pergunta boba, mas me pergunto se a migração processaria mais rápido se suspendêssemos ou desabilitássemos o site ao vivo de alguma forma?

1 curtida

Isso depende da carga e da quantidade de CPUs no seu servidor de produção. Você sempre pode tentar parar o servidor web por 5 minutos e ver se a importação fica mais rápida.

3 curtidas

A importação demora muito. Pelo que sei, o importador em massa deve ser mais rápido. Fizemos uma primeira importação de um backup em nossa máquina de desenvolvimento robusta e, em seguida, fizemos uma incremental de outro backup para fazer a transição para o Discourse com apenas meia hora de inatividade. Cuidado com as coisas que podem dar errado ao fazer atualizações incrementais :slight_smile: (Veja aqui: Migrate a vBulletin 4 forum to Discourse - #132 by paresy)

paresy

3 curtidas

Vejo um núcleo ocupado que acredito ser o servidor ingerindo os dados atualizados, e outro núcleo ocupado ao executar o script de importação. Eu realmente não tenho o conhecimento de domínio para saber se a competição entre esses dois processos pelo recurso do banco de dados pode estar retardando o importador, e também não tenho o conhecimento de domínio para saber se é possível parar a ingestão enquanto o contêiner permanece ativo. A ingestão tem que acontecer de qualquer forma, então suponho que a coisa mais segura a fazer é apenas deixá-la continuar processando.

Uma dica para futuros leitores, vejo que 27 mil (22%!) de nossos usuários são spambots banidos. Nós os purgaremos no lado da origem antes de fazer a importação final.

[adicionando] Uma edição necessária que não vejo mencionada acima:

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

E uma edição que pode ser específica do 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

[adicionando] A importação está sendo executada em uma instância Ampere de 4 núcleos da Oracle Cloud. Para comparação, instalei um servidor de desenvolvimento Discourse localmente/nativamente em um MacBook Air M1 e fiquei surpreso que o processo de importação foi significativamente mais lento.

6 curtidas

Você estava recebendo erros com o script pré-existente? Perdi as informações de data e hora de todas as nossas postagens antigas do vBulletin 4 por causa disso. Se esta for uma correção, eu adoraria saber se a reimportação seria uma boa ideia se todas as postagens tivessem sido copiadas.

2 curtidas

Sim, o script daria erro porque estava alimentando um inteiro a uma função de tempo.

3 curtidas

Não. O script ignora postagens que já foram importadas.

3 curtidas

Olá,

Você descobriu como consertar isso?

Nossos dois fóruns principais/inferiores têm parentid = -1 (acho que isso se deve à nossa conversão do v3 antigamente).

Não tenho certeza de como prosseguir, devo apenas defini-los como 0 se for -1 no script de conversão? Assumindo que 0 é a categoria principal do Discourse?

Na verdade, olhando o site do Discourse agora; esses dois parecem ser os únicos que foram importados?

 importing top level categories...
         2 / 2 (100.0%)  [211 items/min]  in]
 importing children categories...
 Traceback (most recent call last):
         5: from 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 curtida

Provavelmente. Eu fiz várias importações do vBulletin desde então. :person_shrugging:

Você só terá que tentar e ver o que acontece. Parece a mesma coisa que descrevi.

Eu apenas modificaria o script para . . . fazer alguma coisa . . . se essa coisa for nula.

1 curtida

Com certeza, mas não sei o suficiente sobre como o discourse funciona para saber o que definir.
O que o discourse faria se eu definisse um número aleatório como 0? Ou devo encontrar um número de categoria já no banco de dados e defini-lo para ele?

Não sou muito bom em Ruby, você acha que isso funcionaria?

        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

Na verdade, parece que há muitos fóruns excluídos cujo parentid não existe mais.

EDIT
Acabei de definir todos eles para um tópico pai e posso corrigir mais tarde.

1 curtida

Chegamos finalmente à parte de importação de anexos, chegou a cerca de 1,9% e agora recebemos este erro

    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)

Alguém tem alguma ideia de como corrigir isso?

Está tentando ler short_url_basename, e ele retorna nil; então .hex falha?

1 curtida

Minha suposição, sem olhar o código, é que o arquivo está faltando ou talvez haja um campo de nome de arquivo e ele esteja vazio? Eu provavelmente colocaria um puts em import_attachments e veria o que está no registro que ele está tentando importar.

1 curtida

Obrigado pela ajuda! Sou novo no Ruby, esta seria a maneira correta de fazer isso?

      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}"

          # A desduplicação interna de upload garantirá que não importemos anexos novamente
          html = html_for_upload(upload, filename)
          if !new_raw[html]
            new_raw += "\n\n#{html}\n\n"
          end
        end
      end

Aha, short_url_basename é uma função, então isso não funcionará.

É simplesmente, puts “{post}”? E isso exibirá todo o conteúdo do objeto post?

Esta parece ser a linha em que está falhando em 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)

Então é upload.original_filename, upload.width, upload.height ou upload.short_url então

Então, se eu fizer uma verificação de nulo em upload_markdown, isso deve evitar o erro, certo?

Precisa do shortURL para funcionar; posso criar meu próprio shortURL aleatório?

2 curtidas

Acho que é aí que está o problema. Ele não encontra o upload, então retorna nil. Talvez o arquivo esteja faltando ou seja inválido.

1 curtida

Mas isso não o pegaria então?

unless upload
  fail_count += 1
  next
end

Ou unless não verifica nil?

Ou ele passa porque criou o objeto upload, mas a propriedade upload.short_url no objeto upload está faltando, talvez?

1 curtida

Desculpe. Certo. Isso resolveria. Receio que seja por isso que este nível de depuração não é realmente apropriado para um fórum. :person_shrugging:

Você está no caminho certo, no entanto. Continue. Parece que você sabe o suficiente para descobrir. Eu escrevi pelo menos alguns importadores antes de aprender Ruby.

1 curtida