Grande migração de fórum Drupal, erros e limitações do importador

Olá, este tópico fornece algum contexto sobre a migração que estou lentamente planejando e testando. Finalmente, tentei o importador Drupal na sexta-feira passada em um ambiente de teste VPS usando uma combinação de este e este. O importador ainda está em execução enquanto digito isso, então ainda não consegui testar a funcionalidade do site de teste, mas ele está prestes a terminar em breve.

O maior problema que estou enfrentando é um “valor de chave duplicado” em 8 nós aparentemente aleatórios (o equivalente a tópicos no Discourse) de um total de ~80.000 nós. Estes são os números nid específicos, caso haja algum bug de matemática muito estranho do tipo Y2K em jogo:

42081, 53125, 57807, 63932, 66756, 76561, 78250, 82707

Este mesmo erro sempre acontece nos mesmos nids ao reexecutar o importador:

Traceback (most recent call last):
	19: from script/import_scripts/drupal.rb:537:in `<main>'
	18: from /var/www/discourse/script/import_scripts/base.rb:47:in `perform'
	17: from script/import_scripts/drupal.rb:39:in `execute'
	16: from script/import_scripts/drupal.rb:169:in `import_forum_topics'
	15: from /var/www/discourse/script/import_scripts/base.rb:916:in `batches'
	14: from /var/www/discourse/script/import_scripts/base.rb:916:in `loop'
	13: from /var/www/discourse/script/import_scripts/base.rb:917:in `block in batches'
	12: from script/import_scripts/drupal.rb:195:in `block in import_forum_topics'
	11: from /var/www/discourse/script/import_scripts/base.rb:224:in `all_records_exist?'
	10: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-7.0.3.1/lib/active_record/transactions.rb:209:in `transaction'
	 9: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-7.0.3.1/lib/active_record/connection_adapters/abstract/database_statements.rb:316:in `transaction'
	 8: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-7.0.3.1/lib/active_record/connection_adapters/abstract/transaction.rb:317:in `within_new_transaction'
	 7: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activesupport-7.0.3.1/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `synchronize'
	 6: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activesupport-7.0.3.1/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `handle_interrupt'
	 5: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activesupport-7.0.3.1/lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `block in synchronize'
	 4: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activesupport-7.0.3.1/lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `handle_interrupt'
	 3: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-7.0.3.1/lib/active_record/connection_adapters/abstract/transaction.rb:319:in `block in within_new_transaction'
	 2: from /var/www/discourse/script/import_scripts/base.rb:231:in `block in all_records_exist?'
	 1: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/rack-mini-profiler-3.0.0/lib/patches/db/pg.rb:56:in `exec'
/var/www/discourse/vendor/bundle/ruby/2.7.0/gems/rack-mini-profiler-3.0.0/lib/patches/db/pg.rb:56:in `exec': ERROR:  duplicate key value violates unique constraint "import_ids_pkey" (PG::UniqueViolation)
DETAIL:  Key (val)=(nid:42081) already exists.
	20: from script/import_scripts/drupal.rb:537:in `<main>'
	19: from /var/www/discourse/script/import_scripts/base.rb:47:in `perform'
	18: from script/import_scripts/drupal.rb:39:in `execute'
	17: from script/import_scripts/drupal.rb:169:in `import_forum_topics'
	16: from /var/www/discourse/script/import_scripts/base.rb:916:in `batches'
	15: from /var/www/discourse/script/import_scripts/base.rb:916:in `loop'
	14: from /var/www/discourse/script/import_scripts/base.rb:917:in `block in batches'
	13: from script/import_scripts/drupal.rb:195:in `block in import_forum_topics'
	12: from /var/www/discourse/script/import_scripts/base.rb:224:in `all_records_exist?'
	11: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-7.0.3.1/lib/active_record/transactions.rb:209:in `transaction'
	10: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-7.0.3.1/lib/active_record/connection_adapters/abstract/database_statements.rb:316:in `transaction'
	 9: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-7.0.3.1/lib/active_record/connection_adapters/abstract/transaction.rb:317:in `within_new_transaction'
	 8: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activesupport-7.0.3.1/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `synchronize'
	 7: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activesupport-7.0.3.1/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `handle_interrupt'
	 6: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activesupport-7.0.3.1/lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `block in synchronize'
	 5: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activesupport-7.0.3.1/lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `handle_interrupt'
	 4: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/activerecord-7.0.3.1/lib/active_record/connection_adapters/abstract/transaction.rb:319:in `block in within_new_transaction'
	 3: from /var/www/discourse/script/import_scripts/base.rb:243:in `block in all_records_exist?'
	 2: from /var/www/discourse/script/import_scripts/base.rb:243:in `ensure in block in all_records_exist?'
	 1: from /var/www/discourse/vendor/bundle/ruby/2.7.0/gems/rack-mini-profiler-3.0.0/lib/patches/db/pg.rb:56:in `exec'
/var/www/discourse/vendor/bundle/ruby/2.7.0/gems/rack-mini-profiler-3.0.0/lib/patches/db/pg.rb:56:in `exec': ERROR:  current transaction is aborted, commands ignored until end of transaction block (PG::InFailedSqlTransaction)

A única maneira que consegui fazer com que ele prosseguisse foi alterando as condições SQL:

...
	 LEFT JOIN node_counter nc ON nc.nid = n.nid
         WHERE n.type = 'forum'
           AND n.status = 1
AND n.nid != 42081
AND n.nid != 53125
AND n.nid != 57807
AND n.nid != 63932
AND n.nid != 66756
AND n.nid != 76561
AND n.nid != 78250
AND n.nid != 82707
         LIMIT #{BATCH_SIZE}
        OFFSET #{offset};
...

Inspecionei o primeiro nó com falha, bem como os nids anteriores e posteriores a ele no banco de dados Drupal de origem, e não consigo ver nada de errado. O nid é definido como a chave primária e tem AUTO_INCREMENT, e o site Drupal original funciona bem, então não pode haver nenhum problema fundamental com a integridade do banco de dados de origem.


Além do bug acima, estas são as limitações que estou tendo com o script:

  1. Permalinks: Parece que o script importador criará permalinks para os antigos URLs de nós example.com/node/XXXXXXX. Mas também preciso manter links para comentários específicos dentro desses nós, que têm o formato: example.com/comment/YYYYYYY#comment-YYYYYYY (YYYYYYY é o mesmo em ambas as ocorrências). O esquema de URL do Drupal não inclui o ID do nó ao qual o comentário está associado, enquanto o Discourse sim (example.com/t/topic-keywords/XXXXXXX/YY), o que parece ser uma complicação importante.

  2. Limitações de nome de usuário: O Drupal permite espaços em nomes de usuário. Entendo que o Discourse não permite, pelo menos não permite que novos usuários os criem dessa forma. Esta postagem sugere que o script importador “converterá” automaticamente os nomes de usuário problemáticos, mas não vejo nenhum código para isso em /import_scripts/drupal.rb. Atualização: Na verdade, parece que o Discourse lidou com isso automaticamente da maneira correta.

  3. Usuários banidos: Parece que o script importa todos os usuários, incluindo contas banidas. Eu provavelmente poderia adicionar uma condição muito facilmente à seleção SQL WHERE status = 1 para importar apenas contas de usuário ativas, mas não tenho certeza se isso causaria problemas com a serialização dos registros. Acima de tudo, eu preferiria manter esses nomes de contas previamente banidos com seus endereços de e-mail associados permanentemente bloqueados para que os mesmos usuários problemáticos não se registrem novamente no Discourse.

  4. Campos de perfil de usuário: Alguém sabe se há código de exemplo em um dos outros importadores para importar campos de informações pessoais dos perfis de contas de usuário? Tenho apenas um campo de perfil (“Localização”) que preciso importar.

  5. Avatares (não Gravatars): Parece um pouco estranho que haja código no importador Drupal para importar Gravatars, mas não para as imagens de avatar de conta local muito mais usadas.

  6. Mensagens privadas: Quase todos os fóruns Drupal 7 provavelmente usarão o módulo de terceiros privatemsg (não há funcionalidade oficial de PM no Drupal). O importador não suporta a importação de PMs. No meu caso, preciso importar cerca de 1,5 milhão deles.

Obrigado antecipadamente pela sua ajuda e por disponibilizar o script importador Drupal.

Este conjunto de problemas é bastante comum para uma grande importação. Quem quer que tenha sido escrito não se importou (talvez não o suficiente para notar) com os problemas que você descreve.

O que parece ser um bug no Drupal ou no próprio banco de dados (IDs duplicados não deveriam acontecer). Eu provavelmente teria modificado o script para testar e/ou capturar o erro quando houver duplicatas, mas o seu jeito funcionou (a menos que ainda haja mais).

Você pode procurar em outros scripts de importação que criam permalinks de posts. O import_id está no PostCustomField de cada post.

Está em base.rb ou no sugeridor de nomes de usuário. Geralmente funciona e não há muito que você possa fazer para mudá-lo.

Provavelmente você não vai querer fazer isso. O problema é que os posts criados por esses usuários serão de propriedade do system. Você pode procurar em outros scripts exemplos de como desativá-los. O fluxbb tem um script suspend_users, que deve ajudar.

O fluxbb (no qual estou trabalhando agora) faz isso. Você só precisa adicionar algo como isto ao script de importação de usuários:

          location: user['location'],

Gravatars são tratados pelo core do discourse, então o script não faz nada para importá-los; apenas funciona. Você pode usar grep nos outros scripts para “avatar” para encontrar exemplos de como fazer isso.

Procure por exemplos. . . . O ipboard tem import_private_messages.

1 curtida

Obrigado pela resposta. Não acho que seja um problema com o banco de dados do Drupal, porque inspecionei o banco de dados de origem e não consigo encontrar chaves nid duplicadas.

Ahhh, então ele tem essa funcionalidade fora de drupal.rb. Agora que estou explorando o site de importação de teste, na verdade parece que ele lidou muito bem com as conversões de nome de usuário. Obrigado!

1 curtida

Qual seria a maneira mais fácil de habilitar a importação de nomes de usuário Unicode (sem convertê-los, ou seja, manter o nome de usuário Narizón em vez de convertê-lo para Narizon)?

Fiz meu primeiro teste do importador do Drupal em uma instância sem GUI web configurada, então não defini a opção do Discourse para permitir nomes de usuário Unicode. Se isso tivesse sido definido, o importador teria respeitado? Qual é a maneira recomendada de habilitar isso para quando eu executar minha migração de produção?

E, enquanto isso, para minha instância de teste atual, existe algum comando rake para aplicar o nome completo ao nome de usuário? (Eu já ativei prioritize username in ux, mas como meus usuários de teste estão acostumados com o Drupal, que suporta apenas nomes de usuário para login [não endereço de e-mail], acho que seria melhor manter seus nomes de usuário de produção, que pelo menos foram mantidos no campo fullname.)

Provavelmente?

Você pode definir a configuração do site no início do script.

Eu acho que mudar nomes de usuário é uma má ideia, mas se você não gosta deles, você pode mudar o que é passado para o gerador de nomes de usuário.

Obrigado, você quer dizer mudá-los após a importação ser concluída?

Eu acho que quero dizer alterá-los, a menos que no sistema antigo os nomes de usuário fossem invisíveis e eles vissem apenas nomes reais.

Se for este o caso, então eu mudaria o script para fazer o nome de usuário ser o nome real deles. O problema com isso é que, se eles não souberem o endereço de e-mail deles, não conseguirão encontrar a conta deles.

Entendi. No fórum do Drupal, existem apenas nomes de usuário do sistema e nenhum nome real separado. E adicionalmente, o Drupal não permite o login com o endereço de e-mail, apenas com o nome de usuário. É por isso que é muito importante no meu caso manter os nomes de usuário o máximo possível. (Ainda haverá alguns nomes de usuário convertidos, como aqueles com espaços.) Portanto, preciso investigar como definir as configurações do Discourse no início do script de importação.

Mas o Discourse permite, então se eles souberem o endereço de e-mail, eles podem usá-lo para redefinir a senha, que é provavelmente o que você deveria dizer a todos para fazerem, já que você não pode adivinhar quem não consegue adivinhar o nome de usuário deles, eu acho.

Eu acho que o que eu faria seria definir SiteSetting.unicode_username=true no script de importação e executá-lo novamente para ver se funciona. Você pode tentar testá-lo no console do Rails para ver. Isso pode lhe dizer:

  User.create(username: 'Narizón', email: 'user@x.com',password: 'xxx', active: true)

Bem, eu acho que isso ainda não chamaria a função de criação de nome de usuário, então você precisará chamá-la

  UserNameSuggester.suggest("Narizón")

Não. Isso ainda não lhe dará um nome de usuário unicode. Você precisará encontrar o UserNameSuggester e ajustá-lo, eu acho.

Mas se você realmente quiser mudar os nomes de usuário, mudá-los agora em vez de corrigir o script pode ser o que você quer fazer. Você precisa ter certeza de que a maneira como você faz isso atualiza o nome de usuário em todas as postagens. Se você estiver usando uma tarefa rake, ela definitivamente fará isso.

1 curtida

Excelente, muito obrigado Jay! Vou tentar isso na próxima vez que executar o importador.

Acho que você não deveria se incomodar:

Isso está em lib/user_name_suggester.rb, mas talvez você queira User.normalize_username

1 curtida

Com certeza, você estava certo. Nem é um bug em si, acabou sendo uma forma estranha que o Drupal lida com tópicos movidos, deixando um rastro na antiga categoria do tópico. Ele simplesmente cria uma linha duplicada em uma das muitas tabelas que são puxadas para o que eventualmente se torna um tópico completo do Drupal. Então, parece que preciso descobrir como aplicar DISTINCT a apenas uma das tabelas que são selecionadas…

1 curtida

Sim. É incrível como cada importação é única, e de alguma forma o seu é o primeiro fórum a ter esse problema (claro, muitas pessoas podem ter resolvido o problema e não conseguiram enviar um PR com a atualização). Ou talvez eles ignoraram os erros?

Aha. Suspeito que não seja uma função muito usada, quando um tópico é movido para uma nova categoria, há uma caixa de seleção opcional para deixar um link “Movido para…” na categoria antiga.

A duplicata ofensiva está na coluna nid de forum_index. Então, parece que posso corrigi-la com um GROUP BY nid, certo?

        SELECT fi.nid nid,
               fi.title title,
               fi.tid tid,
               n.uid uid,
               fi.created created,
               fi.sticky sticky,
               f.body_value body,
	       nc.totalcount views
          FROM forum_index fi
	 LEFT JOIN node n ON fi.nid = n.nid
	 LEFT JOIN field_data_body f ON f.entity_id = n.nid
	 LEFT JOIN node_counter nc ON nc.nid = n.nid
         WHERE n.type = 'forum'
           AND n.status = 1
         GROUP BY nid

Parece promissor, porque quando executo a consulta com o GROUP BY nid, há 8 linhas a menos.

Isso pode funcionar. Eu pensaria que haveria algum valor nessa tabela que indicasse que ela havia sido movida e você poderia selecionar apenas aquelas sem esse valor.

Essa seria definitivamente a maneira mais lógica de projetá-la. Acho que é uma coisa do Drupal…

A única coisa que ele faz é alterar o tid (ID da categoria). Isso segue o estilo que aprendi durante essa provação com o banco de dados Drupal. Não sei nada sobre design de banco de dados, mas tenho a impressão de que você pode armazenar dados explicitamente, ou então pode deixar algumas coisas implícitas e depois descobri-las por meio de lógica programática; o Drupal parece cair firmemente no último grupo.

2 curtidas

Well, it looks like I’m almost there. Thanks a lot to Jay for the guidance.

Thanks, this was key, it was actually as simple as copying the permalink part of the Drupal import script itself and changing it to run on posts instead of topics:

    ## I added permalinks for each Drupal comment (reply) link: /comment/DIGITS#comment-DIGITS
    Post.find_each do |post|
      begin
        pcf = post.custom_fields
        if pcf && pcf['import_id']
          cid = pcf['import_id'][/cid:(\d+)/, 1]
          slug = "/comment/#{cid}" # The #comment-DIGITS part breaks the permalink and isn't needed
          Permalink.create(url: slug, post_id: post.id)
        end
      rescue => e
        puts e.message
        puts "Permalink creation failed for cid #{post.id}"
      end
    end

I was stuck for a while with my original attempt that included the relative page #comment-DIGITS part of the original Drupal link, which completely breaks the permalink in Discourse. then I realized that of course the # part of a link doesn’t actually get passed to the webserver and was only needed for Drupal to make it scroll to the part of the page where the specific comment was located. So it works fine without that in Discourse even if coming from an external web page with an old /comment/YYYYYY#comment-YYYYY link, it simply looks like this in Discourse: /comment/YYYYYY/t/topic-title-words/123456/X and the URL bar shows like: /t/topic-title-words/123456/X#comment-YYYYYY , it doesn’t appear to care about the bogus #comment-YYYYYY part.

For some forums I suspect that the stock Drupal importer postprocess_posts function might actually be enough. It should be noted that it needs to be adjusted for each forum, there’s a rather sloppy hard-coded regexp replace for site.comcommunity.site.com. But after adjusting that it does a good job of rewriting internal forum links for nodes → topics as well as comments → replies. But I do have a fair number of external websites linking to individual comments (replies) on my forum and it’s worth conserving those. Plus Google indexes most of the 1.7M /comment-YYYYYY URLs and it would probably hurt my ranking if those all disappeared. I hope it won’t cause any problems for Discourse to have ~2M permalinks though?


Thanks a lot, I lifted that function almost without modifications, just had to adjust a few column names. Works great.

  def suspend_users
    puts '', "updating banned users"

    banned = 0
    failed = 0
    total = mysql_query("SELECT COUNT(*) AS count FROM users WHERE status = 0").first['count']

    system_user = Discourse.system_user

    mysql_query("SELECT name username, mail email FROM users WHERE status = 0").each do |b|
      user = User.find_by_email(b['email'])
      if user
        user.suspended_at = Time.now
        user.suspended_till = 200.years.from_now

        if user.save
          StaffActionLogger.new(system_user).log_user_suspend(user, "banned during initial import")
          banned += 1
        else
          puts "Failed to suspend user #{user.username}. #{user.errors.try(:full_messages).try(:inspect)}"
          failed += 1
        end
      else
        puts "Not found: #{b['email']}"
        failed += 1
      end

      print_status banned + failed, total
    end
  end

Also worked! I did have to deal with Drupal’s diffuse DB schema and LEFT JOIN profile_value location ON users.uid = location.uid to correlate another table that contains the profile data, but very cool that it’s so easy to add on the Discourse side of things. It’s worth noting that this process runs about 50% slower than stock, I suspect it’s due to the LEFT JOIN. But I can live with it, as I only have about 80K users.


This was fairly hard, once again due to Drupal’s disjointed database schema. I ended up using jforum.rb as the basis with a little help from the Vanilla importer too. The original script was rather paranoid with checking at every single variable pass to make sure the avatar filename isn’t null, so I removed most of those checks to make the code less messy. The worst that can happen is that the script could crash, but with the SQL query I used I don’t think even that could go wrong.

  def import_users
    puts "", "importing users"

    user_count = mysql_query("SELECT count(uid) count FROM users").first["count"]

    last_user_id = -1
    
    batches(BATCH_SIZE) do |offset|
      users = mysql_query(<<-SQL
          SELECT users.uid,
                 name username,
                 mail email,
                 created,
                 picture,
                 location.value location
            FROM users
             LEFT JOIN profile_value location ON users.uid = location.uid
           WHERE users.uid > #{last_user_id}
        ORDER BY uid
           LIMIT #{BATCH_SIZE}
      SQL
      ).to_a

      break if users.empty?

      last_user_id = users[-1]["uid"]

      users.reject! { |u| @lookup.user_already_imported?(u["uid"]) }

      create_users(users, total: user_count, offset: offset) do |row|
        if row['picture'] > 0
        	q = mysql_query("SELECT filename FROM file_managed WHERE fid = #{row['picture']};").first
        	avatar = q["filename"]
        end
        email = row["email"].presence || fake_email
        email = fake_email if !EmailAddressValidator.valid_value?(email)

        username = @htmlentities.decode(row["username"]).strip

        {
          id: row["uid"],
          name: username,
          email: email,
          location: row["location"],
          created_at: Time.zone.at(row["created"]),
	  	post_create_action: proc do |user|
		    import_avatar(user, avatar)
		end
        }
      end 
    end
  end
  def import_avatar(user, avatar_source)
    return if avatar_source.blank?

    path = File.join(ATTACHMENT_DIR, avatar_source)

      @uploader.create_avatar(user, path)
  end

After your paid help with the SQL query I ended up trying to hack it into the script for Discuz, IPboard, and Xenforo. I kept getting hitting dead ends with each one, I got closest with the Discuz model which appears to have a very similar database schema, but I couldn’t get past a bug with the @first_post_id_by_topic_id instance variable. After tons of trial and error I finally realized that it was improperly initialized at the beginning of the Discuz script (I tried to put it in the same location in the Drupal script) and this finally fixed it:

  def initialize
    super
    
    @first_post_id_by_topic_id = {}

    @htmlentities = HTMLEntities.new

    @client = Mysql2::Client.new(
      host: "172.17.0.3",
      username: "user",
      password: "pass",
      database: DRUPAL_DB
    )
  end

def import_private_messages
	puts '', 'creating private messages'

	pm_indexes = 'pm_index'
	pm_messages = 'pm_message'
	total_count = mysql_query("SELECT count(*) count FROM #{pm_indexes}").first['count']

	batches(BATCH_SIZE) do |offset|
		results = mysql_query("
SELECT pi.mid id, thread_id, pi.recipient to_user_id, pi.deleted deleted, pm.author user_id, pm.subject subject, pm.body message, pm.format format, pm.timestamp created_at FROM pm_index pi LEFT JOIN pm_message pm ON pi.mid=pm.mid WHERE deleted = 0
             LIMIT #{BATCH_SIZE}
            OFFSET #{offset};")

		break if results.size < 1

		# next if all_records_exist? :posts, results.map {|m| "pm:#{m['id']}"}

		create_posts(results, total: total_count, offset: offset) do |m|
			skip = false
			mapped = {}
			mapped[:id] = "pm:#{m['id']}"
			mapped[:user_id] = user_id_from_imported_user_id(m['user_id']) || -1
			mapped[:raw] = preprocess_raw(m['message'],m['format'])
			mapped[:created_at] = Time.zone.at(m['created_at'])
			thread_id = "pm_#{m['thread_id']}"
			if is_first_pm(m['id'], m['thread_id'])
				# find the title from list table
				#          pm_thread = mysql_query("
				#                SELECT thread_id, subject
				#                  FROM #{table_name 'ucenter_pm_lists'}
				#                 WHERE plid = #{m['thread_id']};").first
				mapped[:title] = m['subject']
				mapped[:archetype] = Archetype.private_message

          # Find the users who are part of this private message.
          import_user_ids = mysql_query("
                SELECT thread_id plid, recipient user_id
                  FROM pm_index
                 WHERE thread_id = #{m['thread_id']};
              ").map { |r| r['user_id'] }.uniq
          mapped[:target_usernames] = import_user_ids.map! do |import_user_id|
            import_user_id.to_s == m['user_id'].to_s ? nil : User.find_by(id: user_id_from_imported_user_id(import_user_id)).try(:username)
          end.compact
          if mapped[:target_usernames].empty? # pm with yourself?
            skip = true
            puts "Skipping pm:#{m['id']} due to no target"
          else
            @first_post_id_by_topic_id[thread_id] = mapped[:id]
          end
        else
          parent = topic_lookup_from_imported_post_id(@first_post_id_by_topic_id[thread_id])
          if parent
            mapped[:topic_id] = parent[:topic_id]
          else
            puts "Parent post pm thread:#{thread_id} doesn't exist. Skipping #{m["id"]}: #{m["message"][0..40]}"
            skip = true
          end
        end
        skip ? nil : mapped
      end

    end
end
# search for first pm id for the series of pm
def is_first_pm(pm_id, thread_id)
	result = mysql_query("
          SELECT mid id
            FROM pm_index
           WHERE thread_id = #{thread_id}
        ORDER BY id")
	result.first['id'].to_s == pm_id.to_s
end

Oh, and for most of these queries it also requires running this in the MySQL container to disable a strict mode SQL sanity check:
mysql -u root -ppass -e "SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''));"


Another thing I realized was missing were a few thousand Drupal nodes of type poll . I first tried to just include WHERE type = 'forum' OR type = 'poll' in the import_topics function, but there is some seriously janky stuff going on in the original Drupal database that causes it to miss many of them. So I ended up copying the import_topics into a new import_polls function:

    def import_poll_topics
    puts '', "importing poll topics"

    polls = mysql_query(<<-SQL
      SELECT n.nid nid, n.title title, n.uid uid, n.created created, n.sticky sticky, taxonomy_index.tid tid, node_counter.totalcount views
        FROM node n
        LEFT JOIN taxonomy_index ON n.nid = taxonomy_index.nid
        LEFT JOIN node_counter ON n.nid = node_counter.nid
       WHERE n.type = 'poll'
         AND n.status = 1
    SQL
    ).to_a

    create_posts(polls) do |topic|
      {
        id: "nid:#{topic['nid']}",
        user_id: user_id_from_imported_user_id(topic['uid']) || -1,
        category: category_id_from_imported_category_id(topic['tid']),
        raw: "### You can see the archived poll results on the Wayback Machine:\n**https://web.archive.org/web/1234567890/http://myforum.com/node/#{topic['nid']}**",
        created_at: Time.zone.at(topic['created']),
        pinned_at: topic['sticky'].to_i == 1 ? Time.zone.at(topic['created']) : nil,
        title: topic['title'].try(:strip),
        views: topic['views'],
        custom_fields: { import_id: "nid:#{topic['nid']}" }
      }
    end
  end

I don’t care too much about importing the actual poll results, and it would require re-coding the entire algorithm that Drupal uses to tally up all the votes and eliminates duplicates. I mainly just want to import the followup comments in the poll thread. But just in case anyone wants to see the original poll results I made it write out a direct link to the original forum node in the Wayback Machine.


So the code is not at all elegant and probably isn’t very efficient, but for a one-shot deal that should get the job done.

Sorry for the walls of code, let me know if that irritates anyone and I can move them to a pastebin URL.

1 curtida

E é assim que a maioria deles vai. Este tópico é um ótimo exemplo para outra pessoa seguir (dado que eles comecem com um conjunto de habilidades semelhante ao seu).

Você tem uma estimativa de quanto tempo gastou fazendo suas personalizações?

Parabéns!

1 curtida

Obrigado, Jay! Agradeço o incentivo.

Ugh, prefiro não pensar nisso. :stuck_out_tongue_winking_eye: Provavelmente foram mais de 15 ou 20 horas depois que você me colocou no caminho certo com a consulta SQL.

Gostaria de conversar com você sobre isso se tiver alguma ideia:

Levou cerca de 70 horas para fazer um teste completo com dados de produção em um VPS muito poderoso. Gostaria de fazer com que meus usuários interajam novamente o mais rápido possível, mesmo que a importação de posts e MPs ainda esteja incompleta. Ou outra ideia alternativa que pensei seria desabilitar a função preprocess_posts, que também modifiquei bastante com substituições adicionais de regex gsub e também para passar todos os posts e MPs pelo Pandoc com um ou dois comandos diferentes, dependendo se o post original era marcação Textile ou HTML puro. Se eu desabilitar toda a rotina preprocess_posts, provavelmente cortaria o tempo de importação pela metade, e então eu poderia adicionar todo esse material de formatação à seção postprocess_posts depois que todos os dados brutos fossem importados. Mas a desvantagem é que, depois, eu não conseguiria acessar facilmente a coluna original do banco de dados que mostra o formato de origem (Textile ou HTML) para cada post, o que é uma condição para minha manipulação do Pandoc. Ou eu poderia adicionar um campo personalizado a cada post, rotulando-o como textile ou html e recuperá-lo mais tarde durante o pós-processamento? Sei lá, apenas pensando alto aqui.

Quando você executar o script de importação novamente apenas com os novos dados, ele será executado muito mais rápido, pois não importará os dados novamente. Assim, levará apenas algumas horas. E cada execução subsequente será mais rápida, pois haverá menos dados para importar.

Você pode então acelerar isso modificando as consultas para retornar apenas dados mais recentes que um determinado tempo. A maioria dos scripts que toquei tem uma configuração de import_after para esse propósito (mas também para permitir um desenvolvimento mais rápido importando um pequeno subconjunto dos dados).

1 curtida