Миграция форума NodeBB с MongoDB на Discourse

Как вам известно, NodeBB поддерживает две СУБД: Redis и MongoDB. Скрипт импорта Discourse поддерживает обе из них. В этом руководстве мы разберём, как мигрировать NodeBB с MongoDB в качестве базы данных. Мы будем использовать импортер NodeBB с адаптером mongo. Если ваш форум NodeBB использует Redis в качестве базы данных, пожалуйста, следуйте этому руководству, которое демонстрирует работу адаптера redis.

План действий

  • Подготовка среды разработки.
  • Экспорт базы данных из рабочей среды.
  • Импорт рабочей БД в экземпляр Discourse.
  • Запуск скрипта импорта.

Что можно мигрировать

  • Группы
  • Вложения
  • Категории
    • Корневая категория => Корневая категория
    • Подкатегория и под-подкатегория => Подкатегория
  • Темы и сообщения
    • Закреплённая тема => Закреплённая тема
    • Закрытая тема => Закрытая тема
    • Просмотры тем
    • Пользователи, поставившие лайк (upvoted_by)
    • Стили, упоминания, эмодзи и вложения.
  • Пользователи (со следующими атрибутами)
    • Фоновое изображение профиля
    • Аватары
    • Статус блокировки
    • Имя пользователя
    • Имя
    • Электронная почта
    • Администратор
    • Биография
    • Группа
    • Веб-сайт
    • Местоположение
    • Дата регистрации

Подготовка локальной среды разработки

Как указано в нашем плане, сначала необходимо подготовить среду разработки. Следуйте одному из этих руководств для установки самого Discourse:

:bulb: Пожалуйста, обратитесь к этому руководству, если у вас возникнут проблемы с настройкой сервера Discourse.

Затем установите сервер базы данных MongoDB.

Ubuntu-18-04:

$ wget -qO - https://www.mongodb.org/static/pgp/server-4.0.asc | sudo apt-key add -
$ echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.0.list
$ sudo apt-get update
$ sudo apt-get install -y mongodb-org
$ sudo service mongod status

Для получения более подробной информации обратитесь к официальному руководству

Mac OS:

$ brew tap mongodb/brew
$ brew install mongodb-community@4.0
$ brew services start mongodb-community@4.0
$ brew services status mongodb-community@4.0

Для получения более подробной информации обратитесь к официальному руководству

Windows 10:

Загрузите установщик и установите MongoDB как службу Windows:

https://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2008plus-ssl-4.0.12-signed.msi

Запустите cmd с правами администратора, чтобы проверить, работает ли сервер MongoDB корректно:

"C:\Program Files\MongoDB\Server\4.0\bin\mongo.exe"

Для получения более подробной информации обратитесь к официальному руководству.

Эта среда будет нашим сервером Discourse.

Экспорт дампа рабочей базы данных:

Остановите ваш форум NodeBB (рабочий сервер).

$ cd /path_to_nodebb
$ ./nodebb stop

Остановите вашу базу данных:

$ sudo service mongodb stop

Сделайте резервную копию вашей БД:

$ mongodump --out ~/my_backup_path/

Вывод предыдущей команды будет примерно следующим:

2019-08-16T15:17:09.845+0300 done dumping admin.system.users (1 document)
2019-08-16T15:17:09.846+0300 done dumping admin.system.version (2 documents)
2019-08-16T15:17:09.849+0300 done dumping nodebb.sessions (10 documents)
2019-08-16T15:17:09.850+0300 done dumping nodebb.socket.io (215 documents)
2019-08-16T15:17:09.854+0300 done dumping nodebb.objects (1488 documents)

Обратите внимание, что резервная копия — это на самом деле каталог, а не просто файл.

:bulb: Вы можете проверить размер вашей БД, выполнив команду use myDatabase, а затем db.stats().dataSize; в CLI mongo. Возвращаемое значение будет в байтах.

Сделайте резервную копию активов вашего форума:

$ cd /path_to_nodebb_root_directory/
$ tar -czf ./uploads.tar.gz ./public/uploads

После того как у вас есть резервная копия БД и активов форума, скопируйте их на сервер Discourse.

Импорт базы данных

Теперь, когда у нас есть база данных, мы можем импортировать её в наш локальный экземпляр MongoDB:

$ mongorestore ~/path_of_my_backup_directory/

Для получения дополнительных опций обратитесь к официальному руководству.

Далее вам нужно распаковать файл uploads.tar.gz, чтобы импортер мог загрузить активы:

$ tar xvzf uploads.tar.gz

Запуск скрипта импорта

Теперь, когда база данных и активы на месте, мы готовы запустить наш скрипт импорта. Перед этим нам нужно отредактировать скрипт импорта NodeBB, чтобы он соответствовал нашим потребностям.

Вот путь к папке загрузок вашего NodeBB:

ATTACHMENT_DIR = '/absolute_path/uploads'

Далее нам нужно указать импортеру использовать адаптер mongo вместо адаптера redis:

adapter = NodeBB::Mongo
@client = adapter.new('mongodb://127.0.0.1:27017/nodebb')

# adapter = NodeBB::Redis
# @client = adapter.new(
# host: "localhost",
# port: "6379",
# db: 14
# )

Запустите импортер с чистым Discourse и поддержкой gem mongo:

$ cd ~/discourse
$ echo "gem 'mongo'" >> Gemfile
$ bundle install
$ bundle exec rake db:drop db:create db:migrate
$ bundle exec ruby script/import_scripts/nodebb/nodebb.rb

Импортер подключится к экземпляру MongoDB и перенесёт всё в Discourse.

После завершения работы импортера запустите Discourse:

$ bundle exec rails server

Запустите Sidekiq для обработки перенесённых данных:

$ bundle exec sidekiq

Вы можете отслеживать прогресс по адресу http://localhost:3000/sidekiq/queues.

Сделайте резервную копию Discourse и загрузите её на ваш рабочий сервер Discourse, следуя этому руководству.

:tada:

Если у вас возникнут вопросы по процессу, я с радостью помогу.

Удачной миграции :grinning:

10 лайков

Отличная работа! Пока я пробовал, у меня возникает следующая ошибка. Есть какая-то идея, что я могу делать не так?

importing groups
   10 / 10 (100.0%)  [474765 items/min]    
importing top level categories...
    8 / 8 (100.0%)  [437896 items/min]    
importing child categories...
   68 / 68 (100.0%)  [774048 items/min]  
importing users
 5534 / 5534 (100.0%)  [355520 items/min]    
adding users to groups...

importing topics...
Traceback (most recent call last):
	12: from script/import_scripts/nodebb/nodebb.rb:532:in `<main>'
	11: from /home/fsuzer/discourse/script/import_scripts/base.rb:47:in `perform'
	10: from script/import_scripts/nodebb/nodebb.rb:52:in `execute'
	 9: from script/import_scripts/nodebb/nodebb.rb:336:in `import_topics'
	 8: from /home/fsuzer/discourse/script/import_scripts/base.rb:877:in `batches'
	 7: from /home/fsuzer/discourse/script/import_scripts/base.rb:877:in `loop'
	 6: from /home/fsuzer/discourse/script/import_scripts/base.rb:878:in `block in batches'
	 5: from script/import_scripts/nodebb/nodebb.rb:337:in `block in import_topics'
	 4: from /home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:71:in `topics'
	 3: from /home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:71:in `map'
	 2: from /home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:71:in `block in topics'
	 1: from /home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:79:in `topic'
/home/fsuzer/discourse/script/import_scripts/nodebb/mongo.rb:100:in `post': undefined method `[]' for nil:NilClass (NoMethodError)

Некоторое поле темы пустое. Или, возможно, запрос по теме не вернул ничего.

1 лайк

Кто-нибудь может помочь?

Ошибка: Не удалось найти gem ‘mongo’ ни в одном из источников gem, указанных в вашем Gemfile.

После выполнения $ bundle install

Это произойдет, если вы пропустили следующее:

Убедитесь, что в вашем файле Gemfile есть эта строка:

gem 'mongo'

Затем выполните:

$ bundle install
1 лайк

Спасибо за помощь. Но эта строка уже есть в моём файле. Я также выполнил предыдущие указанные шаги.

Не знаю, поможет ли это, но при запуске gem list mongo он не отображается в списке.

Мне ваш Gemfile кажется странным. Пожалуйста, можете поделиться его полным содержимым?

Ваш Gemfile составлен неверно. В нём должно быть другое содержимое, например такое:

Вдобавок к этому:

gem 'mongo'

Привет,

Спасибо за руководство! Когда я запускаю скрипт миграции, вот что я получаю, когда скрипт доходит до точки импорта пользователей:

Traceback (most recent call last):
        21: from script/import_scripts/nodebb/nodebb.rb:532:in `<main>'
        20: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:47:in `perform'
        19: from script/import_scripts/nodebb/nodebb.rb:50:in `execute'
        18: from script/import_scripts/nodebb/nodebb.rb:130:in `import_users'
        17: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:262:in `create_users'
        16: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:262:in `each'
        15: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:274:in `block in create_users'
        14: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:391:in `create_user'
        13: from /home/odyslam/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.3/lib/active_support/core_ext/object/try.rb:15:in `try'
        12: from /home/odyslam/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.3/lib/active_support/core_ext/object/try.rb:15:in `public_send'
        11: from script/import_scripts/nodebb/nodebb.rb:164:in `block (2 levels) in import_users'
        10: from script/import_scripts/nodebb/nodebb.rb:211:in `import_profile_picture'
         9: from /home/odyslam/forum/discourse/lib/upload_creator.rb:45:in `create_for'
         8: from /home/odyslam/forum/discourse/lib/distributed_mutex.rb:14:in `synchronize'
         7: from /home/odyslam/forum/discourse/lib/distributed_mutex.rb:29:in `synchronize'
         6: from /home/odyslam/forum/discourse/lib/distributed_mutex.rb:29:in `synchronize'
         5: from /home/odyslam/forum/discourse/lib/distributed_mutex.rb:33:in `block in synchronize'
         4: from /home/odyslam/forum/discourse/lib/upload_creator.rb:66:in `block in create_for'
         3: from /home/odyslam/forum/discourse/lib/upload_creator.rb:381:in `optimize!'
         2: from /home/odyslam/forum/discourse/app/models/optimized_image.rb:178:in `ensure_safe_paths!'
         1: from /home/odyslam/forum/discourse/app/models/optimized_image.rb:178:in `each'
/home/odyslam/forum/discourse/app/models/optimized_image.rb:179:in `block in ensure_safe_paths!': Discourse::InvalidAccess (Discourse::InvalidAccess)

Мне удалось обойти это, закомментировав часть кода, отвечающую за загрузку аватарок — это не проблема.

Теперь при импорте тем возникает ошибка nil. :frowning:

Traceback (most recent call last):
        12: from script/import_scripts/nodebb/nodebb.rb:532:in `<main>'
        11: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:47:in `perform'
        10: from script/import_scripts/nodebb/nodebb.rb:52:in `execute'
         9: from script/import_scripts/nodebb/nodebb.rb:336:in `import_topics'
         8: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:862:in `batches'
         7: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:862:in `loop'
         6: from /home/odyslam/forum/discourse/script/import_scripts/base.rb:863:in `block in batches'
         5: from script/import_scripts/nodebb/nodebb.rb:337:in `block in import_topics'
         4: from /home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:71:in `topics'
         3: from /home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:71:in `map'
         2: from /home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:71:in `block in topics'
         1: from /home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:79:in `topic'
/home/odyslam/forum/discourse/script/import_scripts/nodebb/mongo.rb:100:in `post': undefined method `[]' for nil:NilClass (NoMethodError)

На случай, если кому-то это будет интересно в будущем: мне удалось выполнить миграцию. Вот несколько комментариев по моему опыту; в процессе есть пара подводных камней.

https://odyslam.com/blog/Migrating-from-Nodebb-to-Discourse/

2 лайка
21: from script/import_scripts/nodebb/nodebb.rb:532:in `<main>'
        20: from /home/nodebb/discourse/script/import_scripts/base.rb:47:in `perform'
        19: from script/import_scripts/nodebb/nodebb.rb:50:in `execute'
        18: from script/import_scripts/nodebb/nodebb.rb:130:in `import_users'
        17: from /home/nodebb/discourse/script/import_scripts/base.rb:264:in `create_users'
        16: from /home/nodebb/discourse/script/import_scripts/base.rb:264:in `each'
        15: from /home/nodebb/discourse/script/import_scripts/base.rb:276:in `block in create_users'
        14: from /home/nodebb/discourse/script/import_scripts/base.rb:393:in `create_user'
        13: from /home/nodebb/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.1.4.1/lib/active_support/core_ext/object/try.rb:15:in `try'
        12: from /home/nodebb/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.1.4.1/lib/active_support/core_ext/object/try.rb:15:in `public_send'
        11: from script/import_scripts/nodebb/nodebb.rb:164:in `block (2 levels) in import_users'
        10: from script/import_scripts/nodebb/nodebb.rb:211:in `import_profile_picture'
         9: from /home/nodebb/discourse/lib/upload_creator.rb:64:in `create_for'
         8: from /home/nodebb/discourse/lib/distributed_mutex.rb:14:in `synchronize'
         7: from /home/nodebb/discourse/lib/distributed_mutex.rb:29:in `synchronize'
         6: from /home/nodebb/discourse/lib/distributed_mutex.rb:29:in `synchronize'
         5: from /home/nodebb/discourse/lib/distributed_mutex.rb:33:in `block in synchronize'
         4: from /home/nodebb/discourse/lib/upload_creator.rb:83:in `block in create_for'
         3: from /home/nodebb/discourse/lib/upload_creator.rb:501:in `optimize!'
         2: from /home/nodebb/discourse/app/models/optimized_image.rb:181:in `ensure_safe_paths!'
         1: from /home/nodebb/discourse/app/models/optimized_image.rb:181:in `each'
/home/nodebb/discourse/app/models/optimized_image.rb:182:in `block in ensure_safe_paths!': Discourse::InvalidAccess (Discourse::InvalidAccess)

@enigmaty Я сталкиваюсь с той же ошибкой, что и @OdysLam @FreeWorLD.

Привет!
Кто-нибудь уже пробовал мигрировать с последней версии NodeBB 4.9.x на текущую версию Discourse dev?

/home/dev/discourse/script/import_scripts/nodebb/mongo.rb:100:in 'NodeBB::Mongo#post': undefined method '[]' for nil (NoMethodError)

      post["timestamp"] = timestamp_to_date(post["timestamp"])
                                                ^^^^^^^^^^^^^
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'block in NodeBB::Mongo#posts'
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'Array#map'
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'NodeBB::Mongo#posts'
        from script/import_scripts/nodebb/nodebb.rb:413:in 'block in ImportScripts::NodeBB#import_posts'
        from /home/dev/discourse/script/import_scripts/base.rb:943:in 'block in ImportScripts::Base#batches'
        from <internal:kernel>:168:in 'Kernel#loop'
        from /home/dev/discourse/script/import_scripts/base.rb:942:in 'ImportScripts::Base#batches'
        from script/import_scripts/nodebb/nodebb.rb:412:in 'ImportScripts::NodeBB#import_posts'
        from script/import_scripts/nodebb/nodebb.rb:52:in 'ImportScripts::NodeBB#execute'
        from /home/dev/discourse/script/import_scripts/base.rb:47:in 'ImportScripts::Base#perform'
        from script/import_scripts/nodebb/nodebb.rb:568:in '<main>'

Я получаю эту ошибку при миграции в среде разработки.
Есть какие-то идеи?

Поддерживаются ли скрипты миграции Сообществом в 2026 году?

Поле timestamp существует и содержит корректные данные.

Спасибо.

Проблема в том, что post равен nil. Значит, вы что-то настроили неправильно. Импортировались ли пользователи?

Мы можем попробовать, но возможности довольно ограничены. Скорее всего, вам придётся разбираться в работе множества компонентов, что выходит за рамки того, что легко поддерживается на форуме.

3 лайка
импортирование пользователей
      182 / 182 (100.0%)  [437953 элементов/мин]
добавление пользователей в группы...

импортирование тем...
      132 / 132 (100.0%)  [3567036 элементов/мин]
импортирование постов...
/home/dev/discourse/script/import_scripts/nodebb/mongo.rb:100:in 'NodeBB::Mongo#post': метод '[]' не определен для nil (NoMethodError)

      post["timestamp"] = timestamp_to_date(post["timestamp"])
                                                ^^^^^^^^^^^^^
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'block in NodeBB::Mongo#posts'
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'Array#map'
        from /home/dev/discourse/script/import_scripts/nodebb/mongo.rb:95:in 'NodeBB::Mongo#posts'
        from script/import_scripts/nodebb/nodebb.rb:413:in 'block in ImportScripts::NodeBB#import_posts'
        from /home/dev/discourse/script/import_scripts/base.rb:943:in 'block in ImportScripts::Base#batches'
        from <internal:kernel>:168:in 'Kernel#loop'
        from /home/dev/discourse/script/import_scripts/base.rb:942:in 'ImportScripts::Base#batches'
        from script/import_scripts/nodebb/nodebb.rb:412:in 'ImportScripts::NodeBB#import_posts'
        from script/import_scripts/nodebb/nodebb.rb:52:in 'ImportScripts::NodeBB#execute'
        from /home/dev/discourse/script/import_scripts/base.rb:47:in 'ImportScripts::Base#perform'
        from script/import_scripts/nodebb/nodebb.rb:568:in '<main>'


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

Извините, вы живой человек или просто достаточно умный ИИ-помощник?

Тогда проблема в том, что не читаются данные постов.

Полагаю, дело здесь, и по какой-то причине посты не находятся.

    def posts(offset = 0, page_size = 2000)
      post_keys = mongo.find(_key: "posts:pid").skip(offset).limit(page_size).pluck(:value)

      post_keys.map { |post_key| post(post_key) }
    end

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

Если вы хотите получить помощь от ИИ-помощника, обратитесь по адресу: https://ask.discourse.com/

6 лайков

Как-то мне удалось заставить функцию скрипта работать правильно.

Вот мои изменения (с небольшой помощью от Claude Code :slight_smile:)

 >>mongo.rb
   
   def posts(offset = 0, page_size = 1000)
      post_keys = mongo.find(_key: "posts:pid").skip(offset).limit(page_size).pluck(:value)
      post_keys
          .map { |pid| post(pid) }
          .compact  # <-- убирает любые nil-результаты (сиротские pid)
      post_keys.map { |post_key| post(post_key) }
    end

    def post(id)
    post = mongo.find(_key: "post:#{id}").first
    return nil if post.nil? # <-- проверка на null
    post["timestamp"] = timestamp_to_date(post["timestamp"])
    if post["upvoted_by"] = mongo.find(_key: "pid:#{id}:upvote").first
        post["upvoted_by"] = mongo.find(_key: "pid:#{id}:upvote").first[:members]
      else
        post["upvoted_by"] = []
      end

      post["pid"] = post["pid"].to_s
      post["deleted"] = post["deleted"].to_s

      post
    end
	
>>nodebb.rb

 create_posts(posts, total: post_count, offset: offset) do |post|
        # пропускаем, если пост пустой
		# пропускаем, если это merged_post
        next if post.nil?
        next if @merged_posts_map[post["pid"]]

        # пропускаем, если пост удалён
        next if post["deleted"] == "1"

        raw = post["content"]
        post_id = "p#{post["pid"]}"

        next if raw.blank?
        topic = topic_lookup_from_imported_post_id("t#{post["tid"]}")

        unless topic
          puts "Тема с id #{post["tid"]} не найдена, пропускаем"
          next
        end	

Похоже, теперь всё работает как надо.

Хотя я не знаю, насколько это правильно с точки зрения внутренней архитектуры Discourse, на первый взгляд всё работает.

Любые предложения по улучшению и оптимизации будут очень кстати.

1 лайк

Совет: не позволяйте ошибке проходить полностью незаметно — измените это на:

if post.nil?
  puts "!!! Не удалось найти пост #{id}"
  return nil
end

Иначе вы можете задаться вопросом, почему исчезла половина ваших постов, и после часов разочарования выяснится, что причина именно в этом.

То же самое относится к этим двум строкам:

        next, если post.nil?
        next, если post["deleted"] == "1"
5 лайков

Переписан метод posts в файле mongo.rb

def posts(offset = 0, page_size = 1000)
post_keys = mongo.find(_key: "posts:pid").sort(score: 1).skip(offset).limit(page_size).pluck(:value)
post_keys.map { |pid| post(pid)}.compact
end

Этот подход гарантирует, что сообщения в теме отсортированы в правильном хронологическом порядке. По возрастанию.
Обратите внимание на вызов sort(score: 1).