Migrer un forum vBulletin 4 vers 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 « J'aime »

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 « J'aime »

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 « J'aime »

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 « J'aime »

Salut tout le monde,

J’ai lancé le script pour une migration vb3. Je procède étape par étape et il traite actuellement 122 000 utilisateurs à 330/minute. Ensuite, nous aurons 2,5 millions de messages à traiter.

Nous faisons cela sur un serveur de production. Personne n’utilise le site discourse, nous venons de le mettre en place et il est à une URL anonyme. Si je me connecte, je peux voir les notifications de nouveaux utilisateurs s’incrémenter. C’est probablement une question stupide, mais je me demande si la migration serait plus rapide si nous suspendions ou désactivions le site en direct d’une manière ou d’une autre ?

1 « J'aime »

Cela dépend de la charge et du nombre de processeurs de votre serveur de production. Vous pouvez toujours essayer d’arrêter le serveur web pendant 5 minutes et voir si l’importation est plus rapide.

3 « J'aime »

L’importation prend vraiment du temps. Autant que je sache, l’importateur en masse devrait être plus rapide. Nous avons effectué une première importation à partir d’une sauvegarde sur notre machine de développement performante, puis une importation incrémentielle à partir d’une autre sauvegarde pour passer à Discourse avec seulement une demi-heure d’interruption. Méfiez-vous des choses qui peuvent mal tourner lors des mises à jour incrémentielles :slight_smile: (Voir ici : Migrate a vBulletin 4 forum to Discourse - #132 by paresy)

paresy

3 « J'aime »

Je vois un cœur sollicité qui, je pense, est le serveur qui ingère les données mises à jour, et un autre cœur sollicité lors de l’exécution du script d’importation. Je n’ai pas vraiment les connaissances du domaine pour savoir si la compétition entre ces deux processus pour la ressource de base de données pourrait ralentir l’importateur, et je n’ai pas non plus les connaissances du domaine pour savoir s’il est même possible d’arrêter l’ingestion tout en laissant le conteneur actif. L’ingestion doit se faire de toute façon, donc je suppose que la chose la plus sûre à faire est de la laisser continuer à tourner.

Un conseil pour les futurs lecteurs, je vois que 27k (22% !) de nos utilisateurs sont des spambots bannis. Nous les purgerons du côté source avant de faire l’importation finale.

[ajout] Une modification nécessaire qui n’est pas mentionnée ci-dessus :

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

Et une modification qui pourrait être spécifique à 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

[ajout] L’importation s’exécute sur une instance Oracle Cloud Ampere à 4 cœurs. À titre de comparaison, j’ai installé un serveur de développement Discourse localement/nativement sur un MacBook Air M1 et j’ai été surpris que le processus d’importation soit nettement plus lent.

6 « J'aime »

Rencontriez-vous des erreurs avec le script existant ? J’ai perdu les informations de date et d’heure de tous nos anciens messages vBulletin 4 à cause de cela. S’il s’agit d’une correction, j’aimerais savoir si une réimportation serait une bonne idée si tous les messages ont été transférés.

2 « J'aime »

Oui, le script générerait une erreur car il transmettait un entier à une fonction de temps.

3 « J'aime »

Non. Le script ignore les messages qui ont déjà été importés.

3 « J'aime »

Salut,

As-tu trouvé comment résoudre ce problème ?

Nos deux forums principaux/inférieurs ont un parentid = -1 (je pense que c’est dû à notre conversion depuis la v3 à l’époque).

Je ne suis pas sûr de la marche à suivre, dois-je simplement les définir à 0 si c’est -1 dans le script de conversion ? En supposant que 0 est la catégorie principale de Discourse ?

En fait, en regardant le site de Discourse maintenant ; ces deux-là semblent être les seuls qui ont été importés ?

 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 « J'aime »

Probablement. J’ai fait un tas d’importations vBulletin depuis lors. :person_shrugging:

Il vous suffit d’essayer et de voir ce qui se passe. Cela ressemble à la même chose que j’ai décrite.

Je modifierais simplement le script pour . . . faire quelque chose . . . si cette chose est nulle.

1 « J'aime »

Absolument, mais je n’en sais pas assez sur le fonctionnement de Discourse pour savoir à quoi le régler.
Que ferait Discourse si je les réglai à un nombre aléatoire comme 0 ? Ou devrais-je trouver un numéro de catégorie déjà dans la base de données et le régler à celui-ci ?

Pas très solide en Ruby, est-ce que cela fonctionnerait, selon vous ?

        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 fait, il semble qu’il y ait beaucoup de forums supprimés dont le parentid n’existe plus.

EDIT
Je viens de tout régler à un sujet parent, et je pourrai le corriger plus tard.

1 « J'aime »

Nous arrivons enfin à la partie d’importation des pièces jointes, elle était à environ 1,9 % et maintenant nous obtenons cette erreur

    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)

Quelqu’un a une idée sur la façon de résoudre ce problème ?

Essaie-t-il de lire short_url_basename, et il renvoie nil ; donc .hex échoue ?

1 « J'aime »

Ma supposition, sans regarder le code, est que le fichier est manquant ou qu’il y a peut-être un champ filename et qu’il est vide ? Je mettrais probablement un puts dans import_attachments et je verrais ce qu’il y a dans l’enregistrement qu’il essaie d’importer.

1 « J'aime »

Merci pour votre aide ! Je suis nouveau dans Ruby, serait-ce la bonne façon de faire ?

      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 déduplication interne des téléchargements garantira que nous n'importerons pas à nouveau les pièces jointes
          html = html_for_upload(upload, filename)
          if !new_raw[html]
            new_raw += "\n\n#{html}\n\n"
          end
        end
      end

Aha, short_url_basename est une fonction, donc ça ne fonctionnera pas.

Est-ce simplement, puts “{post}” ? Et cela affichera tout le contenu de l’objet post ?

Cela semble être la ligne qui plante dans 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)

Donc, c’est soit upload.original_filename, upload.width, upload.height ou upload.short_url alors

Donc, si je fais une vérification nil dans upload_markdown, cela devrait empêcher l’erreur, n’est-ce pas ?

A-t-il besoin du shortURL pour fonctionner ; pourrais-je simplement créer mon propre shortURL aléatoire ?

2 « J'aime »

Je pense que c’est là que se situe le problème. Il ne trouve pas le téléchargement, donc il renvoie nil. Peut-être que le fichier est manquant ou invalide.

1 « J'aime »

Mais cela ne le détecterait-il pas alors ?

unless upload
  fail_count += 1
  next
end

Ou bien unless ne vérifie-t-il pas nil ?

Ou bien cela passe-t-il parce qu’il a créé l’objet upload, mais que la propriété upload.short_url dans l’objet upload est manquante, peut-être ?

1 « J'aime »

Désolé. C’est exact. Ça le détecterait. Je crains que ce niveau de débogage ne soit pas vraiment approprié pour un forum. :person_shrugging:
Vous êtes sur la bonne voie, cependant. Continuez comme ça. Il semble que vous en sachiez assez pour y arriver. J’ai écrit au moins quelques importateurs avant d’apprendre le Ruby.

1 « J'aime »