Migrare un forum 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 Mi Piace

Just wanted to leave feedback after our migration from vB4:

  • FIXED Soft-Deleted posts where not properly hidden: https://github.com/discourse/discourse/pull/12057
  • [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 Mi Piace

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: https://github.com/discourse/discourse/pull/12187

Thanks!

3 Mi Piace

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