Skip to content

Commit

Permalink
Add quests data migration tool
Browse files Browse the repository at this point in the history
  • Loading branch information
LMBishop committed Apr 22, 2022
1 parent 43e6479 commit 894f1c2
Show file tree
Hide file tree
Showing 10 changed files with 356 additions and 1 deletion.
Expand Up @@ -526,7 +526,7 @@ private void generateConfigurations() {
}
}

private void writeResourceToFile(String resource, File file) {
public void writeResourceToFile(String resource, File file) {
try {
file.createNewFile();
try (InputStream in = BukkitQuestsPlugin.class.getClassLoader().getResourceAsStream(resource);
Expand Down
Expand Up @@ -20,6 +20,7 @@ public AdminCommandSwitcher(BukkitQuestsPlugin plugin) {
super.subcommands.put("reload", new AdminReloadCommandHandler(plugin));
super.subcommands.put("items", new AdminItemsCommandHandler(plugin));
super.subcommands.put("config", new AdminConfigCommandHandler(plugin));
super.subcommands.put("migratedata", new AdminMigrateCommandHandler(plugin));
super.subcommands.put("update", new AdminUpdateCommandHandler(plugin));
super.subcommands.put("wiki", new AdminWikiCommandHandler(plugin));
super.subcommands.put("about", new AdminAboutCommandHandler(plugin));
Expand All @@ -37,6 +38,7 @@ public void showHelp(CommandSender sender) {
sender.sendMessage(ChatColor.DARK_GRAY + " * " + ChatColor.RED + "/quests a items [import <id>] " + ChatColor.DARK_GRAY + ": view registered quest items");
sender.sendMessage(ChatColor.DARK_GRAY + " * " + ChatColor.RED + "/quests a reload " + ChatColor.DARK_GRAY + ": reload Quests configuration");
sender.sendMessage(ChatColor.DARK_GRAY + " * " + ChatColor.RED + "/quests a config " + ChatColor.DARK_GRAY + ": see detected problems in config");
sender.sendMessage(ChatColor.DARK_GRAY + " * " + ChatColor.RED + "/quests a migratedata " + ChatColor.DARK_GRAY + ": migrate quests data");
sender.sendMessage(ChatColor.DARK_GRAY + " * " + ChatColor.RED + "/quests a update " + ChatColor.DARK_GRAY + ": check for updates");
sender.sendMessage(ChatColor.DARK_GRAY + " * " + ChatColor.RED + "/quests a wiki " + ChatColor.DARK_GRAY + ": get a link to the Quests wiki");
sender.sendMessage(ChatColor.DARK_GRAY + " * " + ChatColor.RED + "/quests a about " + ChatColor.DARK_GRAY + ": get information about Quests");
Expand Down
@@ -0,0 +1,172 @@
package com.leonardobishop.quests.bukkit.command;

import com.leonardobishop.quests.bukkit.BukkitQuestsPlugin;
import com.leonardobishop.quests.bukkit.storage.MySqlStorageProvider;
import com.leonardobishop.quests.bukkit.storage.YamlStorageProvider;
import com.leonardobishop.quests.common.player.questprogressfile.QuestProgressFile;
import com.leonardobishop.quests.common.storage.StorageProvider;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

public class AdminMigrateCommandHandler implements CommandHandler {

private final BukkitQuestsPlugin plugin;

private final AtomicBoolean migrationInProgress;

public AdminMigrateCommandHandler(BukkitQuestsPlugin plugin) {
this.plugin = plugin;
this.migrationInProgress = new AtomicBoolean(false);
}

@Override
public void handle(CommandSender sender, String[] args) {
File dataMigrateFile = new File(plugin.getDataFolder(), "migrate_data.yml");

if (migrationInProgress.get()) {
sender.sendMessage(ChatColor.RED + "A migration is already in progress.");
}

if (args.length == 3 && args[2].equalsIgnoreCase("execute")) {
if (!dataMigrateFile.exists()) {
sender.sendMessage(ChatColor.RED + "Please run '/quests admin migratedata' first.");
return;
}

YamlConfiguration configuration;
try {
configuration = YamlConfiguration.loadConfiguration(dataMigrateFile);
} catch (Exception e) {
sender.sendMessage(ChatColor.RED + "An error occurred while loading the data migration file.");
e.printStackTrace();
sender.sendMessage(ChatColor.RED + "See server console for more details.");
return;
}

if (!configuration.getBoolean("ready")) {
sender.sendMessage(ChatColor.RED + "The 'ready' flag has not been set.");
sender.sendMessage(ChatColor.RED + "Please see the migrate_data.yml file, or the wiki, for instructions.");
return;
}

ConfigurationSection fromConfiguration = configuration.getConfigurationSection("from");
ConfigurationSection toConfiguration = configuration.getConfigurationSection("to");

if (fromConfiguration == null || toConfiguration == null) {
sender.sendMessage(ChatColor.RED + "The 'from' and 'to' sections have not been configured.");
sender.sendMessage(ChatColor.RED + "Please see the migrate_data.yml file, or the wiki, for instructions.");
return;
}

StorageProvider fromProvider = getStorageProvider(fromConfiguration);
StorageProvider toProvider = getStorageProvider(toConfiguration);

if (fromProvider.getName().equals("yaml") && toProvider.getName().equals("yaml")) {
//TODO check mysql databases aren't the same as well
sender.sendMessage(ChatColor.RED + "Refusing to migrate from 'yaml' to 'yaml'.");
sender.sendMessage(ChatColor.RED + "Please see the migrate_data.yml file, or the wiki, for instructions.");
return;
}

long startTime = System.currentTimeMillis();
sender.sendMessage(ChatColor.GRAY + "Performing migration...");
migrationInProgress.set(true);
plugin.getScheduler().doAsync(() -> {
try {
sender.sendMessage(ChatColor.GRAY + "Initialising storage provider '" + fromProvider.getName() + "'...");
fromProvider.init();
} catch (Exception e) {
sender.sendMessage(ChatColor.RED + "An error occurred while initializing '" + fromProvider.getName() + "' storage provider.");
return;
}

try {
sender.sendMessage(ChatColor.GRAY + "Initialising storage provider '" + toProvider.getName() + "'...");
toProvider.init();
} catch (Exception e) {
sender.sendMessage(ChatColor.RED + "An error occurred while initializing '" + toProvider.getName() + "' storage provider.");
return;
}

sender.sendMessage(ChatColor.GRAY + "Loading quest progress files from '" + fromProvider.getName() + "'...");
List<QuestProgressFile> files = fromProvider.loadAllProgressFiles();
sender.sendMessage(ChatColor.GRAY.toString() + files.size() + " files loaded.");

for (QuestProgressFile file : files) {
file.setModified(true);
}

sender.sendMessage(ChatColor.GRAY + "Writing quest progress files to '" + toProvider.getName() + "'...");
toProvider.saveAllProgressFiles(files);
sender.sendMessage(ChatColor.GRAY + "Done.");

try {
sender.sendMessage(ChatColor.GRAY + "Shutting down storage provider '" + fromProvider.getName() + "'...");
fromProvider.shutdown();
} catch (Exception e) {
sender.sendMessage(ChatColor.RED + "An error occurred while shutting down '" + fromProvider.getName() + "' storage provider.");
}

try {
sender.sendMessage(ChatColor.GRAY + "Shutting down storage provider '" + toProvider.getName() + "'...");
toProvider.shutdown();
} catch (Exception e) {
sender.sendMessage(ChatColor.RED + "An error occurred while shutting down '" + toProvider.getName() + "' storage provider.");
}

long endTime = System.currentTimeMillis();
sender.sendMessage(ChatColor.GREEN + "Migration complete. Took " + String.format("%.3f", (endTime - startTime) / 1000f) + "s.");

configuration.set("ready", false);
try {
configuration.save(dataMigrateFile);
} catch (IOException ignored) { }
migrationInProgress.set(false);
});
return;
}

if (!dataMigrateFile.exists()) {
plugin.writeResourceToFile("resources/bukkit/migrate_data.yml", dataMigrateFile);
}
sender.sendMessage(ChatColor.GRAY + "A file has been generated at /plugins/Quests/migrate_data.yml.");
sender.sendMessage(ChatColor.GRAY + "Please see this file, or the wiki, for further instructions.");
}

private StorageProvider getStorageProvider(ConfigurationSection configurationSection) {
String configuredProvider = configurationSection.getString("provider", "yaml");
StorageProvider storageProvider;
switch (configuredProvider.toLowerCase()) {
default:
case "yaml":
storageProvider = new YamlStorageProvider(plugin);
break;
case "mysql":
ConfigurationSection section = configurationSection.getConfigurationSection("database-settings");
storageProvider = new MySqlStorageProvider(plugin, section);
}
return storageProvider;
}

@Override
public List<String> tabComplete(CommandSender sender, String[] args) {
if (args.length == 3) {
return TabHelper.matchTabComplete(args[2], Collections.singletonList("execute"));
}
return Collections.emptyList();
}

@Override
public @Nullable String getPermission() {
return "quests.admin";
}
}
Expand Up @@ -41,6 +41,8 @@ public class MySqlStorageProvider implements StorageProvider {
"SELECT quest_id, started, completed, completed_before, completion_date FROM `{prefix}quest_progress` WHERE uuid=?;";
private static final String SELECT_PLAYER_TASK_PROGRESS =
"SELECT quest_id, task_id, completed, progress, data_type FROM `{prefix}task_progress` WHERE uuid=?;";
private static final String SELECT_UUID_LIST =
"SELECT DISTINCT uuid FROM `{prefix}quest_progress`;";
private static final String SELECT_KNOWN_PLAYER_QUEST_PROGRESS =
"SELECT quest_id FROM `{prefix}quest_progress` WHERE uuid=?;";
private static final String SELECT_KNOWN_PLAYER_TASK_PROGRESS =
Expand All @@ -66,6 +68,11 @@ public MySqlStorageProvider(BukkitQuestsPlugin plugin, ConfigurationSection conf
this.fault = true;
}

@Override
public String getName() {
return "mysql";
}

@Override
public void init() {
String address = configuration.getString("network.address", "localhost:3306");
Expand Down Expand Up @@ -285,4 +292,47 @@ public void saveProgressFile(@NotNull UUID uuid, @NotNull QuestProgressFile ques
e.printStackTrace();
}
}

@Override
public @NotNull List<QuestProgressFile> loadAllProgressFiles() {
if (fault) return Collections.emptyList();

Set<UUID> uuids = new HashSet<>();

try (Connection connection = hikari.getConnection()) {
try (PreparedStatement ps = connection.prepareStatement(this.statementProcessor.apply(SELECT_UUID_LIST))) {
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String uuidString = rs.getString(1);
try {
UUID uuid = UUID.fromString(uuidString);
uuids.add(uuid);
} catch (IllegalArgumentException ignored) { }
}
}
}
} catch (SQLException e) {
e.printStackTrace();
return Collections.emptyList();
}

List<QuestProgressFile> files = new ArrayList<>();
for (UUID uuid : uuids) {
QuestProgressFile file = loadProgressFile(uuid);
if (file != null) {
files.add(file);
}
}

return files;
}

@Override
public void saveAllProgressFiles(List<QuestProgressFile> files) {
if (fault) return;

for (QuestProgressFile file : files) {
saveProgressFile(file.getPlayerUUID(), file);
}
}
}
Expand Up @@ -12,6 +12,8 @@

import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
Expand All @@ -32,6 +34,11 @@ private ReentrantLock lock(UUID uuid) {
return lock;
}

@Override
public String getName() {
return "yaml";
}

@Override
public void init() {
File directory = new File(plugin.getDataFolder() + File.separator + "playerdata");
Expand Down Expand Up @@ -147,4 +154,44 @@ public void saveProgressFile(@NotNull UUID uuid, @NotNull QuestProgressFile ques
lock.unlock();
}
}

public @NotNull List<QuestProgressFile> loadAllProgressFiles() {
List<QuestProgressFile> files = new ArrayList<>();

File directory = new File(plugin.getDataFolder() + File.separator + "playerdata");
FileVisitor<Path> fileVisitor = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes attributes) {
if (path.toString().endsWith(".yml")) {
UUID uuid;
try {
uuid = UUID.fromString(path.getFileName().toString().replace(".yml", ""));
} catch (IllegalArgumentException e) {
return FileVisitResult.CONTINUE;
}

QuestProgressFile file = loadProgressFile(uuid);
if (file != null) {
files.add(file);
}
}
return FileVisitResult.CONTINUE;
}
};

try {
Files.walkFileTree(directory.toPath(), fileVisitor);
} catch (IOException e) {
e.printStackTrace();
}

return files;
}

@Override
public void saveAllProgressFiles(List<QuestProgressFile> files) {
for (QuestProgressFile file : files) {
saveProgressFile(file.getPlayerUUID(), file);
}
}
}
49 changes: 49 additions & 0 deletions bukkit/src/main/resources/resources/bukkit/migrate_data.yml
@@ -0,0 +1,49 @@
# This file was automatically generated by the Quests data migration tool (/q a migratedata).
# Please read these instructions carefully.
#
# The Quests data migration tool is a tool that allows you to migrate your data from one storage
# system (referred to as 'storage provider') to another. This file serves as a configuration for
# both storage systems to migrate from.
#
# The 'from' section below is the configuration for the storage provider you are migrating from.
# The 'to' section below is the configuration for the storage provider you are migrating to.
#
# The 'from' and 'to' sections are both required.
#
# When you have configured both storage providers, you must set the 'ready' flag to true.
# The command will not work if this is not done.
#
# WARNING: This process should be done on a server with no players online, from your
# server console. Unexpected behaviour or potential data corruption may occur
# if players are online!
#
# One everything is configured, you can execute the migration with the following command:
# /quests admin migratedata execute
#
# When the process has finished, you can remove this file. You must update your main
# configuration file to point to the new storage provider manually.
#
# These instructions are also available on the wiki:
# https://github.com/LMBishop/Quests/wiki/Data-migration-tool

# Data provider to migrate from
from:
provider: "yaml"

# Data provider to migrate to
to:
provider: "mysql"
database-settings:
network:
database: "minecraft"
username: "root"
password: ""
address: "localhost:3306"
connection-pool-settings:
maximum-pool-size: 8
minimum-idle: 8
maximum-lifetime: 1800000
connection-timeout: 5000
table-prefix: "quests_"

ready: false
Expand Up @@ -150,4 +150,11 @@ public void resetModified() {
progress.resetModified();
}
}

public void setModified(boolean modified) {
this.modified = modified;
for (TaskProgress progress : this.taskProgress.values()) {
progress.setModified(modified);
}
}
}

0 comments on commit 894f1c2

Please sign in to comment.