Migrare un forum vBulletin 4 a Discourse

Sono solo un recente convertito a Discourse, quindi dopo molti tentativi ed errori ho combinato tutto quanto sopra in un elenco completo comando per comando (grazie a @titusca e @enigmaty).

Spero che questo possa aiutare (o almeno accelerare) i nuovi arrivati a procedere dall’inizio alla fine. Vorrei incorporarlo nel primo post, dato gli aggiornamenti da mysql a mariadb che, secondo me, hanno creato molta confusione nel processo.

Contesto:

  • Trasferimento di 1,6 milioni di post.
  • Utilizzo di un Droplet Digital Ocean (CPU Optimized 4 vCPU/8GB)

#1 - Installare il droplet Discourse 1-click di Digital Ocean

#2 - Completare l’installazione di Discourse tramite SSH seguendo le istruzioni

Aprire la console SSH
root
(la tua password di root)
(invio)
(iltuodominio).com
(ecc…)

#3 - Accedere a SFTP per caricare il dump del database

sftp root@XXX.XXX.XX.XX
y
yes
(la tua password di root)
put db.sql /var/discourse/shared/standalone/db.sql

#4 - Accedere al nuovo sito Discourse per configurare l’account amministratore

#5 - Accedere via SSH - avviare il 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 - Installare MariaDB (sostituto di mysql)

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

#7 - Configurazione del database MySQL

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

#8 - Trasferimento da Vbulletin al database MySQL

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

#9 - File 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’
(Ignorare il risultato in rosso)

#10 - Configurare lo script di installazione

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

#10.a - Apportare le modifiche necessarie al file di testo

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 - Terminare le modifiche

:wq

#11 - Configurazione di Bundle

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

#12 - Configurazione di MySQL (potrebbe essere possibile farlo con il passaggio precedente)

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

#13 - Script di installazione

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

Buona fortuna!

8 Mi Piace

Volevo solo lasciare un feedback dopo la nostra migrazione da vB4:

  • RISOLTO [s]I post soft-cancellati non erano correttamente nascosti: https://github.com/discourse/discourse/pull/12057[/s]
  • [ul] + [li] e [LIST] nidificati non sono stati migrati correttamente e il plugin BBcode non sembra gestirli nemmeno → Questo sembra essere previsto: CommonMark testing started here! (Citazione: Il core non implementerà il supporto per [ul], [ol] e [li] per BBcode perché è una ricetta per il fallimento.) → Dovrò creare qualche magia con le espressioni regolari per una correzione post-migrazione.
  • Abbiamo effettuato una migrazione iniziale utilizzando l’importatore normale (ha richiesto > 3 giorni) e abbiamo riavviato la migrazione con snapshot DB più recenti un paio di volte per mantenere l’importazione “fresca” e ridurre il tempo di inattività a effettivamente 30 minuti. Questa procedura ha funzionato molto bene, tranne per tutto ciò che è stato modificato dopo l’importazione iniziale dei thread e dei post. Ora dobbiamo rielaborare manualmente queste informazioni.
  • Creare plugin per Discourse è davvero difficile a causa della mancanza di documentazione e di una visione d’insieme di come funziona la struttura delle cartelle. Anche se diventa più semplice e migliore una volta compreso il funzionamento.

Domande che mi sono rimaste:

  • Non sono sicuro di come l’importatore mappi i post già importati e di come abbinare l’old post_id di vB4 al nuovo post_id di Discourse per nascondere quei post “soft-cancellati”. Se qualcuno può darmi un suggerimento, sarebbe molto apprezzato! L’ho trovato: import_id all’interno della tabella post_custom_fields. Ottimo. Ora devo scrivere uno script pratico per risolvere questo problema :slight_smile: → Modifica: Un modo ancora migliore è utilizzare lo script dell’importatore, che mappa tutti gli ID importati per un uso agevole.
2 Mi Piace

Purtroppo non posso modificare il mio post precedente :slight_smile:

Ho trovato un altro problema: ogni allegato non collegato a un post non sarà disponibile per Discourse.

La mia bozza di PR per risolvere questo problema: FIX: vBulletin importer should import unreferenced attachments by paresy · Pull Request #12187 · discourse/discourse · GitHub

Grazie!

3 Mi Piace

Solo un rapido aggiornamento sulla mia lista di problemi. Ho risolto il problema di visibilità.

Estrai tutti i post interessati dal tuo vecchio database vBulletin:

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

Crea un file imported_post_ids.txt contenente tutti gli postid, uno per riga.

Crea un nuovo file per lo script di correzione:

nano script/import_scripts/fix_visibility.rb 

Contenuto:

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

Esegui lo script:

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

Lo script utilizzerà la logica dell’importatore per mappare gli postid importati agli postid di Discourse leggibili che vogliamo nascondere.

4 Mi Piace

Ciao a tutti,

Ho lo script in esecuzione su una migrazione vb3. Sto facendo un passo alla volta e attualmente sta elaborando 122.000 utenti a 330/minuto. Poi avremo 2,5 milioni di post da elaborare.

Stiamo facendo questo su un server di produzione. Nessuno sta usando il sito discourse, lo abbiamo appena configurato e si trova su un URL anonimo. Se accedo, posso vedere le notifiche dei nuovi utenti aumentare. Probabilmente è una domanda stupida, ma mi chiedo se la migrazione verrebbe elaborata più velocemente se sospendessimo o disabilitassimo il sito live in qualche modo?

1 Mi Piace

Dipende dal carico e dal numero di CPU sul tuo server di produzione. Puoi sempre provare a interrompere il web server per 5 minuti e vedere se l’importazione procede più velocemente.

3 Mi Piace

L’importazione richiede molto tempo. Per quanto ne so, l’importatore di massa dovrebbe essere più veloce. Abbiamo eseguito una prima importazione da un backup sulla nostra potente macchina di sviluppo e poi ne abbiamo eseguita una incrementale da un altro backup per passare a Discourse con solo mezz’ora di inattività. Attenzione alle cose che possono andare storte durante gli aggiornamenti incrementali :slight_smile: (Vedi qui: Migrate a vBulletin 4 forum to Discourse - #132 by paresy)

paresy

3 Mi Piace

Vedo un core impegnato che penso sia il server che ingerisce i dati aggiornati, e un altro core impegnato durante l’esecuzione dello script di importazione. Non ho le conoscenze di dominio per sapere se la competizione tra questi due processi per la risorsa del database potrebbe rallentare l’importatore, né ho le conoscenze di dominio per sapere se è anche possibile interrompere l’ingestione lasciando il container attivo. L’ingestione deve comunque avvenire, quindi suppongo che la cosa più sicura da fare sia lasciarla continuare.

Un consiglio per i futuri lettori: vedo che 27.000 (22%!) dei nostri utenti sono bot di spam bannati. Li elimineremo dal lato sorgente prima di eseguire l’importazione finale.

[aggiunta] Una modifica necessaria che non vedo menzionata sopra:

--- 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 una modifica che potrebbe essere specifica per 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

[aggiunta] L’importazione è in esecuzione su un’istanza Oracle Cloud Ampere a 4 core. Per confronto, ho installato un server di sviluppo Discourse localmente/nativamente su un MacBook Air M1 e sono rimasto sorpreso dal fatto che il processo di importazione fosse significativamente più lento.

6 Mi Piace

Stavi riscontrando errori con lo script preesistente? Ho perso le informazioni sulla data e l’ora di tutti i nostri vecchi post di vBulletin 4 a causa di ciò. Se questa è una correzione, mi piacerebbe sapere se una reimportazione sarebbe una buona idea se tutti i post sono stati copiati.

2 Mi Piace

Sì, lo script genererebbe un errore perché stava passando un intero a una funzione di tempo.

3 Mi Piace

No. Lo script salta i post già importati.

3 Mi Piace

Ciao,

Sei riuscito a capire come risolvere questo problema?

I nostri due forum principali/inferiori hanno parentid = -1 (penso che questo sia dovuto alla nostra conversione da v3 in passato).

Non sono sicuro di come procedere, dovrei semplicemente impostarli a 0 se -1 nello script di conversione? Supponendo che 0 sia la categoria principale di discourse?

In realtà, guardando ora il sito di discourse; sembrano essere solo questi due quelli che sono stati importati?

 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 Mi Piace

Probabilmente. Da allora ho eseguito molte importazioni di vBulletin. :person_shrugging:

Dovrai solo provarci e vedere cosa succede. Sembra la stessa cosa che ho descritto.

Modificherei semplicemente lo script per . . . fare qualcosa . . . se quella cosa è nil.

1 Mi Piace

Certamente, ma non so abbastanza su come funziona discourse per sapere cosa impostare.
Cosa farebbe discourse se li impostassi su un numero casuale come 0? O dovrei trovare una categoria già presente nel database e impostarla su quella?

Non sono molto ferrato in Ruby, pensi che questo funzionerebbe?

        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

In realtà sembra che ci siano molti forum eliminati il cui parentid non esiste più.

EDIT
Li ho appena impostati tutti su un argomento padre, e potrò sistemarli più tardi.

1 Mi Piace

Siamo finalmente arrivati alla parte di importazione degli allegati, è arrivata intorno all’1,9% e ora otteniamo questo errore

    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)

Qualcuno ha qualche indizio su come risolvere questo problema?

Sta cercando di leggere short_url_basename, e restituisce nil; quindi .hex fallisce?

1 Mi Piace

La mia ipotesi, senza guardare il codice, è che il file manchi o che ci sia un campo filename e sia vuoto? Probabilmente inserirei un puts in import_attachments e vedrei cosa c’è nel record che sta cercando di importare.

1 Mi Piace

Grazie per l’aiuto! Sono nuovo a Ruby, questo sarebbe il modo corretto per farlo?

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

          # internal upload deduplication will make sure that we do not import attachments again
          html = html_for_upload(upload, filename)
          if !new_raw[html]
            new_raw += "\n\n#{html}\n\n"
          end
        end
      end

Aha, short_url_basename è una funzione, quindi non funzionerà.

È semplicemente, puts “{post}”? E visualizzerà tutto il contenuto dell’oggetto post?

Questa sembra essere la riga che causa l’errore in 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)`

Quindi si tratta di upload.original_filename, upload.width, upload.height o upload.short_url allora

Quindi, se faccio un controllo nil in upload_markdown, dovrebbe prevenire l'errore, giusto?

Ha bisogno dell'URL breve per funzionare; potrei semplicemente creare il mio URL breve casuale?
2 Mi Piace

Penso che il problema sia lì. Non trova l’upload, quindi restituisce nil. Forse il file manca o non è valido.

1 Mi Piace

Ma questo non lo catturerebbe?

unless upload
  fail_count += 1
  next
end

O unless non controlla nil?

O passa perché ha creato l’oggetto upload, ma manca la proprietà upload.short_url nell’oggetto upload, forse?

1 Mi Piace

Mi dispiace. Giusto. Questo lo intercetterebbe. Temo che questo livello di debug non sia appropriato per un forum. :person_shrugging:

Sei sulla strada giusta, però. Continua così. Sembra che tu sappia abbastanza per scoprirlo. Ho scritto almeno un paio di importatori prima di imparare Ruby.

1 Mi Piace