Große Drupal-Forum-Migration, Importfehler und Einschränkungen

Hallo, dieses Thema gibt einige Hintergrundinformationen zur Migration, die ich langsam plane und teste. Ich habe am vergangenen Freitag endlich den Drupal-Importer auf einem Test-VPS mit einer Kombination aus diesem und diesem ausprobiert. Der Importer läuft noch, während ich das hier tippe, daher konnte ich die Funktionalität der Testseite noch nicht wirklich testen, aber sie wird bald fertig sein.

Das größte Problem, mit dem ich konfrontiert bin, ist ein “duplicate key value” bei 8 scheinbar zufälligen Nodes (das Äquivalent von Themen in Discourse) von insgesamt ca. 80.000 Nodes. Hier sind die spezifischen nid-Nummern, falls es sich um einen seltsamen Y2K-ähnlichen mathematischen Fehler handelt:

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

derselbe Fehler tritt immer bei denselben nids auf, wenn der Importer erneut ausgeführt wird:

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)

Der einzige Weg, wie ich ihn weitermachen konnte, war durch Hacking der SQL-Bedingungen:

...
	 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};
...

Ich habe den ersten fehlgeschlagenen Node sowie die vorherigen und nächsten nids auf beiden Seiten im Quell-Drupal-Datenbank untersucht und kann nichts Falsches erkennen. Der nid ist als Primärschlüssel gesetzt und hat AUTO_INCREMENT, und die ursprüngliche Drupal-Seite funktioniert einwandfrei, sodass es kein grundlegendes Problem mit der Integrität der Quelldatenbank geben kann.


Abgesehen von dem oben genannten Fehler sind dies die Einschränkungen, die ich mit dem Skript habe:

  1. Permalinks: Es scheint, dass das Importer-Skript Permalinks für die ehemaligen Node-URLs example.com/node/XXXXXXX erstellt. Ich muss aber auch Links zu bestimmten Kommentaren innerhalb dieser Nodes beibehalten, die das Format haben: example.com/comment/YYYYYYY#comment-YYYYYYY (YYYYYYY ist in beiden Fällen gleich). Das Drupal-URL-Schema enthält nicht die Node-ID, mit der der Kommentar verknüpft ist, während Discourse dies tut (example.com/t/topic-keywords/XXXXXXX/YY), was eine große Komplikation darstellt.

  2. Benutzername-Beschränkungen: Drupal erlaubt Leerzeichen in Benutzernamen. Ich verstehe, dass Discourse dies nicht tut, zumindest erlaubt es neuen Benutzern nicht, sie auf diese Weise zu erstellen. Dieser Beitrag deutet darauf hin, dass das Importer-Skript die problematischen Benutzernamen automatisch “konvertiert”, aber ich kann keinen Code dafür in /import_scripts/drupal.rb finden. Update: Tatsächlich scheint es, dass Discourse dies automatisch und korrekt gehandhabt hat.

  3. Gesperrte Benutzer: Es scheint, dass das Skript alle Benutzer importiert, einschließlich gesperrter Konten. Ich könnte wahrscheinlich recht einfach eine Bedingung zur SQL-Auswahl WHERE status = 1 hinzufügen, um nur aktive Benutzerkonten zu importieren, aber ich bin mir nicht sicher, ob dies Probleme mit der Serialisierung der Datensätze verursachen würde. Vor allem möchte ich diese zuvor gesperrten Kontonamen mit ihren zugehörigen E-Mail-Adressen dauerhaft blockiert halten, damit dieselben problematischen Benutzer sich nicht erneut auf Discourse anmelden.

  4. Benutzerprofilfelder: Weiß jemand, ob es in einem anderen Importer Beispielcode zum Importieren von persönlichen Informationsfeldern aus Benutzerprofilen gibt? Ich habe nur ein Profilfeld (“Standort”), das ich importieren muss.

  5. Avatare (nicht Gravatars): Es erscheint etwas seltsam, dass im Drupal-Importer Code zum Importieren von Gravataren vorhanden ist, aber nicht für die weitaus häufiger verwendeten lokalen Avatarbilder.

  6. Private Nachrichten: Fast alle Drupal 7-Foren werden wahrscheinlich das Drittanbieter-Modul privatemsg verwenden (es gibt keine offizielle Drupal PM-Funktionalität). Der Importer unterstützt den Import von PMs nicht. In meinem Fall muss ich etwa 1,5 Millionen davon importieren.

Vielen Dank im Voraus für Ihre Hilfe und dafür, dass Sie das Drupal-Importer-Skript zur Verfügung gestellt haben.

Diese Reihe von Problemen ist so ziemlich das Übliche für einen großen Import. Wer auch immer dafür geschrieben wurde, hat sich nicht darum gekümmert (vielleicht nicht genug, um es zu bemerken) um die von Ihnen beschriebenen Probleme.

Was wie ein Fehler in Drupal oder der Datenbank selbst klingt (doppelte IDs sollten nicht vorkommen). Ich hätte das Skript wahrscheinlich modifiziert, um auf Duplikate zu testen und/oder Fehler abzufangen, aber Ihre Methode hat funktioniert (es sei denn, es gibt noch mehr).

Sie können sich andere Importskripte ansehen, die Post-Permalinks erstellen. Die import_id befindet sich im PostCustomField jedes Posts.

Er befindet sich entweder in base.rb oder im Benutzervorschlags-Tool. Es funktioniert meistens einfach und man kann nicht viel daran ändern.

Das möchten Sie wahrscheinlich nicht tun. Das Problem ist, dass die von diesen Benutzern erstellten Beiträge dem system gehören. Sie können sich andere Skripte ansehen, um Beispiele dafür zu finden, wie Sie sie deaktivieren können. fluxbb hat ein suspend_users-Skript, das helfen sollte.

fluxbb (an dem ich gerade arbeite) macht das. Sie fügen einfach etwas wie das hier zum Import-Benutzerskript hinzu:

location: user[‘location’],

Gravatars werden vom Discourse-Kern behandelt, daher tut das Skript nichts, um sie zu importieren; es funktioniert einfach. Sie können die anderen Skripte nach “avatar” durchsuchen, um Beispiele dafür zu finden, wie Sie das tun können.

Suchen Sie nach Beispielen. . . . ipboard hat import_private_messages.

Danke für die Antwort. Ich glaube nicht, dass dies ein Problem mit der Drupal-Datenbank ist, da ich die Quelldatenbank inspiziert habe und keine doppelten nid-Schlüssel finden kann.

Ahhh, es hat also diese Funktionalität außerhalb von drupal.rb. Jetzt, wo ich mich auf der Test-Import-Site umschaue, sieht es tatsächlich so aus, als ob die Benutzernamenkonvertierungen sehr gut gehandhabt wurden. Danke!

Was wäre der einfachste Weg, den Import von Unicode-Benutzernamen zu ermöglichen (ohne sie zu konvertieren, d. h. den Benutzernamen Narizón beizubehalten, anstatt ihn in Narizon zu konvertieren)?

Ich habe meinen ersten Test des Drupal-Importeurs auf einer Instanz ohne konfigurierte Web-GUI durchgeführt, daher hatte ich die Discourse-Option zum Zulassen von Unicode-Benutzernamen nicht aktiviert. Wenn diese aktiviert gewesen wäre, hätte der Importeur sie dann respektiert? Was ist der empfohlene Weg, dies für meine Produktionsmigration zu aktivieren?

Und in der Zwischenzeit, für meine aktuelle Testinstanz, gibt es einen rake-Befehl, um den vollständigen Namen auf den Benutzernamen anzuwenden? (Ich habe prioritize username in ux bereits aktiviert, aber da meine Testbenutzer an Drupal gewöhnt sind, das nur Benutzernamen für die Anmeldung unterstützt [nicht die E-Mail-Adresse], denke ich, es wäre am besten, ihre Produktionsbenutzernamen beizubehalten, die zumindest im Feld fullname beibehalten wurden.)

Wahrscheinlich?

Sie können die Website-Einstellung zu Beginn des Skripts festlegen.

Ich halte es für eine schlechte Idee, Benutzernamen zu ändern, aber wenn sie Ihnen nicht gefallen, könnten Sie ändern, was an den Benutzernamen-Generator übergeben wird.

Danke, meinst du, sie nach Abschluss des Imports zu ändern?

Ich glaube, ich meine, sie überhaupt zu ändern, es sei denn, im alten System waren Benutzernamen unsichtbar und sie sahen nur echte Namen.

Wenn letzteres der Fall ist, würde ich das Skript ändern, um den Benutzernamen zu ihrem echten Namen zu machen. Das Problem dabei ist, dass sie ihr Konto nicht finden können, wenn sie ihre E-Mail-Adresse nicht kennen.

Verstanden. Im Drupal-Forum gibt es nur System-Benutzernamen und keine separaten echten Namen. Und zusätzlich erlaubt Drupal kein Einloggen mit der E-Mail-Adresse, nur mit dem Benutzernamen. Deshalb ist es in meinem Fall sehr wichtig, die Benutzernamen so weit wie möglich beizubehalten. (Es wird immer noch einige konvertierte Benutzernamen geben, wie z. B. solche mit Leerzeichen.) Ich muss also untersuchen, wie die Discourse-Einstellungen zu Beginn des Import-Skripts festgelegt werden.

Aber Discourse tut das, also wenn sie ihre E-Mail-Adresse kennen, können sie diese zum Zurücksetzen des Passworts verwenden, was wahrscheinlich das ist, was Sie allen sagen sollten, da Sie nicht erraten können, wer seinen Benutzernamen nicht erraten kann, nehme ich an.

Ich denke, ich würde SiteSetting.unicode_username=true im Import-Skript setzen und es erneut ausführen, um zu sehen, ob es funktioniert. Sie könnten es in der Rails-Konsole testen, um zu sehen. Dies könnte Ihnen sagen:

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

Nun, ich denke, das würde nicht die Benutzernamen-Erzeuger-Sache aufrufen, also müssen Sie sie aufrufen

  UserNameSuggester.suggest("Narizón")

Nein. Das bringt Ihnen immer noch keinen Unicode-Benutzernamen. Sie müssen den UserNameSuggester finden und ihn anpassen, nehme ich an.

Aber wenn Sie die Benutzernamen wirklich ändern wollen, ist es vielleicht besser, sie jetzt zu ändern, anstatt das Skript zu reparieren. Sie müssen sicherstellen, dass die Art und Weise, wie Sie es tun, den Benutzernamen in allen Beiträgen aktualisiert. Wenn Sie eine Rake-Aufgabe verwenden, wird dies definitiv geschehen.

Ausgezeichnet, vielen Dank, Jay! Ich werde das beim nächsten Mal ausprobieren, wenn ich den Importer ausführe.

Ich glaube nicht, dass Sie sich die Mühe machen sollten:

Das ist in lib/user_name_suggester.rb, aber vielleicht möchten Sie User.normalize_username

Sicher, Sie hatten Recht. Es ist nicht einmal ein Fehler an sich, es stellte sich heraus, dass es eine seltsame Art und Weise war, wie Drupal verschobene Themen behandelt, während es eine Brotkrümelspur in der ehemaligen Themenkategorie hinterlässt. Es erstellt einfach eine doppelte Zeile in einer der vielen Tabellen, die alle zusammengezogen werden, um das zu werden, was schließlich ein vollständiges Drupal-Thema wird. Es sieht also so aus, als müsste ich herausfinden, wie ich DISTINCT nur auf eine der ausgewählten Tabellen anwenden kann…

Ja. Es ist erstaunlich, wie jeder Import ein Einzelstück ist und wie Ihres das erste Forum ist, das dieses Problem hatte (natürlich haben viele Leute das Problem vielleicht gelöst und es nicht geschafft, einen PR mit dem Update einzureichen). Oder vielleicht haben sie die Fehler ignoriert?

Aha. Ich vermute, es ist keine sehr gebräuchliche Funktion. Wenn ein Thread in eine neue Kategorie verschoben wird, gibt es ein optionales Kontrollkästchen, um einen „Verschoben nach…“-Link in der alten Kategorie zu hinterlassen.

Der fehlerhafte Duplikat befindet sich in der Spalte nid aus forum_index. Es sieht also so aus, als könnte ich es mit GROUP BY nid beheben, richtig?

        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

Es sieht vielversprechend aus, denn wenn ich die Abfrage mit GROUP BY nid ausführe, gibt es 8 Zeilen weniger.

Das könnte funktionieren. Ich würde denken, dass es in dieser Tabelle einen Wert gäbe, der besagt, dass sie verschoben wurde, und Sie könnten nur diejenigen auswählen, die diesen Wert nicht haben.

Das wäre definitiv die logischste Art, es zu gestalten. Ich schätze, das ist eine Drupal-Sache…

Das Einzige, was es tut, ist, die tid (Kategorie-ID) zu ändern. Das folgt dem Stil, den ich während dieser Tortur mit der Drupal-Datenbank gelernt habe. Ich weiß nichts über DB-Design, aber ich habe den Eindruck, dass man entweder Daten explizit speichern kann oder einige Dinge implizit lassen und sie dann durch programmatische Logik herausfinden kann; Drupal scheint eindeutig in die letztere Kategorie zu fallen.

Nun, es sieht so aus, als wäre ich fast am Ziel. Vielen Dank an Jay für die Anleitung.

Danke, das war der Schlüssel. Tatsächlich war es so einfach, den Permalink-Teil des Drupal-Importskripts selbst zu kopieren und anzupassen, damit er auf Beiträge statt auf Themen ausgeführt wird:

    ## Ich habe Permalinks für jeden Drupal-Kommentar-(Antwort-)Link hinzugefügt: /comment/ZIFFERN#comment-ZIFFERN
    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}" # Der Teil #comment-ZIFFERN bricht den Permalink und ist nicht erforderlich
          Permalink.create(url: slug, post_id: post.id)
        end
      rescue => e
        puts e.message
        puts "Erstellung des Permalinks fehlgeschlagen für cid #{post.id}"
      end
    end

Ich habe mich eine Weile mit meinem ursprünglichen Versuch festgefahren, der den relativen Seiten-Teil #comment-ZIFFERN des ursprünglichen Drupal-Links enthielt, was den Permalink in Discourse komplett zerstört. Dann wurde mir klar, dass der #-Teil eines Links natürlich nicht an den Webserver übergeben wird und nur für Drupal benötigt wurde, um zur Stelle auf der Seite zu scrollen, an der sich der spezifische Kommentar befand. Es funktioniert also in Discourse auch ohne diesen Teil einwandfrei, selbst wenn man von einer externen Webseite mit einem alten /comment/JJJJJJ#comment-JJJJJJ-Link kommt. Es sieht in Discourse einfach so aus: /comment/JJJJJJ/t/themen-titel-worte/123456/X, und die Adressleiste zeigt so etwas an: /t/themen-titel-worte/123456/X#comment-JJJJJJJ. Es scheint sich nicht um den falschen Teil #comment-JJJJJJJ zu kümmern.

Bei einigen Foren vermute ich, dass die Standardfunktion postprocess_posts des Drupal-Importers eigentlich ausreichen könnte. Es ist jedoch anzumerken, dass sie für jedes Forum angepasst werden muss; es gibt eine ziemlich schlampige, hartkodierte reguläre Ausdruck-Ersetzung von site.comcommunity.site.com. Nach Anpassung leistet sie jedoch gute Arbeit beim Um schreiben interner Foren-Links von Knoten → Themen sowie von Kommentaren → Antworten. Allerdings habe ich eine beträchtliche Anzahl externer Webseiten, die auf einzelne Kommentare (Antworten) in meinem Forum verlinken, und es lohnt sich, diese zu erhalten. Außerdem indiziert Google die meisten der 1,7 Millionen /comment-JJJJJJJ-URLs, und es würde wahrscheinlich mein Ranking schaden, wenn alle diese verschwinden würden. Ich hoffe nur, dass es für Discourse keine Probleme verursacht, ~2 Millionen Permalinks zu haben?


Vielen Dank, ich habe diese Funktion fast ohne Änderungen übernommen, musste nur ein paar Spaltennamen anpassen. Funktioniert großartig.

  def suspend_users
    puts '', "Aktualisierung verbotener Benutzer"

    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, "während des initialen Imports gesperrt")
          banned += 1
        else
          puts "Fehler beim Sperren des Benutzers #{user.username}. #{user.errors.try(:full_messages).try(:inspect)}"
          failed += 1
        end
      else
        puts "Nicht gefunden: #{b['email']}"
        failed += 1
      end

      print_status banned + failed, total
    end
  end

Hat auch funktioniert! Ich musste mich zwar mit dem diffusen DB-Schema von Drupal und LEFT JOIN profile_value location ON users.uid = location.uid herumschlagen, um eine weitere Tabelle mit Profildaten zu korrelieren, aber es ist sehr cool, dass es auf der Discourse-Seite so einfach hinzuzufügen ist. Es ist erwähnenswert, dass dieser Prozess etwa 50 % langsamer läuft als der Standard; ich vermute, das liegt an dem LEFT JOIN. Aber das kann ich in Kauf nehmen, da ich nur etwa 80.000 Benutzer habe.


Das war ziemlich schwierig, wieder einmal aufgrund des zersplitterten Datenbankschemas von Drupal. Ich habe schließlich jforum.rb als Basis verwendet, mit etwas Hilfe vom Vanilla-Importer. Das ursprüngliche Skript war ziemlich paranoid und prüfte bei jedem einzelnen Variablen-Durchlauf, ob der Dateiname des Avatars nicht null ist, daher habe ich die meisten dieser Prüfungen entfernt, um den Code weniger unübersichtlich zu machen. Das Schlimmste, was passieren kann, ist, dass das Skript abstürzt, aber mit der von mir verwendeten SQL-Abfrage glaube ich nicht einmal, dass das passieren könnte.

  def import_users
    puts "", "Importieren der Benutzer"

    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

Nach Ihrer bezahlten Hilfe bei der SQL-Abfrage habe ich es schließlich versucht, es in die Skripte für Discuz, IPboard und Xenforo zu hacken. Bei jedem davon bin ich immer wieder an Sackgassen gescheitert. Am nächsten war ich dem Discuz-Modell, das ein sehr ähnliches Datenbankschema zu haben scheint, aber ich kam nicht über einen Fehler mit der Instanzvariable @first_post_id_by_topic_id hinaus. Nach unzähligen Versuchen und Irrtümern wurde mir schließlich klar, dass sie zu Beginn des Discuz-Skripts falsch initialisiert war (ich habe versucht, sie am selben Ort im Drupal-Skript zu platzieren), und dies hat es endlich behoben:

  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 '', 'Erstellen privater Nachrichten'

	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'])
				# Titel aus der Listentabelle finden
				#          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

          # Benutzer finden, die Teil dieser privaten Nachricht sind.
          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? # pn mit sich selbst?
            skip = true
            puts "Überspringen von pn:#{m['id']} wegen fehlendem Ziel"
          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 "Elternbeitrag pn-Thread:#{thread_id} existiert nicht. Überspringen #{m["id"]}: #{m["message"][0..40]}"
            skip = true
          end
        end
        skip ? nil : mapped
      end

    end
end
# Suche nach der ersten pn-ID für die Serie von pn
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

Ach ja, und für die meisten dieser Abfragen muss auch im MySQL-Container dies ausgeführt werden, um eine strenge SQL-Sicherheitsprüfung zu deaktivieren:
mysql -u root -ppass -e "SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''));"


Eine weitere Sache, die mir fehlte, waren einige tausend Drupal-Knoten vom Typ poll (Umfrage). Ich habe zunächst versucht, einfach WHERE type = 'forum' OR type = 'poll' in die Funktion import_topics aufzunehmen, aber es gibt einige wirklich krumme Dinge im ursprünglichen Drupal-Datenbank, die dazu führen, dass viele davon übersehen werden. Also habe ich am Ende import_topics in eine neue Funktion import_polls kopiert:

    def import_poll_topics
    puts '', "Importieren von Umfragethemen"

    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: "### Sie können die archivierten Umfrageergebnisse auf der Wayback Machine sehen:\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

Es ist mir nicht so wichtig, die eigentlichen Umfrageergebnisse zu importieren, und das würde eine Neukodierung des gesamten Algorithmus erfordern, den Drupal verwendet, um alle Stimmen zu zählen und Duplikate zu eliminieren. Ich möchte hauptsächlich die Folgekommentare im Umfragethema importieren. Aber falls jemand die ursprünglichen Umfrageergebnisse sehen möchte, habe ich es so eingerichtet, dass es einen direkten Link zum ursprünglichen Forenknoten in der Wayback Machine ausgibt.


Der Code ist also keineswegs elegant und wahrscheinlich auch nicht sehr effizient, aber für eine einmalige Aktion sollte er die Aufgabe erledigen.

Entschuldigen Sie die Code-Wände; lassen Sie mich wissen, wenn das jemanden stört, und ich kann sie auf eine Pastebin-URL verschieben.

Und so laufen die meisten davon ab. Dieses Thema ist ein großartiges Beispiel, dem jemand anderes folgen kann (vorausgesetzt, er beginnt mit einem ähnlichen Fähigkeitsset wie deines).

Hast du eine Schätzung, wie viel Zeit du mit deinen Anpassungen verbracht hast?

Glückwunsch!

Danke Jay! Ich schätze die Ermutigung.

Ugh, das möchte ich lieber nicht bedenken. :stuck_out_tongue_winking_eye: Es waren wahrscheinlich über 15 oder 20 Stunden, nachdem Sie mich mit der SQL-Abfrage auf den richtigen Weg gebracht hatten.

Ich würde gerne Ihre Meinung dazu hören, wenn Sie welche haben:

Es dauerte ungefähr 70 Stunden, um einen vollständigen Testlauf mit Produktionsdaten auf einem sehr leistungsstarken VPS durchzuführen. Ich möchte, dass meine Benutzer so schnell wie möglich wieder interagieren, auch wenn der Import von Beiträgen und privaten Nachrichten noch unvollständig ist. Oder eine andere alternative Idee, über die ich nachgedacht habe, wäre, die Funktion preprocess_posts zu deaktivieren, die ich ebenfalls stark mit zusätzlichen gsub-RegExp-Ersetzungen modifiziert habe und um alle Beiträge und privaten Nachrichten durch Pandoc zu leiten, mit einem von zwei verschiedenen Befehlen, je nachdem, ob der ursprüngliche Beitrag Textil-Markup oder reines HTML war. Wenn ich die gesamte preprocess_posts-Routine deaktiviere, würde dies die Importzeit wahrscheinlich fast halbieren, und dann könnte ich all diese Formatierungsarbeiten in den Abschnitt postprocess_posts aufnehmen, sobald alle Rohdaten importiert sind. Der Nachteil ist jedoch, dass ich später nicht mehr einfach auf die ursprüngliche Datenbankspalte zugreifen könnte, die das Quellformat (Textile oder HTML) für jeden Beitrag anzeigt, was eine Bedingung für meine Pandoc-Manipulation ist. Oder könnte ich jedem Beitrag ein benutzerdefiniertes Feld hinzufügen, das ihn als textile oder html kennzeichnet und diesen dann später während der Nachbearbeitung abruft? Keine Ahnung, ich denke nur laut nach.

Wenn Sie das Importskript erneut nur mit den neuen Daten ausführen, wird es viel schneller laufen, da die Daten nicht erneut importiert werden. Es wird also nur wenige Stunden dauern. Und jeder nachfolgende Lauf wird schneller sein, da weniger Daten importiert werden müssen.

Sie können dies beschleunigen, indem Sie die Abfragen so ändern, dass nur Daten zurückgegeben werden, die neuer als eine bestimmte Zeit sind. Die meisten Skripte, die ich bearbeitet habe, verfügen über eine import_after-Einstellung genau zu diesem Zweck (aber auch, um eine schnellere Entwicklung durch den Import einer kleinen Teilmenge der Daten zu ermöglichen).