Grande migration de forum Drupal, erreurs et limitations de l'importateur

Salut, ce sujet donne un peu de contexte sur la migration que je planifie et teste lentement. J’ai finalement essayé l’importateur Drupal vendredi dernier sur un VPS de test en utilisant une combinaison de ceci et ceci. L’importateur est toujours en cours au moment où j’écris ceci, donc je n’ai pas encore pu tester la fonctionnalité du site de test, mais il est sur le point de se terminer bientôt.

Le plus gros problème que je rencontre est une « valeur de clé dupliquée » sur 8 nœuds apparemment aléatoires (l’équivalent des sujets dans Discourse) sur environ 80 000 nœuds au total. Ce sont les numéros nid spécifiques au cas où il y aurait un bug mathématique étrange de type Y2K :

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

La même erreur se produit toujours sur les mêmes nid lors de la réexécution de l’importateur :

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)

La seule façon que j’ai trouvée pour qu’il continue a été de modifier les conditions 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};
...

J’ai inspecté le premier nœud échoué ainsi que les nid précédents et suivants de chaque côté dans la base de données Drupal source, et je ne vois rien de mal. Le nid est défini comme clé primaire et il a AUTO_INCREMENT, et le site Drupal d’origine fonctionne bien, donc il ne peut y avoir aucun problème fondamental avec l’intégrité de la base de données source.


Outre le bug ci-dessus, voici les limitations que je rencontre avec le script :

  1. Permaliens : Il semble que le script d’importation créera des permaliens pour les anciennes URL de nœuds example.com/node/XXXXXXX. Mais je dois également maintenir des liens vers des commentaires spécifiques dans ces nœuds, qui ont le format : example.com/comment/YYYYYYY#comment-YYYYYYY (YYYYYYY est le même dans les deux occurrences). Le schéma d’URL de Drupal n’inclut pas l’ID du nœud auquel le commentaire est associé, alors que Discourse le fait (example.com/t/topic-keywords/XXXXXXX/YY), ce qui semble être une complication majeure.

  2. Limitations des noms d’utilisateur : Drupal autorise les espaces dans les noms d’utilisateur. Je comprends que Discourse ne le fait pas, du moins il ne permet pas aux nouveaux utilisateurs d’en créer de cette façon. Ce post suggère que l’importateur « convertira » automatiquement les noms d’utilisateur problématiques, mais je ne vois aucun code pour cela dans /import_scripts/drupal.rb. Mise à jour : En fait, il semble que Discourse ait géré cela automatiquement de manière correcte.

  3. Utilisateurs bannis : Il semble que le script importe tous les utilisateurs, y compris les comptes bannis. Je pourrais probablement ajouter une condition assez facilement à la sélection SQL WHERE status = 1 pour n’importer que les comptes d’utilisateurs actifs, mais je ne suis pas sûr que cela causerait des problèmes avec la sérialisation des enregistrements. Par-dessus tout, je préférerais garder ces noms de comptes précédemment bannis avec leurs adresses e-mail associées bloqués en permanence afin que les mêmes utilisateurs problématiques ne s’inscrivent pas à nouveau sur Discourse.

  4. Champs de profil utilisateur : Quelqu’un sait-il s’il existe un exemple de code dans l’un des autres importateurs pour importer des champs d’informations personnelles à partir des profils de comptes utilisateurs ? J’ai un seul champ de profil (« Lieu ») que je dois importer.

  5. Avatars (pas Gravatars) : Il semble assez étrange qu’il y ait du code dans l’importateur Drupal pour importer les Gravatars mais pas pour les images d’avatar de compte local, beaucoup plus couramment utilisées.

  6. Messages privés : Presque tous les forums Drupal 7 utiliseront probablement le module tiers privatemsg (il n’y a pas de fonctionnalité officielle de PM Drupal). L’importateur ne prend pas en charge l’importation des PM. Dans mon cas, je dois en importer environ 1,5 million.

Merci d’avance pour votre aide et pour avoir rendu le script d’importation Drupal disponible.

Cet ensemble de problèmes est tout à fait normal pour une importation importante. Celui pour lequel cela a été écrit ne se souciait pas (peut-être pas assez pour le remarquer) des problèmes que vous décrivez.

Ce qui ressemble à un bug dans Drupal ou dans la base de données elle-même (les identifiants dupliqués ne devraient pas se produire). J’aurais probablement modifié le script pour tester et/ou intercepter l’erreur en cas de doublons, mais votre méthode a fonctionné (sauf s’il y en a d’autres).

Vous pouvez consulter d’autres scripts d’importation qui créent des permaliens de publication. L’import_id se trouve dans le PostCustomField de chaque publication.

Il se trouve soit dans base.rb, soit dans le suggéreur de nom d’utilisateur. Cela fonctionne globalement et il n’y a pas grand-chose que vous puissiez faire pour le changer.

Vous ne voudriez probablement pas faire cela. Le problème est que les publications créées par ces utilisateurs seront la propriété de system. Vous pouvez consulter d’autres scripts pour des exemples sur la façon de les désactiver. fluxbb a un script suspend_users, qui devrait aider.

fluxbb (sur lequel je travaille actuellement) fait cela. Il suffit d’ajouter quelque chose comme ceci au script d’importation d’utilisateur :

          location: user['location'],

Les Gravatars sont gérés par le cœur de discourse, donc le script ne fait rien pour les importer ; cela fonctionne simplement. Vous pouvez rechercher “avatar” dans les autres scripts pour trouver des exemples sur la façon de faire cela.

Recherchez des exemples. . . . ipboard a import_private_messages.

Merci pour votre réponse. Je ne pense pas qu’il s’agisse d’un problème avec la base de données Drupal, car j’ai inspecté la base de données source et je ne trouve aucune clé nid en double.

Ahhh, donc il a cette fonctionnalité en dehors de drupal.rb. Maintenant que je fouille dans le site d’importation de test, il semble en fait qu’il ait très bien géré les conversions de noms d’utilisateur. Merci !

Quelle serait la manière la plus simple d’activer l’importation des noms d’utilisateur unicode (sans les convertir, c’est-à-dire conserver le nom d’utilisateur Narizón au lieu de le convertir en Narizon) ?

J’ai effectué mon premier test de l’importateur Drupal sur une instance sans interface graphique web configurée, donc je n’avais pas défini l’option Discourse pour autoriser les noms d’utilisateur unicode. Si cela avait été défini, l’importateur l’aurait-il respecté ? Quelle est la méthode recommandée pour activer cela pour ma migration de production ?

Et en attendant, pour mon instance de test actuelle, existe-t-il une commande rake pour appliquer le nom complet au nom d’utilisateur ? (J’ai déjà activé prioritize username in ux mais comme mes utilisateurs de test sont habitués à Drupal qui ne prend en charge que les noms d’utilisateur pour la connexion [pas l’adresse e-mail], je pense qu’il serait préférable de conserver leurs noms d’utilisateur de production, qui ont au moins été conservés dans le champ nom complet.)

Probablement ?

Vous pouvez définir le paramètre du site au début du script.

Je pense que changer les noms d’utilisateur est une mauvaise idée, mais si vous ne les aimez pas, vous pourriez changer ce qui est passé au générateur de noms d’utilisateur.

Merci, vous voulez dire les changer après la fin de l’importation ?

Je pense que je veux dire les changer du tout, sauf si sur l’ancien système les noms d’utilisateur étaient invisibles et qu’ils ne voyaient que les vrais noms.

Si c’est le cas, alors je changerais le script pour que le nom d’utilisateur soit leur vrai nom. Le problème avec cela est que s’ils ne connaissent pas leur adresse e-mail, ils ne pourront pas trouver leur compte.

Bien reçu. Sur le forum Drupal, il n’y a que des noms d’utilisateur système et pas de vrais noms distincts. De plus, Drupal ne permet pas de se connecter avec l’adresse e-mail, mais uniquement avec le nom d’utilisateur. C’est pourquoi il est très important dans mon cas de conserver autant que possible les noms d’utilisateur. (Il y aura toujours des noms d’utilisateur convertis, comme ceux avec des espaces.) Je dois donc examiner comment configurer les paramètres de Discourse au début du script d’importation.

Mais Discourse le fait, donc s’ils connaissent leur adresse e-mail, ils peuvent l’utiliser pour réinitialiser leur mot de passe, ce qui est probablement ce que vous devriez dire à tout le monde de faire, puisque vous ne pouvez pas deviner qui ne peut pas deviner son nom d’utilisateur, je suppose.

Je pense que ce que je ferais, c’est définir SiteSetting.unicode_username=true dans le script d’importation et le relancer pour voir si cela fonctionne. Vous pourriez essayer de le tester dans la console Rails pour voir. Cela pourrait vous dire :

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

Eh bien, je pense que cela n’appellera pas le créateur de nom d’utilisateur, vous devrez donc l’appeler

  UserNameSuggester.suggest("Narizón")

Non. Cela ne vous donnera toujours pas un nom d’utilisateur unicode. Vous devrez trouver le UserNameSuggester et le modifier, je suppose.

Mais si vous voulez vraiment changer les noms d’utilisateur, les changer maintenant plutôt que de corriger le script pourrait être ce que vous voulez faire. Vous devez vous assurer que la façon dont vous le faites met à jour le nom d’utilisateur dans tous les messages. Si vous utilisez une tâche Rake, cela le fera certainement.

Excellent, merci beaucoup Jay ! J’essaierai cela la prochaine fois que j’exécuterai l’importateur.

Je ne pense pas que vous devriez vous en soucier :

C’est dans lib/user_name_suggester.rb, mais peut-être voulez-vous User.normalize_username

Effectivement, vous aviez raison. Ce n’est même pas un bug en soi, il s’est avéré être une manière étrange dont Drupal gère les sujets déplacés tout en laissant une trace dans l’ancienne catégorie de sujet. Il crée simplement une ligne en double dans l’une des nombreuses tables qui sont toutes récupérées pour ce qui devient finalement un sujet Drupal complet. Il semble donc que je doive trouver comment appliquer DISTINCT à une seule des tables sélectionnées…

Ouais. C’est incroyable comment chaque importation est unique, et d’une manière ou d’une autre, le vôtre est le premier forum à avoir eu ce problème (bien sûr, beaucoup de gens ont peut-être résolu le problème et n’ont pas réussi à soumettre une PR avec la mise à jour). Ou peut-être ont-ils ignoré les erreurs ?

Aha. Je soupçonne que ce n’est pas une fonction très couramment utilisée, lorsqu’un fil de discussion est déplacé vers une nouvelle catégorie, il y a une case à cocher facultative pour laisser un lien « Déplacé vers… » dans l’ancienne catégorie.

Le duplicata fautif se trouve dans la colonne nid de forum_index. Donc, il semble que je puisse le corriger avec un GROUP BY nid, n’est-ce pas ?

        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

Cela semble prometteur, car lorsque j’exécute la requête avec le GROUP BY nid, il y a 8 lignes en moins.

Cela pourrait fonctionner. Je pensais qu’il y aurait une valeur dans cette table indiquant qu’elle avait été déplacée et que vous pourriez sélectionner uniquement celles qui n’ont pas cette valeur.

Ce serait certainement la façon la plus logique de le concevoir. Je suppose que c’est une chose propre à Drupal…

La seule chose que cela fait est de changer le tid (ID de catégorie). Cela suit le style que j’ai appris au cours de cette épreuve avec la base de données Drupal. Je ne sais rien de la conception de bases de données, mais j’ai l’impression que vous pouvez soit stocker explicitement des données, soit laisser certaines choses implicites, puis les déterminer par une logique programmatique ; Drupal semble tomber squarely dans ce dernier camp.

Eh bien, il semble que j’y sois presque. Merci beaucoup à Jay pour son aide.

Merci, c’était la clé. C’était en fait aussi simple que de copier la partie du permalien du script d’importation Drupal lui-même et de l’adapter pour fonctionner sur les publications plutôt que sur les sujets :

    ## J'ai ajouté des permaliens pour chaque lien de commentaire (réponse) Drupal : /comment/CHIFFRES#comment-CHIFFRES
    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}" # La partie #comment-CHIFFRES brise le permalien et n'est pas nécessaire
          Permalink.create(url: slug, post_id: post.id)
        end
      rescue => e
        puts e.message
        puts "Échec de la création du permalien pour cid #{post.id}"
      end
    end

J’étais bloqué pendant un moment avec ma tentative initiale qui incluait la partie relative de la page #comment-CHIFFRES du lien Drupal original, ce qui brise complètement le permalien dans Discourse. Ensuite, j’ai réalisé que, bien sûr, la partie # d’un lien n’est pas réellement transmise au serveur web et n’était nécessaire que pour que Drupal fasse défiler la page jusqu’à l’endroit où se trouvait le commentaire spécifique. Donc, cela fonctionne très bien sans cela dans Discourse, même si l’on vient d’une page web externe avec un ancien lien /comment/AAAAAA#comment-AAAAA. Dans Discourse, cela ressemble simplement à ceci : /comment/AAAAAA/t/titre-du-sujet-mots/123456/X, et la barre d’adresse affiche : /t/titre-du-sujet-mots/123456/X#comment-AAAAAA. Il ne semble pas se soucier de la partie fictive #comment-AAAAAA.

Pour certains forums, je soupçonne que la fonction postprocess_posts de l’importateur Drupal par défaut pourrait suffire. Il est à noter qu’elle doit être ajustée pour chaque forum ; il y a un remplacement de regexp assez négligé et codé en dur pour site.comcommunity.site.com. Mais après cet ajustement, elle fait un bon travail de réécriture des liens internes du forum pour les nœuds → sujets ainsi que les commentaires → réponses. Cependant, j’ai un bon nombre de sites web externes pointant vers des commentaires individuels (réponses) sur mon forum, et il vaut la peine de les conserver. De plus, Google indexe la plupart des 1,7 million d’URL /comment-AAAAAA, et cela nuirait probablement à mon classement si elles disparaissaient toutes. J’espère que cela ne causera aucun problème à Discourse d’avoir environ 2 millions de permaliens ?


Merci beaucoup, j’ai repris cette fonction presque sans modifications, il a fallu ajuster quelques noms de colonnes. Ça fonctionne parfaitement.

  def suspend_users
    puts '', "mise à jour des utilisateurs bannis"

    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, "banni lors de l'importation initiale")
          banned += 1
        else
          puts "Échec de la suspension de l'utilisateur #{user.username}. #{user.errors.try(:full_messages).try(:inspect)}"
          failed += 1
        end
      else
        puts "Non trouvé : #{b['email']}"
        failed += 1
      end

      print_status banned + failed, total
    end
  end

Aussi fonctionné ! J’ai dû gérer le schéma de base de données diffus de Drupal et LEFT JOIN profile_value location ON users.uid = location.uid pour corréler une autre table contenant les données de profil, mais c’est très cool que ce soit si facile à ajouter du côté de Discourse. Il est à noter que ce processus est environ 50 % plus lent que le processus standard ; je soupçonne que cela est dû au LEFT JOIN. Mais je peux vivre avec, car je n’ai qu’environ 80 000 utilisateurs.


C’était assez difficile, encore une fois à cause du schéma de base de données disjoint de Drupal. J’ai fini par utiliser jforum.rb comme base avec un peu d’aide de l’importateur Vanilla également. Le script original était plutôt paranoïaque avec des vérifications à chaque passage de variable pour s’assurer que le nom de fichier de l’avatar n’est pas nul, alors j’ai supprimé la plupart de ces vérifications pour rendre le code moins désordonné. Le pire qui puisse arriver, c’est que le script plante, mais avec la requête SQL que j’ai utilisée, je ne pense pas que cela puisse même arriver.

  def import_users
    puts "", "importation des utilisateurs"

    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

Après votre aide payante avec la requête SQL, j’ai fini par essayer de l’intégrer dans les scripts pour Discuz, IPboard et Xenforo. J’ai constamment buté sur des impasses avec chacun d’eux ; j’ai été le plus proche avec le modèle Discuz, qui semble avoir un schéma de base de données très similaire, mais je n’ai pas pu dépasser un bug avec la variable d’instance @first_post_id_by_topic_id. Après des tonnes d’essais et d’erreurs, j’ai finalement réalisé qu’elle était mal initialisée au début du script Discuz (j’ai essayé de la mettre au même endroit dans le script Drupal) et cela l’a enfin corrigé :

  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 '', 'création des messages privés'

	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'])
				# trouver le titre depuis la table de liste
				#          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

          # Trouver les utilisateurs qui font partie de ce message privé.
          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? # message privé avec soi-même ?
            skip = true
            puts "Saut du mp:#{m['id']} en raison de l'absence de cible"
          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 "Publication parente du fil mp:#{thread_id} n'existe pas. Saut de #{m["id"]}: #{m["message"][0..40]}"
            skip = true
          end
        end
        skip ? nil : mapped
      end

    end
end
# rechercher le premier id de mp pour la série de mp
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, et pour la plupart de ces requêtes, il faut également exécuter ceci dans le conteneur MySQL pour désactiver un contrôle de cohérence SQL en mode strict :
mysql -u root -ppass -e "SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''));"


Une autre chose que j’ai remarquée manquante était quelques milliers de nœuds Drupal de type poll. J’ai d’abord essayé d’inclure simplement WHERE type = 'forum' OR type = 'poll' dans la fonction import_topics, mais il y a des choses vraiment bancales dans la base de données Drupal originale qui font qu’il en manque beaucoup. J’ai donc fini par copier import_topics dans une nouvelle fonction import_polls :

    def import_poll_topics
    puts '', "importation des sujets de sondage"

    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: "### Vous pouvez voir les résultats archivés du sondage sur la 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

Je ne me soucie pas trop d’importer les résultats réels du sondage, et cela nécessiterait de re-coder tout l’algorithme que Drupal utilise pour compter tous les votes et éliminer les doublons. Je veux surtout importer les commentaires de suivi dans le fil du sondage. Mais au cas où quelqu’un voudrait voir les résultats originaux du sondage, j’ai fait en sorte qu’il écrive un lien direct vers le nœud du forum original dans la Wayback Machine.


Donc, le code n’est absolument pas élégant et n’est probablement pas très efficace, mais pour une opération ponctuelle, cela devrait faire l’affaire.

Désolé pour les murs de code, faites-le-moi savoir si cela irrite quelqu’un et je peux les déplacer vers une URL pastebin.

Et c’est comme ça que la plupart d’entre eux se déroulent. Ce sujet est un excellent exemple pour quelqu’un d’autre à suivre (étant donné qu’il commence avec un ensemble de compétences similaire au vôtre).

Avez-vous une estimation du temps que vous avez passé à faire vos personnalisations ?

Félicitations !

Merci Jay ! J’apprécie tes encouragements.

Argh, je préférerais ne pas y penser. :stuck_out_tongue_winking_eye: C’était probablement plus de 15 ou 20 heures après que tu m’aies mis sur la bonne voie avec la requête SQL.

J’aimerais te poser quelques questions à ce sujet si tu as des idées :

Il a fallu environ 70 heures pour faire un essai complet avec des données de production sur un VPS très puissant. J’aimerais que mes utilisateurs interagissent à nouveau le plus rapidement possible, même si l’importation des posts et des messages privés est toujours incomplète. Ou une autre idée alternative à laquelle j’ai pensé serait de désactiver la fonction preprocess_posts, que j’ai également fortement modifiée avec des remplacements supplémentaires par expressions régulières gsub et aussi pour faire passer tous les posts et messages privés par Pandoc avec une ou deux commandes différentes selon que le post original était du balisage Textile ou du HTML pur. Si je désactive toute la routine preprocess_posts, cela réduirait probablement le temps d’importation de moitié, et je pourrais ensuite ajouter tout ce matériel de formatage dans la section postprocess_posts une fois que toutes les données brutes sont importées. Mais l’inconvénient est qu’après coup, je ne pourrais pas facilement accéder à la colonne de la base de données d’origine qui indique le format source (Textile ou HTML) pour chaque post, ce qui est une condition pour ma manipulation Pandoc. Ou pourrais-je ajouter un champ personnalisé à chaque post le labellisant comme textile ou html et le récupérer plus tard lors du post-traitement ? Je ne sais pas, je réfléchis à voix haute.

Lorsque vous exécutez à nouveau le script d’importation avec uniquement les nouvelles données, il sera beaucoup plus rapide car il n’importera pas les données à nouveau. Cela ne prendra donc que quelques heures. Et chaque exécution ultérieure sera plus rapide car il y aura moins de données à importer.

Vous pouvez ensuite accélérer cela en modifiant les requêtes pour qu’elles ne retournent que les données plus récentes qu’une certaine heure. La plupart des scripts que j’ai touchés ont un paramètre import_after à cet effet (mais aussi pour permettre un développement plus rapide en important un petit sous-ensemble des données).