discourse/migrations/lib/importer/unique_name_finder.rb
Gerhard Schlager 251cac39af DEV: Adds a basic importer for the IntermediateDB
* It only imports users and emails so far
* It stores mapped IDs and usernames in a SQLite DB. In the future, we might want to copy those into the Discourse DB at the end of a migration.
* The importer is split into steps which can mostly be configured with a simple DSL
* Data that needs to be shared between steps can be stored in an instance of the `SharedData` class
* Steps are automatically sorted via their defined dependencies before they are executed
* Common logic for finding unique names (username, group name) is extracted into a helper class
* If possible, steps try to avoid loading already imported data (via `mapping.ids` table)
* And steps should select the `discourse_id` instead of the `original_id` of mapped IDs via SQL
2025-04-07 17:22:36 +02:00

84 lines
2.5 KiB
Ruby
Vendored

# frozen_string_literal: true
module Migrations::Importer
class UniqueNameFinder
MAX_USERNAME_LENGTH = 60
def initialize(shared_data)
@used_usernames_lower = shared_data.load(:usernames)
@used_group_names_lower = shared_data.load(:group_names)
@last_suffixes = {}
@fallback_username =
UserNameSuggester.sanitize_username(I18n.t("fallback_username")).presence ||
UserNameSuggester::LAST_RESORT_USERNAME
@fallback_group_name = "group"
end
def find_available_username(username, allow_reserved_username: false)
username, username_lower =
find_available_name(
username,
fallback_name: @fallback_username,
max_name_length: MAX_USERNAME_LENGTH,
allow_reserved_username:,
)
@used_usernames_lower.add(username_lower)
username
end
def find_available_group_name(group_name)
group_name, group_name_lower =
find_available_name(group_name, fallback_name: @fallback_group_name)
@used_group_names_lower.add(group_name_lower)
group_name
end
private
def name_available?(name, allow_reserved_username: false)
name_lower = name.downcase
return false if @used_usernames_lower.include?(name_lower)
return false if @used_group_names_lower.include?(name_lower)
return false if !allow_reserved_username && User.reserved_username?(name_lower)
true
end
def find_available_name(
name,
fallback_name:,
max_name_length: nil,
allow_reserved_username: false
)
name = name.unicode_normalize
name = UserNameSuggester.sanitize_username(name)
name = fallback_name.dup if name.blank?
name = UserNameSuggester.truncate(name, max_name_length) if max_name_length
if !name_available?(name, allow_reserved_username:)
# if the name ends with a number, then use an underscore before appending the suffix
suffix_separator = name.match?(/\d$/) ? "_" : ""
suffix = next_suffix(name).to_s
# TODO This needs better logic, because it's possible that the max username length is exceeded
name = +"#{name}#{suffix_separator}#{suffix}"
name.next! until name_available?(name, allow_reserved_username:)
end
[name, name.downcase]
end
def next_suffix(name)
name_lower = name.downcase
@last_suffixes.fetch(name_lower, 0) + 1
end
def store_last_suffix(name)
name_lower = name.downcase
@last_suffixes[$1] = $2.to_i if name_lower =~ /^(.+?)(\d+)$/
end
end
end