9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-31 20:56:42 +00:00

Implement data rollbacks and deletion

This commit is contained in:
William
2022-07-08 16:05:12 +01:00
parent b7709f2d6c
commit f650db4438
10 changed files with 212 additions and 56 deletions

View File

@@ -1,22 +1,23 @@
package net.william278.husksync.command;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.ItemData;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.editor.ItemEditorMenu;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class EnderChestCommand extends CommandBase {
public class EnderChestCommand extends CommandBase implements TabCompletable {
public EnderChestCommand(@NotNull HuskSync implementor) {
super("enderchest", Permission.COMMAND_ENDER_CHEST, implementor, "echest", "openechest");
@@ -35,12 +36,10 @@ public class EnderChestCommand extends CommandBase {
// View user data by specified UUID
try {
final UUID versionUuid = UUID.fromString(args[1]);
plugin.getDatabase().getUserData(user).thenAccept(userDataList -> userDataList.stream()
.filter(userData -> userData.versionUUID().equals(versionUuid)).findFirst().ifPresentOrElse(
userData -> showEnderChestMenu(player, userData, user, userDataList.stream().sorted().findFirst()
.map(VersionedUserData::versionUUID).orElse(UUID.randomUUID()).equals(versionUuid)),
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage)));
plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse(
userData -> showEnderChestMenu(player, userData, user, false),
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage)));
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/enderchest <player> [version_uuid]").ifPresent(player::sendMessage);
@@ -77,6 +76,14 @@ public class EnderChestCommand extends CommandBase {
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.ENDER_CHEST_COMMAND_EDIT).join();
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
});
}
@Override
public List<String> onTabComplete(@NotNull OnlineUser player, @NotNull String[] args) {
return plugin.getOnlineUsers().stream().map(user -> user.username)
.filter(argument -> argument.startsWith(args.length >= 1 ? args[1] : ""))
.sorted().collect(Collectors.toList());
}
}

View File

@@ -11,11 +11,13 @@ import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
import java.text.DateFormat;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class InventoryCommand extends CommandBase {
public class InventoryCommand extends CommandBase implements TabCompletable {
public InventoryCommand(@NotNull HuskSync implementor) {
super("inventory", Permission.COMMAND_INVENTORY, implementor, "invsee", "openinv");
@@ -34,12 +36,10 @@ public class InventoryCommand extends CommandBase {
// View user data by specified UUID
try {
final UUID versionUuid = UUID.fromString(args[1]);
plugin.getDatabase().getUserData(user).thenAccept(userDataList -> userDataList.stream()
.filter(userData -> userData.versionUUID().equals(versionUuid)).findFirst().ifPresentOrElse(
userData -> showInventoryMenu(player, userData, user, userDataList.stream().sorted().findFirst()
.map(VersionedUserData::versionUUID).orElse(UUID.randomUUID()).equals(versionUuid)),
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage)));
plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse(
userData -> showInventoryMenu(player, userData, user, false),
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage)));
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/inventory <player> [version_uuid]").ifPresent(player::sendMessage);
@@ -78,4 +78,10 @@ public class InventoryCommand extends CommandBase {
});
}
@Override
public List<String> onTabComplete(@NotNull OnlineUser player, @NotNull String[] args) {
return plugin.getOnlineUsers().stream().map(user -> user.username)
.filter(argument -> argument.startsWith(args.length >= 1 ? args[1] : ""))
.sorted().collect(Collectors.toList());
}
}

View File

@@ -1,10 +1,12 @@
package net.william278.husksync.command;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.player.OnlineUser;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -41,14 +43,11 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
final UUID versionUuid = UUID.fromString(args[2]);
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
optionalUser -> optionalUser.ifPresentOrElse(
user -> plugin.getDatabase().getUserData(user).thenAccept(
userDataList -> userDataList.stream().filter(versionedUserData -> versionedUserData
.versionUUID().equals(versionUuid))
.findFirst().ifPresentOrElse(userData ->
plugin.getDataEditor()
.displayDataOverview(player, userData, user),
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage))),
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data ->
data.ifPresentOrElse(userData -> plugin.getDataEditor()
.displayDataOverview(player, userData, user),
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage))),
() -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage))));
} catch (IllegalArgumentException e) {
@@ -91,18 +90,93 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
.ifPresent(player::sendMessage))));
}
case "delete" -> {
// Delete user data by specified UUID
if (args.length < 3) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata delete <username> <version_uuid>")
.ifPresent(player::sendMessage);
return;
}
final String username = args[1];
try {
final UUID versionUuid = UUID.fromString(args[2]);
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
optionalUser -> optionalUser.ifPresentOrElse(
user -> plugin.getDatabase().deleteUserData(user, versionUuid).thenAccept(deleted -> {
if (deleted) {
plugin.getLocales().getLocale("data_deleted",
versionUuid.toString().split("-")[0],
versionUuid.toString(),
user.username,
user.uuid.toString())
.ifPresent(player::sendMessage);
} else {
plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage);
}
}),
() -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage))));
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata delete <username> <version_uuid>")
.ifPresent(player::sendMessage);
}
}
case "restore" -> {
// Get user data by specified uuid and username
if (args.length < 3) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata restore <username> <version_uuid>")
.ifPresent(player::sendMessage);
return;
}
final String username = args[1];
try {
final UUID versionUuid = UUID.fromString(args[2]);
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
optionalUser -> optionalUser.ifPresentOrElse(
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> {
if (data.isEmpty()) {
plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage);
return;
}
plugin.getDatabase().setUserData(user, data.get().userData(),
DataSaveCause.BACKUP_RESTORE);
plugin.getRedisManager().sendUserDataUpdate(user, data.get().userData()).join();
plugin.getLocales().getLocale("data_restored",
user.username,
user.uuid.toString(),
versionUuid.toString().split("-")[0],
versionUuid.toString())
.ifPresent(player::sendMessage);
}),
() -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage))));
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata restore <username> <version_uuid>")
.ifPresent(player::sendMessage);
}
}
}
}
@Override
public List<String> onTabComplete(@NotNull OnlineUser player, @NotNull String[] args) {
return Arrays.stream(COMMAND_ARGUMENTS)
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
.sorted().collect(Collectors.toList());
switch (args.length) {
case 0, 1 -> {
return Arrays.stream(COMMAND_ARGUMENTS)
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
.sorted().collect(Collectors.toList());
}
case 2 -> {
return plugin.getOnlineUsers().stream().map(user -> user.username)
.filter(argument -> argument.startsWith(args[1]))
.sorted().collect(Collectors.toList());
}
}
return Collections.emptyList();
}
}

View File

@@ -42,7 +42,7 @@ public class Locales {
*/
public Optional<String> getRawLocale(@NotNull String localeId) {
if (rawLocales.containsKey(localeId)) {
return Optional.of(rawLocales.get(localeId));
return Optional.of(rawLocales.get(localeId).replaceAll(Pattern.quote("\\n"), "\n"));
}
return Optional.empty();
}

View File

@@ -169,12 +169,30 @@ public abstract class Database {
public abstract CompletableFuture<List<VersionedUserData>> getUserData(@NotNull User user);
/**
* <b>(Internal)</b> Prune user data records for a given user to the maximum value as configured
* Gets a specific {@link VersionedUserData} entry for a user from the database, by its UUID.
*
* @param user The user to get data for
* @param versionUuid The UUID of the {@link VersionedUserData} entry to get
* @return A future returning an optional containing the {@link VersionedUserData}, if it exists, or an empty optional if it does not
*/
public abstract CompletableFuture<Optional<VersionedUserData>> getUserData(@NotNull User user, @NotNull UUID versionUuid);
/**
* <b>(Internal)</b> Prune user data for a given user to the maximum value as configured
*
* @param user The user to prune data for
* @return A future returning void when complete
*/
protected abstract CompletableFuture<Void> pruneUserDataRecords(@NotNull User user);
protected abstract CompletableFuture<Void> pruneUserData(@NotNull User user);
/**
* Deletes a specific {@link VersionedUserData} entry for a user from the database, by its UUID.
*
* @param user The user to get data for
* @param versionUuid The UUID of the {@link VersionedUserData} entry to delete
* @return A future returning void when complete
*/
public abstract CompletableFuture<Boolean> deleteUserData(@NotNull User user, @NotNull UUID versionUuid);
/**
* Save user data to the database<p>

View File

@@ -268,7 +268,38 @@ public class MySqlDatabase extends Database {
}
@Override
protected CompletableFuture<Void> pruneUserDataRecords(@NotNull User user) {
public CompletableFuture<Optional<VersionedUserData>> getUserData(@NotNull User user, @NotNull UUID versionUuid) {
return CompletableFuture.supplyAsync(() -> {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `version_uuid`, `timestamp`, `save_cause`, `data`
FROM `%data_table%`
WHERE `player_uuid`=? AND `version_uuid`=?
ORDER BY `timestamp` DESC
LIMIT 1;"""))) {
statement.setString(1, user.uuid.toString());
statement.setString(2, versionUuid.toString());
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
final Blob blob = resultSet.getBlob("data");
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free();
return Optional.of(new VersionedUserData(
UUID.fromString(resultSet.getString("version_uuid")),
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
getDataAdapter().fromBytes(dataByteArray)));
}
}
} catch (SQLException | DataAdaptionException e) {
getLogger().log(Level.SEVERE, "Failed to fetch specific user data by UUID from the database", e);
}
return Optional.empty();
});
}
@Override
protected CompletableFuture<Void> pruneUserData(@NotNull User user) {
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(data -> {
if (data.size() > maxUserDataRecords) {
try (Connection connection = getConnection()) {
@@ -288,6 +319,25 @@ public class MySqlDatabase extends Database {
}));
}
@Override
public CompletableFuture<Boolean> deleteUserData(@NotNull User user, @NotNull UUID versionUuid) {
return CompletableFuture.supplyAsync(() -> {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM `%data_table%`
WHERE `player_uuid`=? AND `version_uuid`=?
LIMIT 1;"""))) {
statement.setString(1, user.uuid.toString());
statement.setString(2, versionUuid.toString());
return statement.executeUpdate() > 0;
}
} catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to delete specific user data from the database", e);
}
return false;
});
}
@Override
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData,
@NotNull DataSaveCause saveCause) {
@@ -311,7 +361,7 @@ public class MySqlDatabase extends Database {
getLogger().log(Level.SEVERE, "Failed to set user data in the database", e);
}
}
}).thenRun(() -> pruneUserDataRecords(user).join());
}).thenRun(() -> pruneUserData(user).join());
}
@Override

View File

@@ -82,18 +82,21 @@ public class DataEditor {
public void displayDataOverview(@NotNull OnlineUser user, @NotNull VersionedUserData userData,
@NotNull User dataOwner) {
locales.getLocale("data_manager_title",
dataOwner.username, dataOwner.uuid.toString())
.ifPresent(user::sendMessage);
locales.getLocale("data_manager_versioning",
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss").format(userData.versionTimestamp()),
userData.versionUUID().toString().split("-")[0],
userData.versionUUID().toString(),
dataOwner.username,
dataOwner.uuid.toString())
.ifPresent(user::sendMessage);
locales.getLocale("data_manager_timestamp",
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss").format(userData.versionTimestamp()))
.ifPresent(user::sendMessage);
locales.getLocale("data_manager_cause",
userData.cause().name().toLowerCase().replaceAll("_", " "))
.ifPresent(user::sendMessage);
locales.getLocale("data_manager_status",
Double.toString(userData.userData().getStatusData().health),
Double.toString(userData.userData().getStatusData().maxHealth),
Double.toString(userData.userData().getStatusData().hunger),
Integer.toString((int) userData.userData().getStatusData().health),
Integer.toString((int) userData.userData().getStatusData().maxHealth),
Integer.toString(userData.userData().getStatusData().hunger),
Integer.toString(userData.userData().getStatusData().expLevel),
userData.userData().getStatusData().gameMode.toLowerCase())
.ifPresent(user::sendMessage);
@@ -146,7 +149,7 @@ public class DataEditor {
dataOwner.username, dataOwner.uuid.toString())
.ifPresent(user::sendMessage);
final String[] numberedIcons = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳" .split("");
final String[] numberedIcons = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳".split("");
for (int i = 0; i < Math.min(20, userDataList.size()); i++) {
final VersionedUserData userData = userDataList.get(i);
locales.getLocale("data_list_item",

View File

@@ -3,7 +3,6 @@ package net.william278.husksync.util;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
@@ -13,7 +12,6 @@ import java.util.logging.Level;
public class UpdateChecker {
private final static int SPIGOT_PROJECT_ID = 97144;
private final Logger logger;
private final VersionUtils.Version currentVersion;
@@ -52,7 +50,7 @@ public class UpdateChecker {
if (isUpdateAvailable(latestVersion)) {
logger.log(Level.WARNING, "A new version of HuskSync is available: v" + latestVersion);
} else {
logger.log(Level.INFO, "HuskSync is up-to-date! (Running: v" + currentVersion + ")");
logger.log(Level.INFO, "HuskSync is up-to-date! (Running: v" + getCurrentVersion().toString() + ")");
}
});
}