vBulletin 4フォーラムをDiscourseに移行する

私は最近 Discourse への転向者なので、試行錯誤の末、上記の内容をコマンドごとの完全なリストにまとめました(@titusca@enigmaty さん、ありがとうございます)。

これが皆さんの助けになれば、あるいは少なくとも newcomers が最初から最後まで進むスピードが上がることを願っています。mysql から MariaDB への更新によりプロセスに混乱が生じていると思われるため、これを最初の投稿に組み込みたいです。

背景:

  • 160 万件の投稿の移行
  • Digital Ocean Droplet(CPU 最適化 4 vCPU/8GB)を利用

#1 - Digital Ocean Discourse の 1 クリック Droplet をインストール

#2 - SSH を通じてプロンプトに従って Discourse のインストールを完了
\u003e SSH コンソールを開く
\u003e root
\u003e (あなたの root パスワード)
\u003e (Enter)
\u003e (あなたのドメイン).com
\u003e (etc…)

#3 - SFTP にログインしてデータベースダンプをアップロード
\u003e sftp root@XXX.XXX.XX.XX
\u003e y
\u003e yes
\u003e (あなたの root パスワード)
\u003e put db.sql /var/discourse/shared/standalone/db.sql

#4 - 新しい Discourse ウェブサイトにログインして管理者アカウントを設定

#5 - SSH にログインしてプロセスを開始
\u003e ssh root@XXX.XXX.XX.XX
\u003e cd /var/discourse
\u003e ./launcher start app
\u003e docker exec -it app bash
\u003e sudo apt-get update
\u003e sudo apt-get upgrade
\u003e y

#6 - MariaDB のインストール(mysql の代替)
\u003e apt-get update && apt-get install mariadb-server-10.3 libmariadbd-dev
\u003e y

#7 - MySQL データベースの設定
\u003e service mysql start
\u003e mysql -u root -p
\u003e パスワード
\u003e create database vbulletin;
\u003e exit;

#8 - Vbulletin から MySQL データベースへの転送
\u003e mysql -u root -p vbulletin < /shared/db.sql
\u003e パスワード

#9 - GEM ファイル
\u003e echo “gem ‘mysql2’” >> Gemfile
\u003e echo “gem ‘mysql2’, require: false” >> /var/www/discourse/Gemfile
\u003e echo “gem ‘php_serialize’, require: false” >> /var/www/discourse/Gemfile
\u003e cd /var/www/discourse
\u003e su discourse -c ‘bundle install --no-deployment --without test --without development --path vendor/bundle’
(赤色の結果は無視してください)

#10 - インストールスクリプトの設定
\u003e vi /var/www/discourse/script/import_scripts/vbulletin.rb

#10.a - 必要に応じてテキストファイルを編集
\u003e DB_HOST ||= ENV[‘DB_HOST’] || “localhost”
\u003e DB_NAME ||= ENV[‘DB_NAME’] || “vbulletin”
\u003e DB_PW ||= ENV[‘DB_PW’] || “password”
\u003e DB_USER ||= ENV[‘DB_USER’] || “root”
\u003e TIMEZONE ||= ENV[‘TIMEZONE’] || “America/Los_Angeles”
\u003e TABLE_PREFIX ||= ENV[‘TABLE_PREFIX’] || “”
\u003e ATTACHMENT_DIR ||= ENV[‘ATTACHMENT_DIR’] || ‘/shared/attachments/’

#10.c - 編集の終了
\u003e \u003cesc\u003e
\u003e :wq

#11 - Bundle 設定
\u003e bundle config set path ‘vendor/bundle’
\u003e bundle config set without ‘development:test’
\u003e bundle config unset deployment
\u003e su discourse -c ‘bundle install’

#12 - MySQL 設定(前の手順で可能かもしれません)
\u003e mysql --version
\u003e sudo mysql -u root -p
\u003e パスワード
\u003e ALTER USER ‘root’@‘localhost’ IDENTIFIED BY ‘password’;
\u003e FLUSH PRIVILEGES;
\u003e exit

#13 - インストールスクリプトの実行

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

頑張ってください!

「いいね!」 8

vB4 からの移行後にフィードバックを残したかっただけです:

  • [s] ソフト削除された投稿が正しく非表示になっていなかった問題:https://github.com/discourse/discourse/pull/12057[/s]
  • [ul] + [li] とネストされた [LIST] が正しく移行されておらず、BBcode プラグインもこれを処理していないようです → これは想定されているようです:https://meta.discourse.org/t/commonmark-testing-started-here/65121(引用:コアは失敗の元となるため、BBCode に対して [ul] [ol] [li] のサポートを実装しません)→ これについては、後で Regex による修正処理を構築する必要があります。
  • 通常のインポーターを使って初期移行を行いました(3 日以上かかりました)。その後、いくつかの新しい DB スナップショットで移行を再開し、インポートを「最新状態」に保ち、ダウンタイムを実質的に 30 分に抑えました。この手順は非常にうまくいきましたが、初期にスレッドや投稿をインポートした後に編集されたすべてのデータは例外でした。これらの情報は現在、手動で修正する必要があります。
  • Discourse のプラグイン作成は、ドキュメントの不足とフォルダ構造の全体像の欠如により、非常に困難です。ただし、仕組みを理解すれば、より良くなり、使いやすくなっていきます。

残っている質問:

  • インポーターが既にインポートされた投稿をどのようにマッピングし、古い vB4 の post_id を新しい Discourse の post_id と照合して「ソフト削除」された投稿を非表示にするのかよくわかりません。ヒントをいただければ大変助かります! 解決しました:post_custom_fields テーブル内の import_id です。素晴らしい。これでこれを修正するための便利なスクリプトを書く必要があります :slight_smile: → 編集:より良い方法は、インポータースクリプトを使用することです。これにより、インポートされたすべての ID が簡単に使用できるようにマッピングされます。
「いいね!」 2

残念ながら、以前の投稿を編集することができません :slight_smile:

別の問題を見つけました:投稿にリンクされていない添付ファイルは、Discourse から利用できません。

この問題を修正するための私のドラフト PR:FIX: vBulletin importer should import unreferenced attachments by paresy · Pull Request #12187 · discourse/discourse · GitHub

ありがとうございます!

「いいね!」 3

私の問題リストに関する簡単なフォローアップです。表示の問題は修正しました。

古い vBulletin データベースから影響を受けたすべての投稿をダンプしてください:

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

すべての postid を行ごとに記載した imported_post_ids.txt ファイルを作成してください。

修正スクリプト用の新しいファイルを作成します:

nano script/import_scripts/fix_visibility.rb 

内容:

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

スクリプトを実行します:

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

このスクリプトは、インポーターのロジックを使用して、インポートされた post_id を、非表示にしたい実際の Discourse の post_id にマッピングします。

「いいね!」 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