Migrate a vBulletin 4 forum to Discourse

Я только недавно перешел на Discourse, поэтому после множества проб и ошибок я объединил всё вышеперечисленное в полный пошаговый список (спасибо @titusca и @enigmaty).

Надеюсь, это поможет (или хотя бы ускорит) другим новичкам пройти путь от начала до конца. Хотел бы включить это в первый пост, учитывая обновления с переходом от MySQL к MariaDB, которые, как мне кажется, внесли много путаницы в процесс.

Предпосылки:

  • Перенос 1,6 миллиона постов.
  • Использовался Droplet от Digital Ocean (оптимизированный по CPU: 4 vCPU/8 ГБ).

#1 — Установка Droplet Discourse в один клик от Digital Ocean

#2 — Завершение установки Discourse через SSH, следуя подсказкам

Откройте консоль SSH
root
(ваш пароль root)
(Enter)
(вашдомен).com
(и так далее…)

#3 — Вход в SFTP для загрузки дампа базы данных

sftp root@XXX.XXX.XX.XX
y
yes
(ваш пароль root)
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’] || “пароль”
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 'пароль';
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 — это действительно сложно из-за отсутствия документации и общего понимания того, как работает структура папок. Хотя после того, как разберёшься, становится намного проще и приятнее.

Оставшиеся вопросы:

  • Я не уверен, как импортер сопоставляет уже импортированные посты и как связать старый post_id vB4 с новым post_id в Discourse, чтобы скрыть эти “мягко удалённые” посты. Если кто-то сможет дать подсказку, буду очень признателен! Нашёл: import_id внутри таблицы post_custom_fields. Отлично. Теперь нужно написать удобный скрипт для исправления этого :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

Создайте файл 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'

Скрипт использует логику импортера для сопоставления импортированных post_id с актуальными post_id в Discourse, которые мы хотим скрыть.

4 лайка

Всем привет,

Скрипт работает над миграцией с vb3. Я делаю это поэтапно, и сейчас он обрабатывает 122 тысячи пользователей со скоростью 330 в минуту. Затем нам предстоит пройти через 2,5 миллиона сообщений.

Мы делаем это на продакшн-сервере. Никто не использует сайт Discourse — мы его только настроили, и он доступен по анонимному URL. Если я войду в систему, то вижу, как увеличиваются уведомления о новых пользователях. Возможно, глупый вопрос, но интересно, ускорилась бы миграция, если бы мы приостановили или отключили действующий сайт каким-то образом?

1 лайк

Это зависит от нагрузки и количества процессоров на вашем продакшн-сервере. Вы всегда можете попробовать остановить веб-сервер на 5 минут и посмотреть, ускорится ли импорт.

3 лайка

Импорт действительно занимает много времени. Насколько я знаю, массовый импортер должен работать быстрее. Мы выполнили первый импорт из резервной копии на мощной машине для разработки, а затем инкрементальный импорт из другой резервной копии, чтобы перейти на Discourse с временем простоя всего в полчаса. Будьте осторожны с проблемами, которые могут возникнуть при инкрементальных обновлениях :slight_smile: (См. здесь: Migrate a vBulletin 4 forum to Discourse - #132 by paresy)

paresy

3 лайка

Я вижу, что одно ядро нагружено на 100%, и, по-моему, это сервер, который обрабатывает обновленные данные, а другое ядро также нагружено при запуске скрипта импорта. У меня действительно нет достаточных знаний в этой предметной области, чтобы понять, может ли конкуренция этих двух процессов за ресурсы базы данных замедлять работу импортера. Также я не знаю, возможно ли вообще остановить процесс обработки данных, оставив контейнер запущенным. В любом случае обработка данных должна произойти, поэтому, пожалуй, самое безопасное решение — просто позволить ей продолжаться.

Один совет для будущих читателей: я заметил, что 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?

Кстати, посмотрев сейчас на сайт Discourse, вижу, что импортированы, кажется, только эти два?

 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 лайк

Конечно, но я недостаточно разбираюсь в том, как работает Discourse, чтобы знать, какое значение установить.
Что сделает Discourse, если я задам им случайное число, например 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,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}"

          # внутренняя дедупликация загрузок гарантирует, что мы не импортируем вложения повторно
          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 лайка

Думаю, проблема именно здесь. Файл не находится, поэтому возвращается nil. Возможно, файл отсутствует или некорректен.

1 лайк

Но разве это не должно его перехватывать?

unless upload
  fail_count += 1
  next
end

Или unless не проверяет значение nil?

Или код проходит, потому что объект upload был создан, но свойство upload.short_url в этом объекте, возможно, отсутствует?

1 лайк

Извините. Точно. Это должно помочь. Боюсь, что такой уровень отладки не совсем подходит для форума. :person_shrugging:

Однако вы на верном пути. Просто продолжайте. Похоже, у вас достаточно знаний, чтобы разобраться. Я написал как минимум пару импортеров, прежде чем выучил Ruby.

1 лайк