Add /plan db migrate_to_online_uuids command

This commit is contained in:
Aurora Lahtela 2022-08-21 12:19:52 +03:00
parent 2f6718a78f
commit 7b41165af5
13 changed files with 316 additions and 26 deletions

View File

@ -58,6 +58,7 @@ dependencies {
shadow "org.eclipse.jetty:jetty-server:$jettyVersion"
shadow "org.eclipse.jetty:jetty-alpn-java-server:$jettyVersion"
shadow "org.eclipse.jetty.http2:http2-server:$jettyVersion"
shadow 'com.googlecode.json-simple:json-simple:1.1.1' // json simple used by UUIDFetcher
// Swagger annotations
implementation "jakarta.ws.rs:jakarta.ws.rs-api:3.1.0"

View File

@ -333,15 +333,27 @@ public class PlanCommand {
.subcommand(removeCommand())
.subcommand(uninstalledCommand())
.subcommand(removeJoinAddressesCommand())
.subcommand(onlineUuidMigration())
.requirePermission(Permissions.DATA_BASE)
.description(locale.getString(HelpLang.DB))
.inDepthDescription(locale.getString(DeepHelpLang.DB))
.build();
}
private Subcommand onlineUuidMigration() {
return Subcommand.builder()
.aliases("migrate_to_online_uuids", "migratetoonlineuuids")
.requirePermission(Permissions.DATA_CLEAR)
.optionalArgument("--remove_offline", "Remove offline players if given")
.description(locale.getString(HelpLang.ONLINE_UUID_MIGRATION))
.inDepthDescription("Moves and combines offline uuid data to online uuids where possible. Leaves offline-only players to database.")
.onCommand((sender, arguments) -> databaseCommands.onOnlineConversion(commandName, sender, arguments))
.build();
}
private Subcommand removeJoinAddressesCommand() {
return Subcommand.builder()
.aliases("removejoinaddresses")
.aliases("remove_join_addresses", "removejoinaddresses")
.requirePermission(Permissions.DATA_CLEAR)
.requiredArgument(locale.getString(HelpLang.ARG_SERVER), locale.getString(HelpLang.DESC_ARG_SERVER_IDENTIFIER))
.description(locale.getString(HelpLang.JOIN_ADDRESS_REMOVAL))

View File

@ -39,7 +39,7 @@ public class Confirmation {
) {
this.locale = locale;
awaiting = Caffeine.newBuilder()
.expireAfterWrite(90, TimeUnit.SECONDS)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}

View File

@ -19,13 +19,16 @@ package com.djrapitops.plan.commands.subcommands;
import com.djrapitops.plan.commands.use.Arguments;
import com.djrapitops.plan.commands.use.CMDSender;
import com.djrapitops.plan.commands.use.ColorScheme;
import com.djrapitops.plan.commands.use.MessageBuilder;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.exceptions.database.DBOpException;
import com.djrapitops.plan.gathering.domain.BaseUser;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.processing.Processing;
import com.djrapitops.plan.query.QuerySvc;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.DatabaseSettings;
@ -36,23 +39,24 @@ import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.DBType;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.SQLiteDB;
import com.djrapitops.plan.storage.database.queries.objects.BaseUserQueries;
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
import com.djrapitops.plan.storage.database.transactions.BackupCopyTransaction;
import com.djrapitops.plan.storage.database.transactions.commands.RemoveEverythingTransaction;
import com.djrapitops.plan.storage.database.transactions.commands.RemovePlayerTransaction;
import com.djrapitops.plan.storage.database.transactions.commands.SetServerAsUninstalledTransaction;
import com.djrapitops.plan.storage.database.transactions.Transaction;
import com.djrapitops.plan.storage.database.transactions.commands.*;
import com.djrapitops.plan.storage.database.transactions.patches.BadFabricJoinAddressValuePatch;
import com.djrapitops.plan.storage.file.PlanFiles;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import net.playeranalytics.plugin.player.UUIDFetcher;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@Singleton
public class DatabaseCommands {
@ -69,8 +73,10 @@ public class DatabaseCommands {
private final Identifiers identifiers;
private final PluginStatusCommands statusCommands;
private final ErrorLogger errorLogger;
private final Processing processing;
private final Formatter<Long> timestamp;
private final Formatter<Long> clock;
@Inject
public DatabaseCommands(
@ -86,7 +92,8 @@ public class DatabaseCommands {
Formatters formatters,
Identifiers identifiers,
PluginStatusCommands statusCommands,
ErrorLogger errorLogger
ErrorLogger errorLogger,
Processing processing
) {
this.locale = locale;
this.confirmation = confirmation;
@ -102,6 +109,8 @@ public class DatabaseCommands {
this.errorLogger = errorLogger;
this.timestamp = formatters.iso8601NoClockLong();
clock = formatters.clockLong();
this.processing = processing;
}
public void onBackup(CMDSender sender, Arguments arguments) {
@ -472,4 +481,102 @@ public class DatabaseCommands {
}
statusCommands.onReload(sender);
}
public void onOnlineConversion(String mainCommand, CMDSender sender, Arguments arguments) {
boolean removeOfflinePlayers = arguments.get(0)
.map("--remove_offline"::equals)
.orElse(false);
sender.send(locale.getString(CommandLang.PROGRESS_PREPARING));
processing.submitNonCritical(() -> {
Map<UUID, BaseUser> baseUsersByUUID = dbSystem.getDatabase().query(BaseUserQueries.fetchAllBaseUsersByUUID());
List<String> playerNames = baseUsersByUUID.values().stream().map(BaseUser::getName).collect(Collectors.toList());
sender.send("Performing lookup for " + playerNames.size() + " uuids from Mojang..");
sender.send("Preparation estimated complete at: " + clock.apply(System.currentTimeMillis() + playerNames.size() * 100) + " (due to request rate limiting)");
Map<String, UUID> onlineUUIDsOfPlayers = getUUIDViaUUIDFetcher(playerNames);
if (onlineUUIDsOfPlayers.isEmpty()) {
sender.send(locale.getString(CommandLang.PROGRESS_FAIL, "Did not get any UUIDs from Mojang."));
return;
}
int totalProfiles = baseUsersByUUID.size();
int offlineOnlyUsers = 0;
int combine = 0;
int move = 0;
List<Transaction> transactions = new ArrayList<>();
for (BaseUser user : baseUsersByUUID.values()) {
String playerName = user.getName();
UUID recordedUUID = user.getUuid();
UUID actualUUID = onlineUUIDsOfPlayers.get(playerName);
if (actualUUID == null) {
offlineOnlyUsers++;
if (removeOfflinePlayers) transactions.add(new RemovePlayerTransaction(recordedUUID));
continue;
}
if (recordedUUID == actualUUID) {
continue;
}
BaseUser alreadyExistingProfile = baseUsersByUUID.get(actualUUID);
if (alreadyExistingProfile == null) {
move++;
transactions.add(new ChangeUserUUIDTransaction(recordedUUID, actualUUID));
} else {
combine++;
transactions.add(new CombineUserTransaction(recordedUUID, actualUUID));
}
}
MessageBuilder messageBuilder = sender.buildMessage()
.addPart(colors.getMainColor() + "Moving to online-only UUIDs (irreversible):").newLine()
.addPart(colors.getSecondaryColor() + " Total players in database: " + totalProfiles).newLine()
.addPart(colors.getSecondaryColor() + (removeOfflinePlayers ? "Removing (no online UUID): " : " Offline only (no online UUID): ") + offlineOnlyUsers).newLine()
.addPart(colors.getSecondaryColor() + " Moving to new UUID: " + move).newLine()
.addPart(colors.getSecondaryColor() + " Combining offline and online profiles: " + combine).newLine()
.newLine()
.addPart(colors.getSecondaryColor() + " Estimated online UUID players in database after: " + (totalProfiles - combine - offlineOnlyUsers) + (removeOfflinePlayers ? "" : " (+" + offlineOnlyUsers + " offline)")).newLine()
.addPart(colors.getTertiaryColor() + locale.getString(CommandLang.CONFIRM));
if (sender.supportsChatEvents()) {
messageBuilder
.addPart("§2§l[\u2714]").command("/" + mainCommand + " accept").hover(locale.getString(CommandLang.CONFIRM_ACCEPT))
.addPart(" ")
.addPart("§4§l[\u2718]").command("/" + mainCommand + " cancel").hover(locale.getString(CommandLang.CONFIRM_DENY))
.send();
} else {
messageBuilder
.addPart(colors.getTertiaryColor() + locale.getString(CommandLang.CONFIRM)).addPart("§a/" + mainCommand + " accept")
.addPart(" ")
.addPart("§c/" + mainCommand + " cancel")
.send();
}
confirmation.confirm(sender, choice -> {
if (Boolean.TRUE.equals(choice)) {
transactions.forEach(dbSystem.getDatabase()::executeTransaction);
dbSystem.getDatabase().executeTransaction(new Transaction() {
@Override
protected void performOperations() {
sender.send(locale.getString(CommandLang.PROGRESS_SUCCESS));
}
});
} else {
sender.send(colors.getMainColor() + locale.getString(CommandLang.CONFIRM_CANCELLED_DATA));
}
});
});
}
private Map<String, UUID> getUUIDViaUUIDFetcher(List<String> playerNames) {
try {
return new UUIDFetcher(playerNames).call();
} catch (Exception | NoClassDefFoundError failure) {
errorLogger.error(failure, ErrorContext.builder()
.related("Migrating offline uuids to online uuids")
.build());
return new HashMap<>();
}
}
}

View File

@ -118,6 +118,7 @@ public enum CommandLang implements Lang {
HOTSWAP_REMINDER("command.database.manage.hotswap", "Manage - Remind HotSwap", "§eRemember to swap to the new database (/plan db hotswap ${0}) & reload the plugin."),
PROGRESS_START("command.database.manage.start", "Manage - Start", "> §2Processing data.."),
PROGRESS("command.database.manage.progress", "Manage - Progress", "${0} / ${1} processed.."),
PROGRESS_PREPARING("command.database.manage.preparing", "Manage - preparing", "Preparing.."),
PROGRESS_SUCCESS("command.database.manage.success", "Manage - Success", "> §aSuccess!"),
PROGRESS_FAIL("command.database.manage.fail", "Manage - Fail", "> §cSomething went wrong: ${0}"),
CONFIRMATION("command.database.manage.confirm", "Manage - Fail, Confirmation", "> §cAdd '-a' argument to confirm execution: ${0}"),
@ -131,7 +132,8 @@ public enum CommandLang implements Lang {
FAIL_IMPORTER_NOT_FOUND("command.general.failNoImporter", "Manage - Fail No Importer", "§eImporter '${0}' doesn't exist"),
FAIL_EXPORTER_NOT_FOUND("command.general.failNoExporter", "Manage - Fail No Exporter", "§eExporter '${0}' doesn't exist"),
NO_SERVER("command.database.manage.failNoServer", "Manage - Fail No Server", "No server found with given parameters."),
UNINSTALLING_SAME_SERVER("command.database.manage.failSameServer", "Manage - Fail Same server", "Can not mark this server as uninstalled (You are on it)");
UNINSTALLING_SAME_SERVER("command.database.manage.failSameServer", "Manage - Fail Same server", "Can not mark this server as uninstalled (You are on it)"),
;
private final String key;
private final String identifier;
@ -149,7 +151,7 @@ public enum CommandLang implements Lang {
}
@Override
public String getKey() { return key; }
public String getKey() {return key;}
@Override
public String getDefault() {

View File

@ -71,7 +71,8 @@ public enum HelpLang implements Lang {
IMPORT("command.help.import.description", "Command Help - /plan import", "Import data"),
JSON("command.help.json.description", "Command Help - /plan json", "View json of Player's raw data."),
LOGOUT("command.help.logout.description", "Command Help - /plan logout", "Log out other users from the panel."),
JOIN_ADDRESS_REMOVAL("command.help.removejoinaddresses.description", "Command Help - /plan db removejoinaddresses", "Remove join addresses of a specified server");
JOIN_ADDRESS_REMOVAL("command.help.removejoinaddresses.description", "Command Help - /plan db removejoinaddresses", "Remove join addresses of a specified server"),
ONLINE_UUID_MIGRATION("command.help.migrateToOnlineUuids.description", "Command Help - /plan db migratetoonlineuuids", "Migrate offline uuid data to online uuids");
private final String identifier;
private final String key;

View File

@ -26,10 +26,7 @@ import org.apache.commons.text.TextStringBuilder;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.*;
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
@ -57,6 +54,15 @@ public class BaseUserQueries {
return db -> db.queryList(sql, BaseUserQueries::extractBaseUser);
}
public static Query<Map<UUID, BaseUser>> fetchAllBaseUsersByUUID() {
String sql = Select.all(UsersTable.TABLE_NAME).toString();
return db -> db.queryMap(sql, (results, map) -> {
BaseUser baseUser = extractBaseUser(results);
map.put(baseUser.getUuid(), baseUser);
}, HashMap::new);
}
private static BaseUser extractBaseUser(ResultSet set) throws SQLException {
UUID playerUUID = UUID.fromString(set.getString(UsersTable.USER_UUID));
String name = set.getString(UsersTable.USER_NAME);

View File

@ -25,6 +25,8 @@ import com.djrapitops.plan.storage.database.SQLDB;
import com.djrapitops.plan.storage.database.queries.Query;
import com.djrapitops.plan.storage.database.queries.QueryAPIQuery;
import com.djrapitops.plan.storage.database.queries.QueryStatement;
import com.djrapitops.plan.storage.database.queries.schema.MySQLSchemaQueries;
import com.djrapitops.plan.storage.database.queries.schema.SQLiteSchemaQueries;
import com.djrapitops.plan.storage.database.transactions.patches.Patch;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import net.playeranalytics.plugin.scheduling.TimeAmount;
@ -274,4 +276,15 @@ public abstract class Transaction {
String simpleName = getClass().getSimpleName();
return simpleName.isEmpty() ? getClass().getName() : simpleName;
}
protected boolean hasTable(String tableName) {
switch (dbType) {
case SQLITE:
return query(SQLiteSchemaQueries.doesTableExist(tableName));
case MYSQL:
return query(MySQLSchemaQueries.doesTableExist(tableName));
default:
throw new IllegalStateException("Unsupported Database Type: " + dbType.getName());
}
}
}

View File

@ -0,0 +1,68 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.storage.database.transactions.commands;
import com.djrapitops.plan.storage.database.sql.tables.*;
import com.djrapitops.plan.storage.database.transactions.ExecStatement;
import com.djrapitops.plan.storage.database.transactions.Executable;
import com.djrapitops.plan.storage.database.transactions.Transaction;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.UUID;
import static com.djrapitops.plan.storage.database.sql.building.Sql.WHERE;
/**
* Intends to correct UUID of a user.
*
* @author AuroraLS3
*/
public class ChangeUserUUIDTransaction extends Transaction {
protected final UUID oldUUID;
protected final UUID newUUID;
public ChangeUserUUIDTransaction(UUID oldUUID, UUID newUUID) {
this.oldUUID = oldUUID;
this.newUUID = newUUID;
}
@Override
protected void performOperations() {
execute(updateUUID(ExtensionGroupsTable.TABLE_NAME, ExtensionGroupsTable.USER_UUID));
execute(updateUUID(ExtensionPlayerTableValueTable.TABLE_NAME, ExtensionPlayerTableValueTable.USER_UUID));
execute(updateUUID(NicknamesTable.TABLE_NAME, NicknamesTable.USER_UUID));
execute(updateUUID(UsersTable.TABLE_NAME, UsersTable.USER_UUID));
execute(updateUUID(KillsTable.TABLE_NAME, KillsTable.VICTIM_UUID));
execute(updateUUID(KillsTable.TABLE_NAME, KillsTable.KILLER_UUID));
if (hasTable("plan_platforms")) execute(updateUUID("plan_platforms", "uuid"));
if (hasTable("plan_tebex_payments")) execute(updateUUID("plan_tebex_payments", "uuid"));
if (hasTable("plan_version_protocol")) execute(updateUUID("plan_version_protocol", "uuid"));
}
private Executable updateUUID(String tableName, String columnName) {
return new ExecStatement("UPDATE " + tableName + " SET " + columnName + "=?" + WHERE + columnName + "=?") {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setString(1, newUUID.toString());
statement.setString(2, oldUUID.toString());
}
};
}
}

View File

@ -0,0 +1,87 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.storage.database.transactions.commands;
import com.djrapitops.plan.storage.database.queries.objects.BaseUserQueries;
import com.djrapitops.plan.storage.database.sql.tables.*;
import com.djrapitops.plan.storage.database.transactions.ExecStatement;
import com.djrapitops.plan.storage.database.transactions.Executable;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Optional;
import java.util.UUID;
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
/**
* Intends to correct UUID of a user.
*
* @author AuroraLS3
*/
public class CombineUserTransaction extends ChangeUserUUIDTransaction {
public CombineUserTransaction(UUID oldUUID, UUID newUUID) {
super(oldUUID, newUUID);
}
@Override
protected void performOperations() {
Optional<Integer> foundOldId = query(BaseUserQueries.fetchUserId(oldUUID));
Optional<Integer> foundNewId = query(BaseUserQueries.fetchUserId(newUUID));
if (foundOldId.isEmpty() || foundNewId.isEmpty()) return;
Integer oldId = foundOldId.get();
Integer newId = foundNewId.get();
execute(updateUserId(GeoInfoTable.TABLE_NAME, GeoInfoTable.USER_ID, oldId, newId));
execute(updateUserId(PingTable.TABLE_NAME, PingTable.USER_ID, oldId, newId));
execute(updateUserId(SessionsTable.TABLE_NAME, SessionsTable.USER_ID, oldId, newId));
execute(updateUserId(WorldTimesTable.TABLE_NAME, WorldTimesTable.USER_ID, oldId, newId));
execute(updateUserInfo(newId, oldId));
execute(DELETE_FROM + UserInfoTable.TABLE_NAME + WHERE + UserInfoTable.USER_ID + "=" + oldId);
super.performOperations(); // Change UUID fields to match where user_id is not used
}
private Executable updateUserInfo(Integer newId, Integer oldId) {
String sql = "UPDATE " + UserInfoTable.TABLE_NAME +
" SET " + UserInfoTable.USER_ID + "=?" +
WHERE + UserInfoTable.USER_ID + "=?" +
AND + UserInfoTable.SERVER_ID + " NOT IN (" +
SELECT + UserInfoTable.SERVER_ID + FROM + UserInfoTable.TABLE_NAME + WHERE + UserInfoTable.USER_ID + "=?)";
return new ExecStatement(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setInt(1, newId);
statement.setInt(2, oldId);
statement.setInt(3, newId);
}
};
}
private Executable updateUserId(String tableName, String columnName, Integer oldId, Integer newId) {
return new ExecStatement("UPDATE " + tableName + " SET " + columnName + "=?" + WHERE + columnName + "=?") {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setInt(1, newId);
statement.setInt(2, oldId);
}
};
}
}

View File

@ -72,17 +72,6 @@ public abstract class Patch extends OperationCriticalTransaction {
execute("SET FOREIGN_KEY_CHECKS=0");
}
protected boolean hasTable(String tableName) {
switch (dbType) {
case SQLITE:
return query(SQLiteSchemaQueries.doesTableExist(tableName));
case MYSQL:
return query(MySQLSchemaQueries.doesTableExist(tableName));
default:
throw new IllegalStateException("Unsupported Database Type: " + dbType.getName());
}
}
protected boolean hasColumn(String tableName, String columnName) {
switch (dbType) {
case MYSQL:

View File

@ -103,6 +103,8 @@ shadowJar {
relocate 'org.slf4j', 'plan.org.slf4j'
relocate 'org.json.simple', 'plan.org.json.simple'
mergeServiceFiles()
}

View File

@ -88,6 +88,8 @@ shadowJar {
relocate 'jakarta.servlet', 'plan.jakarta.servlet'
relocate 'javax.servlet', 'plan.javax.servlet'
relocate 'org.json.simple', 'plan.org.json.simple'
destinationDirectory.set(file("$rootDir/builds/"))
archiveBaseName.set('Plan')
archiveClassifier.set('')