vBulletin 4フォーラムを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

Just wanted to leave feedback after our migration from vB4:

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

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: FIX: vBulletin importer should import unreferenced attachments by paresy · Pull Request #12187 · discourse/discourse · GitHub

Thanks!

「いいね!」 3

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

皆さん、こんにちは。

vb3移行のためにスクリプトを実行しています。1ステップずつ実行しており、現在は122,000ユーザーを1分あたり330件のペースで処理中です。その後、250万件の投稿を処理する必要があります。

これは本番サーバーで行っています。Discourseサイトは誰も使用しておらず、匿名URLで設定したばかりです。ログインすると、新しいユーザー通知が増加しているのがわかります。おそらく愚かな質問ですが、ライブサイトを一時停止または無効にすると、移行処理が速くなるのではないかと思います。

「いいね!」 1

それは、本番サーバーの負荷とCPUの数によって異なります。Webサーバーを5分間停止してみて、インポートが速くなるかどうか試すことができます。

「いいね!」 3

インポートに時間がかかります。私の知る限り、バルクインポータはもっと速いはずです。最初に強力な開発マシンからバックアップをインポートし、次に別のバックアップから増分インポートを行って、ダウンタイムを30分に抑えて Discourse に切り替えました。増分アップデートで起こりうることに注意してください :slight_smile: (こちらを参照: Migrate a vBulletin 4 forum to Discourse - #132 by paresy)

paresy

「いいね!」 3

サーバーが更新されたデータを摂取していると思われるコアが1つと、インポートスクリプトを実行しているときに負荷がかかる別のコアが見えます。それらの2つのプロセスがDBリソースを競合することでインポーターが遅くなる可能性があるかどうか、また、コンテナを稼働させたままインジェストを停止できるかどうかを知るためのドメイン知識が私にはありません。インジェストは anyway に発生する必要があるため、安全なことは、それを継続させることだと思います。

将来の読者のためのヒントとして、ユーザーの27,000人(22%!)が禁止されたスパムボットであることがわかりました。最終インポートを実行する前に、ソース側でそれらをパージします。

[追加] 上記に記載されていない、必要な編集:

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

そして、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

[追加] インポートは、Oracle Cloudの4コアAmpereインスタンスで実行されています。比較のために、M1 MacBook Airにローカル/ネイティブでDiscourse開発サーバーをインストールしましたが、インポートプロセスは著しく遅く実行されたことに驚きました。

「いいね!」 6

既存のスクリプトでエラーが発生していましたか?それにより、古いvBulletin 4の投稿からすべての日付と時刻の情報が失われました。これが修正であれば、すべての投稿がコピーされた場合、再インポートが良い考えかどうか知りたいです。

「いいね!」 2

はい、スクリプトは整数を時間関数に渡していたため、エラーが発生します。

「いいね!」 3

いいえ。スクリプトはすでにインポートされた投稿をスキップします。

「いいね!」 3

こんにちは。

この問題を解決する方法はわかりましたか?

メイン/最下部の2つのフォーラムのparentidは-1です(これは昔v3から変換したためだと思います)。

どう進めればよいかわかりません。変換スクリプトで-1の場合は0に設定すればよいでしょうか? 0はメインのDiscourseカテゴリだと仮定していますか?

実際、Discourseサイトを見てみると、インポートされたのはこの2つだけのようです。

 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

おそらく。その後、vBulletin のインポートをいくつか行いました。 :person_shrugging:

試してみて、何が起こるか確認するだけです。私が説明したものと同じように見えます。

スクリプトを、そのものが nil の場合に . . . 何かをする . . . に変更するだけです。

「いいね!」 1

もちろんです。しかし、ディスコースがどのように機能するかについて十分な知識がないため、何に設定すべきかわかりません。
ディスコースでランダムな数字(例えば0)を設定した場合、どうなりますか?それとも、すでにデータベースにあるカテゴリ番号を見つけて、それに設定すべきでしょうか?

Rubyにはあまり詳しくありませんが、これは機能すると思いますか?

        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

実際、parentidが存在しない削除されたフォーラムがたくさんあるようです。

編集
すべてを1つの親トピックに設定しました。後で修正できます。

「いいね!」 1

添付ファイルのインポート部分にようやくたどり着きましたが、1.9%ほど進んだところでこのエラーが発生しました。

    67406 / 3550728 (  1.9%)  Traceback (most recent call last):
        23: from script/import_scripts/vbulletin.rb:1006:in `
        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)

どなたかこれを修正する方法をご存知ですか?

short_url_basename を読み込もうとして、nil が返されるため .hex が失敗しているのでしょうか?

「いいね!」 1

コードを見ていませんが、ファイルが存在しないか、あるいは filename フィールドがあってそれが空なのではないかと推測します。import_attachmentsputs を入れて、インポートしようとしているレコードに何が入っているか確認した方が良いでしょう。

「いいね!」 1

お手伝いありがとうございます!Rubyは初心者なのですが、これで正しいのでしょうか?

      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

ああ、short_url_basenameは関数なので、これではうまくいきません。

単に puts "#{post}" とすれば、postオブジェクトの内容すべてが出力されるのでしょうか?

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)

ということは、upload.original_filenameupload.widthupload.height、またはupload.short_urlのいずれかということになりますね。

upload_markdownでnilチェックを行えば、エラーは防げるはずですよね?

機能するためにshortURLが必要なのでしょうか?独自のランダムなshortURLを作成することはできますか?

「いいね!」 2

そこが問題だと思います。アップロードが見つからないため、nilが返されます。ファイルが欠落しているか無効である可能性があります。

「いいね!」 1

しかし、これでは検出されないのでしょうか?

unless upload
  fail_count += 1
  next
end

それとも、unless は nil をチェックしないのでしょうか?

それとも、upload オブジェクトは作成されたものの、upload オブジェクト内のプロパティ upload.short_url が欠落しているために渡されるのでしょうか?

「いいね!」 1

申し訳ありません。その通りです。それが原因でしょう。残念ながら、このレベルのデバッグはフォーラムには適していません。:person_shrugging:

しかし、あなたは正しい方向に向かっています。そのまま続けてください。あなたはそれを理解するのに十分な知識を持っているようです。私はRubyを学ぶ前に、少なくとも数個のインポーターを書きました。

「いいね!」 1