نقل منتدى vBulletin 4 إلى Discourse

أنا مجرد محوّل حديث إلى نظام Discourse، لذا بعد الكثير من التجربة والخطأ، جمّعت كل ما سبق في قائمة كاملة خطوة بخطوة (شكرًا لك @titusca و@enigmaty).

أتمنى أن يساعد هذا (أو على الأقل يسرّع) المبتدئين الآخرين في الانتقال من البداية إلى النهاية. أود دمج هذا في المنشور الأول نظرًا للتحديثات التي طرأت على التحويل من MySQL إلى MariaDB، والتي أعتقد أنها أضافت الكثير من الارتباك إلى العملية.

الخلفية:

  • نقل 1.6 مليون منشور.
  • استُخدمت Droplet من Digital Ocean (محسّنة للـ CPU: 4 vCPU / 8GB)

#1 - تثبيت Droplet Discourse من Digital Ocean بنقرة واحدة

#2 - إكمال تثبيت Discourse عبر SSH باتباع التعليمات

افتح وحدة تحكم SSH
root
(كلمة مرور الجذر الخاصة بك)
(إدخال)
(نطاقك).com
(إلخ…)

#3 - تسجيل الدخول إلى SFTP لرفع نسخة قاعدة البيانات

sftp root@XXX.XXX.XX.XX
y
yes
(كلمة مرور الجذر الخاصة بك)
put db.sql /var/discourse/shared/standalone/db.sql

#4 - تسجيل الدخول إلى موقع Discourse الجديد لإعداد حساب المسؤول

#5 - تسجيل الدخول إلى SSH - ابدأ العملية

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 - تثبيت MariaDB (بديل لـ MySQL)

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

#7 - إعداد قاعدة بيانات MySQL

service mysql start
mysql -u root -p
كلمة المرور
create database vbulletin;
exit;

#8 - نقل قاعدة بيانات Vbulletin إلى MySQL

mysql -u root -p vbulletin < /shared/db.sql
كلمة المرور

#9 - ملف 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’
(تجاهل النتيجة باللون الأحمر)

#10 - تكوين ملف التثبيت

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

#10.a - قم بتعديلات على ملف النص حسب الحاجة

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 - إنهاء التعديلات

:wq

#11 - تكوين Bundle

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

#12 - تكوين MySQL (قد يكون من الممكن القيام بذلك مع الخطوة السابقة)

mysql --version
sudo mysql -u root -p
كلمة المرور
ALTER USER 'root'@'localhost' IDENTIFIED BY 'password';
FLUSH PRIVILEGES;
exit

#13 - تشغيل سكريبت التثبيت

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 تتعامل مع هذا أيضًا → يبدو أن هذا متوقع: CommonMark testing started here! (اقتباس: لن يقوم النواة بتنفيذ دعم الوسوم [ul] [ol] و [li] لـ BBCode لأن ذلك وصفة للفشل.) → سأحتاج إلى بناء بعض سحر التعابير النمطية (RegEx) لإصلاح ذلك لاحقًا.
  • قمنا بهجرة أولية باستخدام أداة الاستيراد العادية (استغرقت أكثر من 3 أيام)، وأعدنا تشغيل الهجرة مع لقطات قاعدة بيانات أحدث عدة مرات للحفاظ على “حداثة” الاستيراد وتقليل وقت التوقف الفعلي إلى 30 دقيقة. سار هذا الإجراء بشكل جيد جدًا، باستثناء كل ما تم تعديله بعد استيراد المواضيع والمنشورات في البداية. نحن بحاجة الآن إلى إعادة معالجة هذه المعلومات يدويًا.
  • إنشاء إضافات لـ Discourse أمر صعب جدًا بسبب نقص التوثيق وعدم وضوح الصورة الشاملة لكيفية عمل هيكل المجلدات. ومع ذلك، يصبح الأمر أفضل وأكثر سلاسة بعد فهم كيفية عمله.

الأسئلة المتبقية لدي:

  • لم أكن متأكدًا من كيفية تعيين أداة الاستيراد للمنشورات التي تم استيرادها بالفعل، وكيفية مطابقة معرف المنشور القديم في vB4 مع معرف المنشور الجديد في Discourse لإخفاء تلك المنشورات “المحذوفة بشكل ناعم”. إذا كان بإمكان أحد تقديم تلميح، فسيكون ذلك موضع ترحيب كبير! وجدتها: حقل import_id داخل جدول post_custom_fields. رائع. الآن أحتاج إلى كتابة سكريبت عملي لإصلاح هذا :slight_smile: → تعديل: طريقة أفضل هي استخدام سكريبت الاستيراد، الذي يقوم بربط جميع المعرفات المستوردة للاستخدام السهل.
إعجابَين (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

أنشئ ملفًا باسم imported_post_ids.txt يحتوي على جميع معرفات المنشورات (postid) سطرًا تلو الآخر.

أنشئ ملفًا جديدًا لبرنامج الإصلاح:

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'

سيستخدم البرنامج المنطق المستمد من أداة الاستيراد لربط معرفات المنشورات المستوردة بمعرفات المنشورات المقروءة في Discourse التي نرغب في إخفائها.

4 إعجابات

مرحباً يا رفاق،

لدي النص البرمجي يعمل على ترحيل vb3. أقوم بذلك خطوة بخطوة وهو حاليًا يعمل على معالجة 122 ألف مستخدم بمعدل 330/دقيقة. ثم سيكون لدينا 2.5 مليون مشاركة لمعالجتها.

نقوم بذلك على خادم إنتاجي. لا أحد يستخدم موقع discourse، لقد قمنا بإعداده للتو وهو موجود على عنوان URL مجهول. إذا قمت بتسجيل الدخول، يمكنني رؤية إشعارات المستخدمين الجدد تتزايد. ربما يكون هذا سؤالًا غبيًا، ولكني أتساءل عما إذا كانت عملية الترحيل ستتم بشكل أسرع إذا قمنا بتعليق أو تعطيل الموقع المباشر بطريقة ما؟

إعجاب واحد (1)

هذا يعتمد على الحمل وعدد وحدات المعالجة المركزية (CPU) على خادم الإنتاج الخاص بك. يمكنك دائمًا محاولة إيقاف خادم الويب لمدة 5 دقائق ومعرفة ما إذا كان الاستيراد يتم بشكل أسرع.

3 إعجابات

الاستيراد يستغرق وقتًا طويلاً حقًا. على حد علمي، يجب أن يكون المستورد بالجملة أسرع. لقد أجرينا استيرادًا أوليًا من نسخة احتياطية على جهاز التطوير القوي الخاص بنا ثم أجرينا استيرادًا تزايديًا من نسخة احتياطية أخرى للتحول إلى Discourse مع توقف لمدة نصف ساعة فقط. احذر من الأشياء التي يمكن أن تحدث بشكل خاطئ عند إجراء التحديثات التزايدية :slight_smile: (انظر هنا: Migrate a vBulletin 4 forum to Discourse - #132 by paresy)

paresy

3 إعجابات

أرى نواة أساسية واحدة مشغولة أعتقد أنها الخادم الذي يستوعب البيانات المحدثة، ونواة أخرى مشغولة عند تشغيل البرنامج النصي للاستيراد. ليس لدي حقًا المعرفة بالمجال لأعرف ما إذا كانت المنافسة بين هاتين العمليتين على مورد قاعدة البيانات يمكن أن تبطئ المستورد، ولا أملك المعرفة بالمجال لأعرف ما إذا كان من الممكن إيقاف الاستيعاب مع ترك الحاوية قيد التشغيل. يجب أن يحدث الاستيعاب على أي حال، لذلك أفترض أن أكثر الأشياء أمانًا هو مجرد السماح له بالاستمرار في العمل.

نصيحة للقراء المستقبليين، أرى أن 27 ألفًا (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. للمقارنة، قمت بتثبيت خادم تطوير Discourse محليًا / أصليًا على جهاز MacBook Air M1 وفوجئت بأن عملية الاستيراد كانت أبطأ بكثير.

6 إعجابات

هل كنت تواجه أخطاء مع النص البرمجي الموجود مسبقًا؟ لقد فقدت معلومات التاريخ والوقت من جميع مشاركاتنا القديمة في vBulletin 4 بسبب ذلك. إذا كان هذا إصلاحًا، فسأكون سعيدًا بمعرفة ما إذا كان إعادة الاستيراد فكرة جيدة إذا تم نسخ جميع المشاركات.

إعجابَين (2)

نعم، سيحدث خطأ في البرنامج النصي لأنه كان يغذي عددًا صحيحًا إلى دالة زمنية.

3 إعجابات

لا. يتخطى البرنامج النصي المشاركات التي تم استيرادها بالفعل.

3 إعجابات

مرحباً،

هل اكتشفت كيفية إصلاح هذا؟

منتديانا الرئيسيان/السفليان لهما parentid = -1 (أعتقد أن هذا بسبب تحويلنا من v3 في الماضي).

لست متأكدًا من كيفية المتابعة، هل أقوم فقط بتعيينها إلى 0 إذا كانت -1 في البرنامج النصي للتحويل؟ بافتراض أن 0 هي فئة الخطاب الرئيسية؟

في الواقع، بالنظر إلى موقع discourse الآن؛ يبدو أن هذين هما الوحيدان اللذان تم استيرادهما؟

 استيراد الفئات العلوية...
         2 / 2 (100.0%)  [211 عنصر/دقيقة]  في]
 استيراد الفئات الفرعية...
 تتبع الخطأ:
         5: من script/import_scripts/vbulletin.rb:1003:in `<main>'
         4: من /var/www/discourse/script/import_scripts/base.rb:47:in `perform'
         3: من script/import_scripts/vbulletin.rb:84:in `execute'
         2: من script/import_scripts/vbulletin.rb:287:in `import_categories'
         1: من 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:

سيتعين عليك فقط تجربته ومعرفة ما سيحدث. يبدو الأمر كما وصفته.

سأقوم فقط بتعديل البرنامج النصي لـ . . . فعل شيء ما . . . إذا كان هذا الشيء فارغًا.

إعجاب واحد (1)

بالتأكيد، لكنني لا أعرف ما يكفي عن كيفية عمل المناقشة لأعرف ما يجب ضبطه عليه.
ماذا ستفعل المناقشة إذا قمت بتعيينها على رقم عشوائي مثل 0؟ أم يجب أن أجد فئة موجودة بالفعل في قاعدة البيانات وأعينها عليها؟

لست قوياً في لغة روبي، هل تعتقد أن هذا سيعمل؟

        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

في الواقع، يبدو أن هناك العديد من المنتديات المحذوفة التي لم يعد معرف الأب الخاص بها موجودًا.

تعديل
لقد قمت للتو بتعيينها جميعًا على موضوع أب واحد، ويمكنني إصلاحها لاحقًا.

إعجاب واحد (1)

لقد وصلنا أخيرًا إلى جزء استيراد المرفقات، ووصل إلى حوالي 1.9% وواجهنا هذا الخطأ الآن

    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)

هل لدى أي شخص أي فكرة عن كيفية إصلاح هذا؟

هل يحاول قراءة short_url_basename، ويعيد nil؛ لذا يفشل .hex؟

إعجاب واحد (1)

تخميني، دون النظر إلى الكود، هو أن الملف مفقود أو ربما يوجد حقل اسم ملف وهو فارغ؟ ربما أضع puts في import_attachments وأرى ما هو موجود في السجل الذي يحاول استيراده.

إعجاب واحد (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_filename، أو upload.width، أو upload.height، أو upload.short_url إذن

إذن إذا قمت بإجراء فحص nil في upload_markdown، فيجب أن يمنع ذلك الخطأ، أليس كذلك؟

هل تحتاج إلى shortURL لكي تعمل؛ هل يمكنني فقط إنشاء shortURL عشوائي خاص بي؟

إعجابَين (2)

أعتقد أن هذه هي المشكلة. إنه لا يعثر على التحميل، لذا يعود بقيمة فارغة. ربما يكون الملف مفقودًا أو غير صالح.

إعجاب واحد (1)

ولكن ألن يلتقط هذا الأمر بعد ذلك؟

unless upload
  fail_count += 1
  next
end

أم أن unless لا تتحقق من nil؟

أم أنه ينجح لأنه أنشأ كائن upload، ولكن الخاصية upload.short_url في كائن upload مفقودة ربما؟

إعجاب واحد (1)

عذرًا. صحيح. هذا سيلتقطه. أخشى أن هذا هو سبب عدم ملاءمة هذا المستوى من التصحيح لمنتديات النقاش. :person_shrugging:

أنت على المسار الصحيح، رغم ذلك. فقط استمر. يبدو أنك تعرف ما يكفي لاكتشافه. لقد كتبت مستوردين اثنين على الأقل قبل أن أتعلم لغة Ruby.

إعجاب واحد (1)