diff --git a/Copyright_Notice.txt b/Copyright_Notice similarity index 88% rename from Copyright_Notice.txt rename to Copyright_Notice index 4740a3f..f6cfe9f 100644 --- a/Copyright_Notice.txt +++ b/Copyright_Notice @@ -3,6 +3,8 @@ This project uses third party libraries as its dependencies and includes them in Cotton config, Cotton logging, and Jankson-Fabric all by Cotton team licensed under MIT license which can be found at https://github.com/CottonMC/Cotton XZ for Java by Tukaani released as public domain. https://tukaani.org/xz/java.html parallelgzip by shevek under Apache 2.0 http://www.apache.org/licenses/ + +To save on space Parallel BZip2 was unpacked Parallel BZip2 compression by Karl Gustafsson at http://at4j.sourceforge.net/ under GPL v3 Some code was partially or fully inspired by: diff --git a/README.md b/README.md index a354bfe..cb19772 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,11 @@ Commands look like that: `/backup [args]` Available operations are: * start - just starts backup. You can add comment* to file by just typing it after command. For example: `/backup start FabricIsGreat` - * restore - restores backup. Note that the current world will be backuped, and you can add comment to it. `/backup restore [comment]` + * restore - restores backup. Note that the current world will be backuped, and you can add comment to it. `/backup restore [comment]` * killR - terminate current restoration. * list - lists all avaliable backups. * cleanup - forces cleanup procedure (deletes old backups according to config) + * delete - delets given file, usage the same as restore * whitelist - here you can add, remove and list player that are allowed to run any operation within this mod despite not having high enough permission level* * blacklist - here you can add, remove and list player that are not allowed to run any operation within this mod despite having high enough permission level* diff --git a/build.gradle b/build.gradle index 5df4d35..8632a33 100644 --- a/build.gradle +++ b/build.gradle @@ -3,11 +3,11 @@ plugins { id 'maven-publish' } -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 +sourceCompatibility = JavaVersion.VERSION_16 +targetCompatibility = JavaVersion.VERSION_16 archivesBaseName = project.archives_base_name -version = "${project.mod_version}-${project.minecraft_version}" +version = "${project.mod_version}-${getMcMinor(project.minecraft_version)}" group = project.maven_group minecraft { @@ -57,8 +57,6 @@ processResources { tasks.withType(JavaCompile) { options.encoding = "UTF-8" } - - java { // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task // if it is present. @@ -72,6 +70,8 @@ jar { } } + from("Copyright_Notice") +} // configure the maven publication publishing { publications { @@ -89,6 +89,16 @@ publishing { // select the repositories you want to publish to repositories { // uncomment to publish to the local maven - // mavenLocal() + + maven { + name = 'myRepo' + url = layout.buildDirectory.dir("repo") + } } } + +static def getMcMinor(ver) { + String[] arr = ((String)ver).split("\\."); + + return (String)(arr[0] + "." + arr[1]); +} diff --git a/gradle.properties b/gradle.properties index 8043c54..dc60769 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,6 +9,6 @@ loader_version=0.11.5 fabric_version=0.35.1+1.17 # Mod Properties -mod_version = 2.0.2 +mod_version = 2.1.0 maven_group = net.szum123321 archives_base_name = textile_backup \ No newline at end of file diff --git a/src/main/java/net/szum123321/textile_backup/ConfigHandler.java b/src/main/java/net/szum123321/textile_backup/ConfigHandler.java index a9e8d89..6ef018d 100644 --- a/src/main/java/net/szum123321/textile_backup/ConfigHandler.java +++ b/src/main/java/net/szum123321/textile_backup/ConfigHandler.java @@ -23,9 +23,7 @@ import io.github.cottonmc.cotton.config.annotations.ConfigFile; import java.io.File; import java.time.format.DateTimeFormatter; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; +import java.util.*; @ConfigFile(name = Statics.MOD_ID) public class ConfigHandler { @@ -53,7 +51,7 @@ public class ConfigHandler { @Comment("\nThis setting allows you to exclude files form being backedup.\n"+ "Be very careful when setting it, as it is easy corrupt your world!\n") - public Set fileBlacklist = new HashSet<>(); + public List fileBlacklist = new ArrayList<>(); @Comment("\nShould backups be deleted after being restored?\n") public boolean deleteOldBackupAfterRestore = true; @@ -71,14 +69,16 @@ public class ConfigHandler { @Comment("\nCompression level \n0 - 9\n Only affects zip compression.\n") public int compression = 7; - @Comment("\nLimit how many cores can be used for compression.\n") + @Comment("\nLimit how many cores can be used for compression.\n" + + "0 means that all available cores will be used\n") public int compressionCoreCountLimit = 0; @Comment(value = "\nAvailable formats are:\n" + "ZIP - normal zip archive using standard deflate compression\n" + "GZIP - tar.gz using gzip compression\n" + "BZIP2 - tar.bz2 archive using bzip2 compression\n" + - "LZMA - tar.xz using lzma compression\n") + "LZMA - tar.xz using lzma compression\n" + + "TAR - .tar with no compression\n") public ArchiveFormat format = ArchiveFormat.ZIP; @Comment("\nMinimal permission level required to run commands\n") @@ -97,7 +97,7 @@ public class ConfigHandler { "Remember not to use '#' symbol or any other character that is not allowed by your operating system such as:\n" + "':', '\\', etc...\n" + "For more info: https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html\n") - public String dateTimeFormat = "dd.MM.yyyy_HH-mm-ss"; + public String dateTimeFormat = "yyyy.MM.dd_HH-mm-ss"; public Optional sanitize() { if(compressionCoreCountLimit > Runtime.getRuntime().availableProcessors()) @@ -123,19 +123,32 @@ public class ConfigHandler { } public enum ArchiveFormat { - ZIP(".zip"), - GZIP(".tar.gz"), - BZIP2(".tar.bz2"), - LZMA(".tar.xz"); + ZIP("zip"), + GZIP("tar", "gz"), + BZIP2("tar", "bz2"), + LZMA("tar", "xz"), + TAR("tar"); - private final String extension; + private final List extensionPieces; - private ArchiveFormat(String extension){ - this.extension = extension; + ArchiveFormat(String... extensionParts) { + extensionPieces = Arrays.asList(extensionParts); } - public String getString() { - return extension; + public String getCompleteString() { + StringBuilder builder = new StringBuilder(); + + extensionPieces.forEach(s -> builder.append('.').append(s)); + + return builder.toString(); + } + + boolean isMultipart() { + return extensionPieces.size() > 1; + } + + public String getLastPiece() { + return extensionPieces.get(extensionPieces.size() - 1); } } } diff --git a/src/main/java/net/szum123321/textile_backup/Statics.java b/src/main/java/net/szum123321/textile_backup/Statics.java index 2f8c50a..1cfd42a 100644 --- a/src/main/java/net/szum123321/textile_backup/Statics.java +++ b/src/main/java/net/szum123321/textile_backup/Statics.java @@ -24,6 +24,7 @@ import net.szum123321.textile_backup.core.restore.AwaitThread; import java.io.File; import java.time.format.DateTimeFormatter; +import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; @@ -40,6 +41,9 @@ public class Statics { public final static DateTimeFormatter defaultDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss"); public static final AtomicBoolean globalShutdownBackupFlag = new AtomicBoolean(true); + public static boolean disableWatchdog = false; public static AwaitThread restoreAwaitThread = null; - public static File untouchableFile; + public static Optional untouchableFile = Optional.empty(); + + public static boolean tmpAvailable; } diff --git a/src/main/java/net/szum123321/textile_backup/TextileBackup.java b/src/main/java/net/szum123321/textile_backup/TextileBackup.java index 1e675b3..e2ffd7b 100644 --- a/src/main/java/net/szum123321/textile_backup/TextileBackup.java +++ b/src/main/java/net/szum123321/textile_backup/TextileBackup.java @@ -28,11 +28,14 @@ import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.minecraft.server.command.ServerCommandSource; import net.szum123321.textile_backup.commands.create.CleanupCommand; import net.szum123321.textile_backup.commands.create.StartBackupCommand; -import net.szum123321.textile_backup.commands.permission.BlacklistCommand; -import net.szum123321.textile_backup.commands.permission.WhitelistCommand; +import net.szum123321.textile_backup.commands.manage.BlacklistCommand; +import net.szum123321.textile_backup.commands.manage.DeleteCommand; +import net.szum123321.textile_backup.commands.manage.WhitelistCommand; import net.szum123321.textile_backup.commands.restore.KillRestoreCommand; -import net.szum123321.textile_backup.commands.restore.ListBackupsCommand; +import net.szum123321.textile_backup.commands.manage.ListBackupsCommand; import net.szum123321.textile_backup.commands.restore.RestoreBackupCommand; +import net.szum123321.textile_backup.core.ActionInitiator; +import net.szum123321.textile_backup.core.Utilities; import net.szum123321.textile_backup.core.create.BackupContext; import net.szum123321.textile_backup.core.create.BackupHelper; @@ -52,12 +55,23 @@ public class TextileBackup implements ModInitializer { System.exit(1); } + //TODO: finish writing wiki + if(Statics.CONFIG.format == ConfigHandler.ArchiveFormat.ZIP) { + Statics.tmpAvailable = Utilities.isTmpAvailable(); + if(!Statics.tmpAvailable) { + Statics.LOGGER.warn(""" + WARNING! It seems like the temporary folder is not accessible on this system! + This will cause problems with multithreaded zip compression, so a normal one will be used instead. + For more info please read: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems"""); + } + } + if(Statics.CONFIG.backupInterval > 0) ServerTickEvents.END_SERVER_TICK.register(Statics.scheduler::tick); + //Restart Executor Service in singleplayer ServerLifecycleEvents.SERVER_STARTING.register(ignored -> { - if(Statics.executorService.isShutdown()) - Statics.executorService = Executors.newSingleThreadExecutor(); + if(Statics.executorService.isShutdown()) Statics.executorService = Executors.newSingleThreadExecutor(); }); ServerLifecycleEvents.SERVER_STOPPED.register(server -> { @@ -65,9 +79,10 @@ public class TextileBackup implements ModInitializer { if (Statics.CONFIG.shutdownBackup && Statics.globalShutdownBackupFlag.get()) { BackupHelper.create( - new BackupContext.Builder() + BackupContext.Builder + .newBackupContextBuilder() .setServer(server) - .setInitiator(BackupContext.BackupInitiator.Shutdown) + .setInitiator(ActionInitiator.Shutdown) .setComment("shutdown") .build() ).run(); @@ -94,6 +109,7 @@ public class TextileBackup implements ModInitializer { .then(BlacklistCommand.register()) .then(RestoreBackupCommand.register()) .then(ListBackupsCommand.register()) + .then(DeleteCommand.register()) .then(KillRestoreCommand.register()) )); } diff --git a/src/main/java/net/szum123321/textile_backup/commands/CommandExceptions.java b/src/main/java/net/szum123321/textile_backup/commands/CommandExceptions.java new file mode 100644 index 0000000..42accf0 --- /dev/null +++ b/src/main/java/net/szum123321/textile_backup/commands/CommandExceptions.java @@ -0,0 +1,39 @@ +/* + * A simple backup mod for Fabric + * Copyright (C) 2020 Szum123321 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.szum123321.textile_backup.commands; + +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import net.minecraft.text.LiteralText; +import net.minecraft.text.MutableText; + +import java.time.format.DateTimeParseException; + +public class CommandExceptions { + public static final DynamicCommandExceptionType DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE = new DynamicCommandExceptionType(o -> { + DateTimeParseException e = (DateTimeParseException)o; + + MutableText message = new LiteralText("An exception occurred while trying to parse:\n") + .append(e.getParsedString()) + .append("\n"); + + for (int i = 0; i < e.getErrorIndex(); i++) message.append(" "); + + return message.append("^"); + }); +} diff --git a/src/main/java/net/szum123321/textile_backup/commands/FileSuggestionProvider.java b/src/main/java/net/szum123321/textile_backup/commands/FileSuggestionProvider.java new file mode 100644 index 0000000..eb2534f --- /dev/null +++ b/src/main/java/net/szum123321/textile_backup/commands/FileSuggestionProvider.java @@ -0,0 +1,67 @@ +/* + * A simple backup mod for Fabric + * Copyright (C) 2020 Szum123321 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.szum123321.textile_backup.commands; + +import com.mojang.brigadier.LiteralMessage; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.command.ServerCommandSource; +import net.szum123321.textile_backup.Statics; +import net.szum123321.textile_backup.core.restore.RestoreHelper; +import org.lwjgl.system.CallbackI; + +import java.util.concurrent.CompletableFuture; + +public final class FileSuggestionProvider implements SuggestionProvider { + private static final FileSuggestionProvider INSTANCE = new FileSuggestionProvider(); + + public static FileSuggestionProvider Instance() { + return INSTANCE; + } + + @Override + public CompletableFuture getSuggestions(CommandContext ctx, SuggestionsBuilder builder) throws CommandSyntaxException { + String remaining = builder.getRemaining(); + + for (RestoreHelper.RestoreableFile file : RestoreHelper.getAvailableBackups(ctx.getSource().getMinecraftServer())) { + String formattedCreationTime = file.getCreationTime().format(Statics.defaultDateTimeFormatter); + + if (formattedCreationTime.startsWith(remaining)) { + if (ctx.getSource().getEntity() instanceof PlayerEntity) { //was typed by player + if (file.getComment() != null) { + builder.suggest(formattedCreationTime, new LiteralMessage("Comment: " + file.getComment())); + } else { + builder.suggest(formattedCreationTime); + } + } else { //was typed from server console + if (file.getComment() != null) { + builder.suggest(file.getCreationTime() + "#" + file.getComment()); + } else { + builder.suggest(formattedCreationTime); + } + } + } + } + return builder.buildFuture(); + } +} diff --git a/src/main/java/net/szum123321/textile_backup/commands/create/StartBackupCommand.java b/src/main/java/net/szum123321/textile_backup/commands/create/StartBackupCommand.java index 5ed374b..bf24c41 100644 --- a/src/main/java/net/szum123321/textile_backup/commands/create/StartBackupCommand.java +++ b/src/main/java/net/szum123321/textile_backup/commands/create/StartBackupCommand.java @@ -20,48 +20,41 @@ package net.szum123321.textile_backup.commands.create; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.ServerCommandSource; import net.szum123321.textile_backup.Statics; import net.szum123321.textile_backup.core.create.BackupContext; import net.szum123321.textile_backup.core.create.BackupHelper; +import javax.annotation.Nullable; + public class StartBackupCommand { public static LiteralArgumentBuilder register() { return CommandManager.literal("start") .then(CommandManager.argument("comment", StringArgumentType.string()) - .executes(StartBackupCommand::executeWithComment) - ).executes(ctx -> execute(ctx.getSource())); + .executes(ctx -> execute(ctx.getSource(), StringArgumentType.getString(ctx, "comment"))) + ).executes(ctx -> execute(ctx.getSource(), null)); } - private static int executeWithComment(CommandContext ctx) { - if(!Statics.executorService.isShutdown()) - Statics.executorService.submit( - BackupHelper.create( - new BackupContext.Builder() - .setCommandSource(ctx.getSource()) - .setComment(StringArgumentType.getString(ctx, "comment")) - .guessInitiator() - .saveServer() - .build() - ) - ); - - return 1; - } - - private static int execute(ServerCommandSource source){ - if(!Statics.executorService.isShutdown()) - Statics.executorService.submit( - BackupHelper.create( - new BackupContext.Builder() - .setCommandSource(source) - .guessInitiator() - .saveServer() - .build() - ) - ); + private static int execute(ServerCommandSource source, @Nullable String comment) { + if(!Statics.executorService.isShutdown()) { + try { + Statics.executorService.submit( + BackupHelper.create( + BackupContext.Builder + .newBackupContextBuilder() + .setCommandSource(source) + .setComment(comment) + .guessInitiator() + .saveServer() + .build() + ) + ); + } catch (Exception e) { + Statics.LOGGER.error("Something went wrong while executing command!", e); + throw e; + } + } return 1; } diff --git a/src/main/java/net/szum123321/textile_backup/commands/permission/BlacklistCommand.java b/src/main/java/net/szum123321/textile_backup/commands/manage/BlacklistCommand.java similarity index 98% rename from src/main/java/net/szum123321/textile_backup/commands/permission/BlacklistCommand.java rename to src/main/java/net/szum123321/textile_backup/commands/manage/BlacklistCommand.java index cc6baf4..b8960b3 100644 --- a/src/main/java/net/szum123321/textile_backup/commands/permission/BlacklistCommand.java +++ b/src/main/java/net/szum123321/textile_backup/commands/manage/BlacklistCommand.java @@ -1,4 +1,4 @@ -package net.szum123321.textile_backup.commands.permission; +package net.szum123321.textile_backup.commands.manage; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; diff --git a/src/main/java/net/szum123321/textile_backup/commands/manage/DeleteCommand.java b/src/main/java/net/szum123321/textile_backup/commands/manage/DeleteCommand.java new file mode 100644 index 0000000..d98db4c --- /dev/null +++ b/src/main/java/net/szum123321/textile_backup/commands/manage/DeleteCommand.java @@ -0,0 +1,84 @@ +/* + * A simple backup mod for Fabric + * Copyright (C) 2020 Szum123321 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.szum123321.textile_backup.commands.manage; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.szum123321.textile_backup.commands.CommandExceptions; +import net.szum123321.textile_backup.Statics; +import net.szum123321.textile_backup.commands.FileSuggestionProvider; +import net.szum123321.textile_backup.core.Utilities; + +import java.io.File; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.Optional; + +public class DeleteCommand { + public static LiteralArgumentBuilder register() { + return CommandManager.literal("delete") + .then(CommandManager.argument("file", StringArgumentType.word()) + .suggests(FileSuggestionProvider.Instance()) + .executes(ctx -> execute(ctx.getSource(), StringArgumentType.getString(ctx, "file"))) + ); + } + + private static int execute(ServerCommandSource source, String fileName) throws CommandSyntaxException { + LocalDateTime dateTime; + + try { + dateTime = LocalDateTime.from(Statics.defaultDateTimeFormatter.parse(fileName)); + } catch (DateTimeParseException e) { + throw CommandExceptions.DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE.create(e); + } + + File root = Utilities.getBackupRootPath(Utilities.getLevelName(source.getMinecraftServer())); + + Optional optionalFile = Arrays.stream(root.listFiles()) + .filter(Utilities::isValidBackup) + .filter(file -> Utilities.getFileCreationTime(file).orElse(LocalDateTime.MIN).equals(dateTime)) + .findFirst(); + + if(optionalFile.isPresent()) { + if(Statics.untouchableFile.isEmpty() || !Statics.untouchableFile.get().equals(optionalFile.get())) { + if(optionalFile.get().delete()) { + Statics.LOGGER.sendInfo(source, "File {} successfully deleted!", optionalFile.get().getName()); + + if(source.getEntity() instanceof PlayerEntity) + Statics.LOGGER.info("Player {} deleted {}.", source.getPlayer().getName(), optionalFile.get().getName()); + } else { + Statics.LOGGER.sendError(source, "Something went wrong while deleting file!"); + } + } else { + Statics.LOGGER.sendError(source, "Couldn't delete the file because it's being restored right now."); + Statics.LOGGER.sendHint(source, "If you want to abort restoration then use: /backup killR"); + } + } else { + Statics.LOGGER.sendError(source, "Couldn't find file by this name."); + Statics.LOGGER.sendHint(source, "Maybe try /backup list"); + } + + return 0; + } +} diff --git a/src/main/java/net/szum123321/textile_backup/commands/restore/ListBackupsCommand.java b/src/main/java/net/szum123321/textile_backup/commands/manage/ListBackupsCommand.java similarity index 88% rename from src/main/java/net/szum123321/textile_backup/commands/restore/ListBackupsCommand.java rename to src/main/java/net/szum123321/textile_backup/commands/manage/ListBackupsCommand.java index dfbde8a..2deb6d8 100644 --- a/src/main/java/net/szum123321/textile_backup/commands/restore/ListBackupsCommand.java +++ b/src/main/java/net/szum123321/textile_backup/commands/manage/ListBackupsCommand.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package net.szum123321.textile_backup.commands.restore; +package net.szum123321.textile_backup.commands.manage; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import net.minecraft.server.command.CommandManager; @@ -24,9 +24,7 @@ import net.minecraft.server.command.ServerCommandSource; import net.szum123321.textile_backup.Statics; import net.szum123321.textile_backup.core.restore.RestoreHelper; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; +import java.util.*; public class ListBackupsCommand { public static LiteralArgumentBuilder register() { @@ -40,9 +38,9 @@ public class ListBackupsCommand { builder.append("There is only one backup available: "); builder.append(backups.get(0).toString()); } else { - backups.sort(Comparator.comparing(RestoreHelper.RestoreableFile::getCreationTime)); + backups.sort(null); Iterator iterator = backups.iterator(); - builder.append("Available backups: "); + builder.append("Available backups:\n"); builder.append(iterator.next()); diff --git a/src/main/java/net/szum123321/textile_backup/commands/permission/WhitelistCommand.java b/src/main/java/net/szum123321/textile_backup/commands/manage/WhitelistCommand.java similarity index 98% rename from src/main/java/net/szum123321/textile_backup/commands/permission/WhitelistCommand.java rename to src/main/java/net/szum123321/textile_backup/commands/manage/WhitelistCommand.java index e2bf0a2..8f5cc8f 100644 --- a/src/main/java/net/szum123321/textile_backup/commands/permission/WhitelistCommand.java +++ b/src/main/java/net/szum123321/textile_backup/commands/manage/WhitelistCommand.java @@ -1,4 +1,4 @@ -package net.szum123321.textile_backup.commands.permission; +package net.szum123321.textile_backup.commands.manage; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; diff --git a/src/main/java/net/szum123321/textile_backup/commands/restore/KillRestoreCommand.java b/src/main/java/net/szum123321/textile_backup/commands/restore/KillRestoreCommand.java index 12081a3..c6a474d 100644 --- a/src/main/java/net/szum123321/textile_backup/commands/restore/KillRestoreCommand.java +++ b/src/main/java/net/szum123321/textile_backup/commands/restore/KillRestoreCommand.java @@ -19,10 +19,13 @@ package net.szum123321.textile_backup.commands.restore; import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.ServerCommandSource; import net.szum123321.textile_backup.Statics; +import java.util.Optional; + public class KillRestoreCommand { public static LiteralArgumentBuilder register() { return CommandManager.literal("killR") @@ -30,11 +33,16 @@ public class KillRestoreCommand { if(Statics.restoreAwaitThread != null && Statics.restoreAwaitThread.isAlive()) { Statics.restoreAwaitThread.interrupt(); Statics.globalShutdownBackupFlag.set(true); - Statics.LOGGER.sendInfo(ctx.getSource(), "Backup restoration successfully stopped"); - Statics.LOGGER.info("{} cancelled backup restoration.", ctx.getSource().getEntity() != null ? + Statics.untouchableFile = Optional.empty(); + + Statics.LOGGER.info("{} cancelled backup restoration.", ctx.getSource().getEntity() instanceof PlayerEntity ? "Player: " + ctx.getSource().getName() : "SERVER" ); + + if(ctx.getSource().getEntity() instanceof PlayerEntity) + Statics.LOGGER.sendInfo(ctx.getSource(), "Backup restoration successfully stopped."); + } else { Statics.LOGGER.sendInfo(ctx.getSource(), "Failed to stop backup restoration"); } diff --git a/src/main/java/net/szum123321/textile_backup/commands/restore/RestoreBackupCommand.java b/src/main/java/net/szum123321/textile_backup/commands/restore/RestoreBackupCommand.java index c25dc4f..ad516f3 100644 --- a/src/main/java/net/szum123321/textile_backup/commands/restore/RestoreBackupCommand.java +++ b/src/main/java/net/szum123321/textile_backup/commands/restore/RestoreBackupCommand.java @@ -18,40 +18,35 @@ package net.szum123321.textile_backup.commands.restore; -import com.mojang.brigadier.LiteralMessage; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; -import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; -import com.mojang.brigadier.suggestion.SuggestionProvider; -import com.mojang.brigadier.suggestion.Suggestions; -import com.mojang.brigadier.suggestion.SuggestionsBuilder; import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.ServerCommandSource; -import net.minecraft.text.LiteralText; +import net.szum123321.textile_backup.commands.CommandExceptions; import net.szum123321.textile_backup.Statics; +import net.szum123321.textile_backup.commands.FileSuggestionProvider; +import net.szum123321.textile_backup.core.restore.RestoreContext; import net.szum123321.textile_backup.core.restore.RestoreHelper; -import java.io.File; +import javax.annotation.Nullable; import java.time.LocalDateTime; import java.time.format.DateTimeParseException; import java.util.Optional; -import java.util.concurrent.CompletableFuture; public class RestoreBackupCommand { public static LiteralArgumentBuilder register() { return CommandManager.literal("restore") .then(CommandManager.argument("file", StringArgumentType.word()) - .suggests(new FileSuggestionProvider()) + .suggests(FileSuggestionProvider.Instance()) .executes(ctx -> execute( StringArgumentType.getString(ctx, "file"), null, ctx.getSource() )) ).then(CommandManager.argument("file", StringArgumentType.word()) - .suggests(new FileSuggestionProvider()) + .suggests(FileSuggestionProvider.Instance()) .then(CommandManager.argument("comment", StringArgumentType.word()) .executes(ctx -> execute( StringArgumentType.getString(ctx, "file"), @@ -64,83 +59,48 @@ public class RestoreBackupCommand { Statics.LOGGER.sendInfo(source, "To restore given backup you have to provide exact creation time in format:"); Statics.LOGGER.sendInfo(source, "[YEAR]-[MONTH]-[DAY]_[HOUR].[MINUTE].[SECOND]"); - Statics.LOGGER.sendInfo(source, "Example: 2020-08-05_10.58.33"); + Statics.LOGGER.sendInfo(source, "Example: /backup restore 2020-08-05_10.58.33"); return 1; }); } - private static int execute(String file, String comment, ServerCommandSource source) throws CommandSyntaxException { - LocalDateTime dateTime; + private static int execute(String file, @Nullable String comment, ServerCommandSource source) throws CommandSyntaxException { + if(Statics.restoreAwaitThread == null || (Statics.restoreAwaitThread != null && !Statics.restoreAwaitThread.isAlive())) { + LocalDateTime dateTime; - try { - dateTime = LocalDateTime.from(Statics.defaultDateTimeFormatter.parse(file)); - } catch (DateTimeParseException e) { - LiteralText message = new LiteralText("An exception occurred while trying to parse:\n"); - message.append(e.getParsedString()) - .append("\n"); + try { + dateTime = LocalDateTime.from(Statics.defaultDateTimeFormatter.parse(file)); + } catch (DateTimeParseException e) { + throw CommandExceptions.DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE.create(e); + } - for(int i = 0; i < e.getErrorIndex(); i++) - message.append(" "); + Optional backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getMinecraftServer()); - message.append("^"); + if(backupFile.isPresent()) { + Statics.LOGGER.info("Found file to restore {}", backupFile.get().getFile().getName()); + } else { + Statics.LOGGER.sendInfo(source, "No file created on {} was found!", dateTime.format(Statics.defaultDateTimeFormatter)); - throw new CommandSyntaxException(new SimpleCommandExceptionType(message), message); - } + return 0; + } - Optional backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getMinecraftServer()); - - if(backupFile.isPresent()) { - Statics.LOGGER.info("Found file to restore {}", backupFile.get().getName()); - } else { - Statics.LOGGER.sendInfo(source, "No file created on {} was found!", dateTime.format(Statics.defaultDateTimeFormatter)); - - return 0; - } - - if(Statics.restoreAwaitThread == null || !Statics.restoreAwaitThread.isAlive()) { - if(source.getEntity() != null) - Statics.LOGGER.info("Backup restoration was initiated by: {}", source.getName()); - else - Statics.LOGGER.info("Backup restoration was initiated form Server Console"); - - Statics.restoreAwaitThread = RestoreHelper.create(backupFile.get(), source.getMinecraftServer(), comment); + Statics.restoreAwaitThread = RestoreHelper.create( + RestoreContext.Builder.newRestoreContextBuilder() + .setCommandSource(source) + .setFile(backupFile.get()) + .setComment(comment) + .build() + ); Statics.restoreAwaitThread.start(); - } else if(Statics.restoreAwaitThread != null && Statics.restoreAwaitThread.isAlive()) { + + return 1; + } else { Statics.LOGGER.sendInfo(source, "Someone has already started another restoration."); return 0; } - - return 1; } - private static final class FileSuggestionProvider implements SuggestionProvider { - @Override - public CompletableFuture getSuggestions(CommandContext ctx, SuggestionsBuilder builder) throws CommandSyntaxException { - String remaining = builder.getRemaining(); - - for(RestoreHelper.RestoreableFile file : RestoreHelper.getAvailableBackups(ctx.getSource().getMinecraftServer())) { - String formattedCreationTime = file.getCreationTime().format(Statics.defaultDateTimeFormatter); - - if(formattedCreationTime.startsWith(remaining)) { - if(ctx.getSource().getEntity() != null) { //was typed by player - if(file.getComment() != null) { - builder.suggest(formattedCreationTime, new LiteralMessage("Comment: " + file.getComment())); - } else { - builder.suggest(formattedCreationTime); - } - } else { //was typed from server console - if(file.getComment() != null) { - builder.suggest(file.getCreationTime() + "#" + file.getComment()); - } else { - builder.suggest(formattedCreationTime); - } - } - } - } - return builder.buildFuture(); - } - } } diff --git a/src/main/java/net/szum123321/textile_backup/core/create/compressors/LZMACompressor.java b/src/main/java/net/szum123321/textile_backup/core/ActionInitiator.java similarity index 54% rename from src/main/java/net/szum123321/textile_backup/core/create/compressors/LZMACompressor.java rename to src/main/java/net/szum123321/textile_backup/core/ActionInitiator.java index 60e535b..f965874 100644 --- a/src/main/java/net/szum123321/textile_backup/core/create/compressors/LZMACompressor.java +++ b/src/main/java/net/szum123321/textile_backup/core/ActionInitiator.java @@ -16,21 +16,29 @@ * along with this program. If not, see . */ -package net.szum123321.textile_backup.core.create.compressors; +package net.szum123321.textile_backup.core; -import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream; +public enum ActionInitiator { + Player("Player", "by"), + ServerConsole("Server Console", "from"), + Timer("Timer", "by"), + Shutdown("Server Shutdown", "by"), + Restore("Backup Restoration", "because of"), + Null("Null (That shouldn't have happened)", "form"); -import java.io.*; + private final String name; + private final String prefix; -public class LZMACompressor extends AbstractTarCompressor { - private static final LZMACompressor INSTANCE = new LZMACompressor(); + ActionInitiator(String name, String prefix) { + this.name = name; + this.prefix = prefix; + } - public static LZMACompressor getInstance() { - return INSTANCE; - } + public String getName() { + return name; + } - @Override - protected OutputStream openCompressorStream(OutputStream outputStream, int coreCountLimit) throws IOException { - return new XZCompressorOutputStream(outputStream); - } -} \ No newline at end of file + public String getPrefix() { + return prefix + ": "; + } +} diff --git a/src/main/java/net/szum123321/textile_backup/core/CustomLogger.java b/src/main/java/net/szum123321/textile_backup/core/CustomLogger.java index 4e9f2c1..b40fea2 100644 --- a/src/main/java/net/szum123321/textile_backup/core/CustomLogger.java +++ b/src/main/java/net/szum123321/textile_backup/core/CustomLogger.java @@ -18,11 +18,10 @@ package net.szum123321.textile_backup.core; -import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.text.LiteralText; import net.minecraft.text.MutableText; -import net.minecraft.text.Text; import net.minecraft.util.Formatting; import net.szum123321.textile_backup.core.create.BackupContext; import org.apache.logging.log4j.Level; @@ -30,19 +29,18 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.MessageFactory; import org.apache.logging.log4j.message.ParameterizedMessageFactory; -import org.apache.logging.log4j.spi.StandardLevel; /* - This is practically just a copy-pate of Cotton's ModLogger with few changes + This is practically just a copy-pate of Cotton's ModLogger with a few changes */ public class CustomLogger { - private final boolean isDev = FabricLoader.getInstance().isDevelopmentEnvironment(); + //private final boolean isDev = FabricLoader.getInstance().isDevelopmentEnvironment(); private final MessageFactory messageFactory; private final Logger logger; private final String prefix; - private final Text prefixText; + private final MutableText prefixText; public CustomLogger(String name, String prefix) { this.messageFactory = ParameterizedMessageFactory.INSTANCE; @@ -83,54 +81,66 @@ public class CustomLogger { log(Level.FATAL, msg, data); } - public void devError(String msg, Object... data) { - if (isDev) error(msg, data); - } - - public void devWarn(String msg, Object... data) { - if (isDev) warn(msg, data); - } - - public void devInfo(String msg, Object... data) { - if (isDev) info(msg, data); - } - - public void devDebug(String msg, Object... data) { - if (isDev) debug(msg, data); - } - - public void devTrace(String msg, Object... data) { - if(isDev) trace(msg, data); - } - - private void sendToPlayer(Level level, ServerCommandSource source, String msg, Object... args) { - if(source != null && source.getEntity() != null) { + boolean sendFeedback(Level level, ServerCommandSource source, String msg, Object... args) { + if(source != null && source.getEntity() instanceof PlayerEntity) { LiteralText text = new LiteralText(messageFactory.newMessage(msg, args).getFormattedMessage()); - if(level.intLevel() <= StandardLevel.WARN.intLevel()) + if(level.intLevel() == Level.TRACE.intLevel()) + text.formatted(Formatting.GREEN); + else if(level.intLevel() <= Level.WARN.intLevel()) text.formatted(Formatting.RED); else text.formatted(Formatting.WHITE); source.sendFeedback(prefixText.shallowCopy().append(text), false); + + return true; } else { - logger.log(level, msg, args); + log(level, msg, args); + + return false; } } - public void sendInfo(ServerCommandSource source, String msg, Object... args) { - sendToPlayer(Level.INFO, source, msg, args); + public void sendHint(ServerCommandSource source, String msg, Object... args) { + sendFeedback(Level.TRACE, source, msg, args); } - public void sendError(ServerCommandSource source, String msg, Object... args) { - sendToPlayer(Level.ERROR, source, msg, args); + public void sendInfo(ServerCommandSource source, String msg, Object... args) { + sendFeedback(Level.INFO, source, msg, args); } public void sendInfo(BackupContext context, String msg, Object... args) { sendInfo(context.getCommandSource(), msg, args); } + public void sendError(ServerCommandSource source, String msg, Object... args) { + sendFeedback(Level.ERROR, source, msg, args); + } + public void sendError(BackupContext context, String msg, Object... args) { sendError(context.getCommandSource(), msg, args); } + + public void sendToPlayerAndLog(Level level, ServerCommandSource source, String msg, Object... args) { + if(sendFeedback(level, source, msg, args)) + log(level, msg, args); + } + + //send info and log + public void sendInfoAL(ServerCommandSource source, String msg, Object... args) { + sendToPlayerAndLog(Level.INFO, source, msg, args); + } + + public void sendInfoAL(BackupContext context, String msg, Object... args) { + sendInfoAL(context.getCommandSource(), msg, args); + } + + public void sendErrorAL(ServerCommandSource source, String msg, Object... args) { + sendToPlayerAndLog(Level.ERROR, source, msg, args); + } + + public void sendErrorAL(BackupContext context, String msg, Object... args) { + sendErrorAL(context.getCommandSource(), msg, args); + } } diff --git a/src/main/java/net/szum123321/textile_backup/core/NoSpaceLeftOnDeviceException.java b/src/main/java/net/szum123321/textile_backup/core/NoSpaceLeftOnDeviceException.java new file mode 100644 index 0000000..b9158c2 --- /dev/null +++ b/src/main/java/net/szum123321/textile_backup/core/NoSpaceLeftOnDeviceException.java @@ -0,0 +1,12 @@ +package net.szum123321.textile_backup.core; + +import java.io.IOException; + +/** + * Wrapper for specific IOException. Temporary way to get more info about issue #51 + */ +public class NoSpaceLeftOnDeviceException extends IOException { + public NoSpaceLeftOnDeviceException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/net/szum123321/textile_backup/core/Utilities.java b/src/main/java/net/szum123321/textile_backup/core/Utilities.java index 56f90b1..e85ecae 100644 --- a/src/main/java/net/szum123321/textile_backup/core/Utilities.java +++ b/src/main/java/net/szum123321/textile_backup/core/Utilities.java @@ -20,10 +20,7 @@ package net.szum123321.textile_backup.core; import net.minecraft.server.MinecraftServer; import net.minecraft.server.world.ServerWorld; -import net.minecraft.util.registry.Registry; -import net.minecraft.util.registry.RegistryKey; import net.minecraft.world.World; -import net.minecraft.world.dimension.DimensionType; import net.szum123321.textile_backup.ConfigHandler; import net.szum123321.textile_backup.Statics; import net.szum123321.textile_backup.mixin.MinecraftServerSessionAccessor; @@ -33,11 +30,9 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.ZoneOffset; +import java.time.*; import java.time.format.DateTimeFormatter; +import java.util.Arrays; import java.util.Optional; public class Utilities { @@ -50,20 +45,40 @@ public class Utilities { .getSession() .getWorldDirectory(World.OVERWORLD); } + + public static File getBackupRootPath(String worldName) { + File path = new File(Statics.CONFIG.path).getAbsoluteFile(); + + if (Statics.CONFIG.perWorldBackup) + path = path.toPath().resolve(worldName).toFile(); + + if (!path.exists()) { + path.mkdirs(); + } + + return path; + } + + public static boolean isTmpAvailable() { + try { + File tmp = File.createTempFile("textile_backup_tmp_test", String.valueOf(Instant.now().getEpochSecond())); + return tmp.delete(); + } catch (IOException ignored) {} + + return false; + } public static void disableWorldSaving(MinecraftServer server) { for (ServerWorld serverWorld : server.getWorlds()) { - if (serverWorld != null && !serverWorld.savingDisabled) { + if (serverWorld != null && !serverWorld.savingDisabled) serverWorld.savingDisabled = true; - } } } public static void enableWorldSaving(MinecraftServer server) { for (ServerWorld serverWorld : server.getWorlds()) { - if (serverWorld != null && serverWorld.savingDisabled) { + if (serverWorld != null && serverWorld.savingDisabled) serverWorld.savingDisabled = false; - } } } @@ -79,41 +94,28 @@ public class Utilities { } } - for(String i : Statics.CONFIG.fileBlacklist) { - if(path.startsWith(i)) - return true; - } + for(String i : Statics.CONFIG.fileBlacklist) if(path.startsWith(i)) return true; return false; } - public static Optional getFileExtension(String fileName) { + public static Optional getArchiveExtension(String fileName) { String[] parts = fileName.split("\\."); - switch (parts[parts.length - 1]) { - case "zip": - return Optional.of(ConfigHandler.ArchiveFormat.ZIP); - case "bz2": - return Optional.of(ConfigHandler.ArchiveFormat.BZIP2); - case "gz": - return Optional.of(ConfigHandler.ArchiveFormat.GZIP); - case "xz": - return Optional.of(ConfigHandler.ArchiveFormat.LZMA); - - default: - return Optional.empty(); - } + return Arrays.stream(ConfigHandler.ArchiveFormat.values()) + .filter(format -> format.getLastPiece().equals(parts[parts.length - 1])) + .findAny(); } - public static Optional getFileExtension(File f) { - return getFileExtension(f.getName()); + public static Optional getArchiveExtension(File f) { + return getArchiveExtension(f.getName()); } public static Optional getFileCreationTime(File file) { LocalDateTime creationTime = null; - if(getFileExtension(file).isPresent()) { - String fileExtension = getFileExtension(file).get().getString(); + if(getArchiveExtension(file).isPresent()) { + String fileExtension = getArchiveExtension(file).get().getCompleteString(); try { creationTime = LocalDateTime.from( @@ -144,24 +146,13 @@ public class Utilities { return Optional.ofNullable(creationTime); } - public static File getBackupRootPath(String worldName) { - File path = new File(Statics.CONFIG.path).getAbsoluteFile(); - - if (Statics.CONFIG.perWorldBackup) - path = path.toPath().resolve(worldName).toFile(); - - if (!path.exists()) { - path.mkdirs(); - } - - return path; - } - public static boolean isValidBackup(File f) { - return getFileExtension(f).isPresent() && getFileCreationTime(f).isPresent() && isFileOk(f); + return getArchiveExtension(f).isPresent() && getFileCreationTime(f).isPresent() && isFileOk(f); } - public static boolean isFileOk(File f) {return f.exists() && f.isFile(); } + public static boolean isFileOk(File f) { + return f.exists() && f.isFile(); + } public static DateTimeFormatter getDateTimeFormatter() { return DateTimeFormatter.ofPattern(Statics.CONFIG.dateTimeFormat); @@ -174,12 +165,9 @@ public class Utilities { public static String formatDuration(Duration duration) { DateTimeFormatter formatter; - if(duration.toHours() > 0) - formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); - else if(duration.toMinutes() > 0) - formatter = DateTimeFormatter.ofPattern("mm:ss.SSS"); - else - formatter = DateTimeFormatter.ofPattern("ss.SSS"); + if(duration.toHours() > 0) formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + else if(duration.toMinutes() > 0) formatter = DateTimeFormatter.ofPattern("mm:ss.SSS"); + else formatter = DateTimeFormatter.ofPattern("ss.SSS"); return LocalTime.ofNanoOfDay(duration.toNanos()).format(formatter); } diff --git a/src/main/java/net/szum123321/textile_backup/core/create/BackupContext.java b/src/main/java/net/szum123321/textile_backup/core/create/BackupContext.java index b458d3f..ac729bc 100644 --- a/src/main/java/net/szum123321/textile_backup/core/create/BackupContext.java +++ b/src/main/java/net/szum123321/textile_backup/core/create/BackupContext.java @@ -18,18 +18,17 @@ package net.szum123321.textile_backup.core.create; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.server.MinecraftServer; import net.minecraft.server.command.ServerCommandSource; +import net.szum123321.textile_backup.core.ActionInitiator; import org.jetbrains.annotations.NotNull; -public class BackupContext { - private final MinecraftServer server; - private final ServerCommandSource commandSource; - private final BackupInitiator initiator; - private final boolean save; - private final String comment; - - protected BackupContext(@NotNull MinecraftServer server, ServerCommandSource commandSource, @NotNull BackupInitiator initiator, boolean save, String comment) { +public record BackupContext(MinecraftServer server, + ServerCommandSource commandSource, + ActionInitiator initiator, boolean save, + String comment) { + public BackupContext(@NotNull MinecraftServer server, ServerCommandSource commandSource, @NotNull ActionInitiator initiator, boolean save, String comment) { this.server = server; this.commandSource = commandSource; this.initiator = initiator; @@ -45,12 +44,12 @@ public class BackupContext { return commandSource; } - public BackupInitiator getInitiator() { + public ActionInitiator getInitiator() { return initiator; } public boolean startedByPlayer() { - return initiator == BackupInitiator.Player; + return initiator == ActionInitiator.Player; } public boolean shouldSave() { @@ -64,7 +63,7 @@ public class BackupContext { public static class Builder { private MinecraftServer server; private ServerCommandSource commandSource; - private BackupInitiator initiator; + private ActionInitiator initiator; private boolean save; private String comment; @@ -80,6 +79,10 @@ public class BackupContext { guessInitiator = false; } + public static Builder newBackupContextBuilder() { + return new Builder(); + } + public Builder setCommandSource(ServerCommandSource commandSource) { this.commandSource = commandSource; return this; @@ -90,7 +93,7 @@ public class BackupContext { return this; } - public Builder setInitiator(BackupInitiator initiator) { + public Builder setInitiator(ActionInitiator initiator) { this.initiator = initiator; return this; } @@ -111,14 +114,14 @@ public class BackupContext { } public BackupContext build() { - if(guessInitiator) { - initiator = commandSource.getEntity() == null ? BackupInitiator.ServerConsole : BackupInitiator.Player; - } else if(initiator == null) { - initiator = BackupInitiator.Null; + if (guessInitiator) { + initiator = commandSource.getEntity() instanceof PlayerEntity ? ActionInitiator.Player : ActionInitiator.ServerConsole; + } else if (initiator == null) { + initiator = ActionInitiator.Null; } - if(server == null) { - if(commandSource != null) + if (server == null) { + if (commandSource != null) setServer(commandSource.getMinecraftServer()); else throw new RuntimeException("Both MinecraftServer and ServerCommandSource weren't provided!"); @@ -128,28 +131,4 @@ public class BackupContext { } } - public enum BackupInitiator { - Player ("Player", "by"), - ServerConsole ("Server Console", "from"), - Timer ("Timer", "by"), - Shutdown ("Server Shutdown", "by"), - Restore ("Backup Restoration", "because of"), - Null ("Null (That shouldn't have happened)", "form"); - - private final String name; - private final String prefix; - - BackupInitiator(String name, String prefix) { - this.name = name; - this.prefix = prefix; - } - - public String getName() { - return name; - } - - public String getPrefix() { - return prefix + ": "; - } - } } diff --git a/src/main/java/net/szum123321/textile_backup/core/create/BackupHelper.java b/src/main/java/net/szum123321/textile_backup/core/create/BackupHelper.java index 6facc0f..d590d59 100644 --- a/src/main/java/net/szum123321/textile_backup/core/create/BackupHelper.java +++ b/src/main/java/net/szum123321/textile_backup/core/create/BackupHelper.java @@ -18,8 +18,14 @@ package net.szum123321.textile_backup.core.create; +import net.minecraft.network.MessageType; import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.LiteralText; +import net.minecraft.text.MutableText; +import net.minecraft.util.Formatting; +import net.minecraft.util.Util; import net.szum123321.textile_backup.Statics; +import net.szum123321.textile_backup.core.ActionInitiator; import net.szum123321.textile_backup.core.Utilities; import org.apache.commons.io.FileUtils; @@ -28,11 +34,12 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Arrays; import java.util.Comparator; -import java.util.Iterator; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.UUID; public class BackupHelper { public static Runnable create(BackupContext ctx) { + notifyPlayers(ctx); + StringBuilder builder = new StringBuilder(); builder.append("Backup started "); @@ -51,74 +58,81 @@ public class BackupHelper { Statics.LOGGER.info(builder.toString()); if (ctx.shouldSave()) { - Statics.LOGGER.sendInfo(ctx.getCommandSource(), "Saving server..."); - Statics.LOGGER.info( "Saving server..."); + Statics.LOGGER.sendInfoAL(ctx, "Saving server..."); - ctx.getServer().save(true, true, true); + ctx.getServer().getPlayerManager().saveAllPlayerData(); - Utilities.disableWorldSaving(ctx.getServer()); + try { + ctx.getServer().save(false, true, true); + } catch (Exception e) { + Statics.LOGGER.sendErrorAL(ctx,"An exception occurred when trying to save the world!"); + } } return new MakeBackupRunnable(ctx); } + private static void notifyPlayers(BackupContext ctx) { + MutableText message = Statics.LOGGER.getPrefixText(); + message.append(new LiteralText("Warning! Server backup will begin shortly. You may experience some lag.").formatted(Formatting.WHITE)); + + UUID uuid; + + if(ctx.getInitiator().equals(ActionInitiator.Player) && ctx.getCommandSource().getEntity() != null) + uuid = ctx.getCommandSource().getEntity().getUuid(); + else uuid = Util.NIL_UUID; + + ctx.getServer().getPlayerManager().broadcastChatMessage( + message, + MessageType.SYSTEM, + uuid + ); + } + public static int executeFileLimit(ServerCommandSource ctx, String worldName) { File root = Utilities.getBackupRootPath(worldName); - AtomicInteger deletedFiles = new AtomicInteger(); + int deletedFiles = 0; if (root.isDirectory() && root.exists() && root.listFiles() != null) { if (Statics.CONFIG.maxAge > 0) { // delete files older that configured final LocalDateTime now = LocalDateTime.now(); - Arrays.stream(root.listFiles()) + deletedFiles += Arrays.stream(root.listFiles()) .filter(Utilities::isValidBackup)// We check if we can get file's creation date so that the next line won't throw an exception .filter(f -> now.toEpochSecond(ZoneOffset.UTC) - Utilities.getFileCreationTime(f).get().toEpochSecond(ZoneOffset.UTC) > Statics.CONFIG.maxAge) - .forEach(f -> { - if(deleteFile(f, ctx)) - deletedFiles.getAndIncrement(); - }); + .map(f -> deleteFile(f, ctx)) + .filter(b -> b).count(); //a bit awkward } if (Statics.CONFIG.backupsToKeep > 0 && root.listFiles().length > Statics.CONFIG.backupsToKeep) { - int i = root.listFiles().length; - - Iterator it = Arrays.stream(root.listFiles()) + deletedFiles += Arrays.stream(root.listFiles()) .filter(Utilities::isValidBackup) - .sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get())) - .iterator(); - - while(i > Statics.CONFIG.backupsToKeep && it.hasNext()) { - if(deleteFile(it.next(), ctx)) - deletedFiles.getAndIncrement(); - - i--; - } + .sorted(Comparator.comparing(f -> Utilities.getFileCreationTime((File) f).get()).reversed()) + .skip(Statics.CONFIG.backupsToKeep) + .map(f -> deleteFile(f, ctx)) + .filter(b -> b).count(); } if (Statics.CONFIG.maxSize > 0 && FileUtils.sizeOfDirectory(root) / 1024 > Statics.CONFIG.maxSize) { - Iterator it = Arrays.stream(root.listFiles()) + deletedFiles += Arrays.stream(root.listFiles()) .filter(Utilities::isValidBackup) .sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get())) - .iterator(); - - while(FileUtils.sizeOfDirectory(root) / 1024 > Statics.CONFIG.maxSize && it.hasNext()) { - if(deleteFile(it.next(), ctx)) - deletedFiles.getAndIncrement(); - } + .takeWhile(f -> FileUtils.sizeOfDirectory(root) / 1024 > Statics.CONFIG.maxSize) + .map(f -> deleteFile(f, ctx)) + .filter(b -> b).count(); } } - return deletedFiles.get(); + return deletedFiles; } private static boolean deleteFile(File f, ServerCommandSource ctx) { - if(f != Statics.untouchableFile) { + if(Statics.untouchableFile.isEmpty()|| !Statics.untouchableFile.get().equals(f)) { if(f.delete()) { - Statics.LOGGER.sendInfo(ctx, "Deleting: {}", f.getName()); - Statics.LOGGER.info("Deleting: {}", f.getName()); + Statics.LOGGER.sendInfoAL(ctx, "Deleting: {}", f.getName()); return true; } else { - Statics.LOGGER.sendError(ctx, "Something went wrong while deleting: {}.", f.getName()); + Statics.LOGGER.sendErrorAL(ctx, "Something went wrong while deleting: {}.", f.getName()); } } diff --git a/src/main/java/net/szum123321/textile_backup/core/create/BackupScheduler.java b/src/main/java/net/szum123321/textile_backup/core/create/BackupScheduler.java index 753ca94..8ff78a9 100644 --- a/src/main/java/net/szum123321/textile_backup/core/create/BackupScheduler.java +++ b/src/main/java/net/szum123321/textile_backup/core/create/BackupScheduler.java @@ -20,6 +20,7 @@ package net.szum123321.textile_backup.core.create; import net.minecraft.server.MinecraftServer; import net.szum123321.textile_backup.Statics; +import net.szum123321.textile_backup.core.ActionInitiator; import java.time.Instant; @@ -40,9 +41,10 @@ public class BackupScheduler { if(nextBackup <= now) { Statics.executorService.submit( BackupHelper.create( - new BackupContext.Builder() + BackupContext.Builder + .newBackupContextBuilder() .setServer(server) - .setInitiator(BackupContext.BackupInitiator.Timer) + .setInitiator(ActionInitiator.Timer) .saveServer() .build() ) @@ -58,9 +60,10 @@ public class BackupScheduler { if(scheduled && nextBackup <= now) { Statics.executorService.submit( BackupHelper.create( - new BackupContext.Builder() + BackupContext.Builder + .newBackupContextBuilder() .setServer(server) - .setInitiator(BackupContext.BackupInitiator.Timer) + .setInitiator(ActionInitiator.Timer) .saveServer() .build() ) diff --git a/src/main/java/net/szum123321/textile_backup/core/create/MakeBackupRunnable.java b/src/main/java/net/szum123321/textile_backup/core/create/MakeBackupRunnable.java index fc0e049..5138a37 100644 --- a/src/main/java/net/szum123321/textile_backup/core/create/MakeBackupRunnable.java +++ b/src/main/java/net/szum123321/textile_backup/core/create/MakeBackupRunnable.java @@ -19,11 +19,17 @@ package net.szum123321.textile_backup.core.create; import net.szum123321.textile_backup.Statics; +import net.szum123321.textile_backup.core.ActionInitiator; import net.szum123321.textile_backup.core.create.compressors.*; import net.szum123321.textile_backup.core.Utilities; +import net.szum123321.textile_backup.core.create.compressors.tar.AbstractTarArchiver; +import net.szum123321.textile_backup.core.create.compressors.tar.ParallelBZip2Compressor; +import net.szum123321.textile_backup.core.create.compressors.tar.ParallelGzipCompressor; +import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream; import java.io.File; import java.io.IOException; +import java.io.OutputStream; import java.time.LocalDateTime; public class MakeBackupRunnable implements Runnable { @@ -36,8 +42,10 @@ public class MakeBackupRunnable implements Runnable { @Override public void run() { try { - Statics.LOGGER.sendInfo(context.getCommandSource(), "Starting backup"); - Statics.LOGGER.info("Starting backup"); + Utilities.disableWorldSaving(context.getServer()); + Statics.disableWatchdog = true; + + Statics.LOGGER.sendInfoAL(context, "Starting backup"); File world = Utilities.getWorldFolder(context.getServer()); @@ -57,7 +65,9 @@ public class MakeBackupRunnable implements Runnable { outFile.createNewFile(); } catch (IOException e) { Statics.LOGGER.error("An exception occurred when trying to create new backup file!", e); - Statics.LOGGER.sendError(context.getCommandSource(), "An exception occurred when trying to create new backup file!"); + + if(context.getInitiator() == ActionInitiator.Player) + Statics.LOGGER.sendError(context, "An exception occurred when trying to create new backup file!"); return; } @@ -70,39 +80,37 @@ public class MakeBackupRunnable implements Runnable { coreCount = Math.min(Statics.CONFIG.compressionCoreCountLimit, Runtime.getRuntime().availableProcessors()); } - Statics.LOGGER.trace("Running compression on {} threads. Available cores = {}", coreCount, Runtime.getRuntime().availableProcessors()); + Statics.LOGGER.trace("Running compression on {} threads. Available cores: {}", coreCount, Runtime.getRuntime().availableProcessors()); switch (Statics.CONFIG.format) { - case ZIP: - ParallelZipCompressor.createArchive(world, outFile, context, coreCount); - break; - - case BZIP2: - ParallelBZip2Compressor.getInstance().createArchive(world, outFile, context, coreCount); - break; - - case GZIP: - ParallelGzipCompressor.getInstance().createArchive(world, outFile, context, coreCount); - break; - - case LZMA: - LZMACompressor.getInstance().createArchive(world, outFile, context, coreCount); - break; - - default: + case ZIP -> { + if (Statics.tmpAvailable && coreCount > 1) + ParallelZipCompressor.getInstance().createArchive(world, outFile, context, coreCount); + else + ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount); + } + case BZIP2 -> ParallelBZip2Compressor.getInstance().createArchive(world, outFile, context, coreCount); + case GZIP -> ParallelGzipCompressor.getInstance().createArchive(world, outFile, context, coreCount); + case LZMA -> new AbstractTarArchiver() { + protected OutputStream getCompressorOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException { + return new LZMACompressorOutputStream(stream); + } + }.createArchive(world, outFile, context, coreCount); + case TAR -> new AbstractTarArchiver().createArchive(world, outFile, context, coreCount); + default -> { Statics.LOGGER.warn("Specified compressor ({}) is not supported! Zip will be used instead!", Statics.CONFIG.format); - Statics.LOGGER.sendError(context.getCommandSource(), "Error! No correct compression format specified! Using default compressor!"); - - ParallelZipCompressor.createArchive(world, outFile, context, coreCount); - break; + if (context.getInitiator() == ActionInitiator.Player) + Statics.LOGGER.sendError(context.getCommandSource(), "Error! No correct compression format specified! Using default compressor!"); + ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount); + } } BackupHelper.executeFileLimit(context.getCommandSource(), Utilities.getLevelName(context.getServer())); - Statics.LOGGER.sendInfo(context, "Done!"); - Statics.LOGGER.info("Done!"); + Statics.LOGGER.sendInfoAL(context, "Done!"); } finally { Utilities.enableWorldSaving(context.getServer()); + Statics.disableWatchdog = false; } } @@ -111,6 +119,6 @@ public class MakeBackupRunnable implements Runnable { return Utilities.getDateTimeFormatter().format(now) + (context.getComment() != null ? "#" + context.getComment().replace("#", "") : "") + - Statics.CONFIG.format.getString(); + Statics.CONFIG.format.getCompleteString(); } } diff --git a/src/main/java/net/szum123321/textile_backup/core/create/compressors/AbstractCompressor.java b/src/main/java/net/szum123321/textile_backup/core/create/compressors/AbstractCompressor.java new file mode 100644 index 0000000..a4634d0 --- /dev/null +++ b/src/main/java/net/szum123321/textile_backup/core/create/compressors/AbstractCompressor.java @@ -0,0 +1,92 @@ +/* + * A simple backup mod for Fabric + * Copyright (C) 2020 Szum123321 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.szum123321.textile_backup.core.create.compressors; + +import net.szum123321.textile_backup.Statics; +import net.szum123321.textile_backup.core.ActionInitiator; +import net.szum123321.textile_backup.core.NoSpaceLeftOnDeviceException; +import net.szum123321.textile_backup.core.Utilities; +import net.szum123321.textile_backup.core.create.BackupContext; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.ExecutionException; + +public abstract class AbstractCompressor { + public void createArchive(File inputFile, File outputFile, BackupContext ctx, int coreLimit) { + Instant start = Instant.now(); + + try (FileOutputStream outStream = new FileOutputStream(outputFile); + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outStream); + OutputStream arc = createArchiveOutputStream(bufferedOutputStream, ctx, coreLimit)) { + + Files.walk(inputFile.toPath()) + .filter(path -> !Utilities.isBlacklisted(inputFile.toPath().relativize(path))) + .map(Path::toFile) + .filter(File::isFile) + .forEach(file -> { + try { + //hopefully one broken file won't spoil the whole archive + addEntry(file, inputFile.toPath().relativize(file.toPath()).toString(), arc); + } catch (IOException e) { + Statics.LOGGER.error("An exception occurred while trying to compress: {}", inputFile.toPath().relativize(file.toPath()).toString(), e); + + if (ctx.getInitiator() == ActionInitiator.Player) + Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!"); + } + }); + + finish(arc); + } catch(NoSpaceLeftOnDeviceException e) { + Statics.LOGGER.error("CRITICAL ERROR OCCURRED!"); + Statics.LOGGER.error("The backup is corrupted."); + Statics.LOGGER.error("Don't panic! This is a known issue!"); + Statics.LOGGER.error("For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems"); + Statics.LOGGER.error("In case this isn't it here's also the exception itself!", e); + + if(ctx.getInitiator() == ActionInitiator.Player) { + Statics.LOGGER.sendError(ctx, "Backup failed. The file is corrupt."); + Statics.LOGGER.error("For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems"); + } + } catch (IOException | InterruptedException | ExecutionException e) { + Statics.LOGGER.error("An exception occurred!", e); + } catch (Exception e) { + if(ctx.getInitiator() == ActionInitiator.Player) + Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!"); + } + + close(); + + Statics.LOGGER.sendInfoAL(ctx, "Compression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now()))); + } + + protected abstract OutputStream createArchiveOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException; + protected abstract void addEntry(File file, String entryName, OutputStream arc) throws IOException; + + protected void finish(OutputStream arc) throws InterruptedException, ExecutionException, IOException { + ;//Basically this function is only needed for the ParallelZipCompressor to write out ParallelScatterZipCreator + } + + protected void close() { + ;//Same as above, just for ParallelGzipCompressor to shutdown ExecutorService + } +} diff --git a/src/main/java/net/szum123321/textile_backup/core/create/compressors/AbstractTarCompressor.java b/src/main/java/net/szum123321/textile_backup/core/create/compressors/AbstractTarCompressor.java deleted file mode 100644 index 06bea66..0000000 --- a/src/main/java/net/szum123321/textile_backup/core/create/compressors/AbstractTarCompressor.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * A simple backup mod for Fabric - * Copyright (C) 2020 Szum123321 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.szum123321.textile_backup.core.create.compressors; - -import net.szum123321.textile_backup.Statics; -import net.szum123321.textile_backup.core.Utilities; -import net.szum123321.textile_backup.core.create.BackupContext; -import org.apache.commons.compress.archivers.ArchiveEntry; -import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; -import org.apache.commons.compress.utils.IOUtils; - -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Duration; -import java.time.Instant; - -public abstract class AbstractTarCompressor { - protected abstract OutputStream openCompressorStream(OutputStream outputStream, int coreCountLimit) throws IOException; - - public void createArchive(File inputFile, File outputFile, BackupContext ctx, int coreLimit) { - Statics.LOGGER.sendInfo(ctx, "Starting compression..."); - - Instant start = Instant.now(); - - try (FileOutputStream outStream = new FileOutputStream(outputFile); - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outStream); - OutputStream compressorOutputStream = openCompressorStream(bufferedOutputStream, coreLimit); - TarArchiveOutputStream arc = new TarArchiveOutputStream(compressorOutputStream)) { - arc.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); - arc.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); - - Files.walk(inputFile.toPath()) - .filter(path -> !Utilities.isBlacklisted(inputFile.toPath().relativize(path))) - .map(Path::toFile) - .filter(File::isFile) - .forEach(file -> { - try (FileInputStream fileInputStream = new FileInputStream(file)){ - ArchiveEntry entry = arc.createArchiveEntry(file, inputFile.toPath().relativize(file.toPath()).toString()); - arc.putArchiveEntry(entry); - - IOUtils.copy(fileInputStream, arc); - - arc.closeArchiveEntry(); - } catch (IOException e) { - Statics.LOGGER.error("An exception occurred while trying to compress: {}", file.getName(), e); - Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!"); - } - }); - } catch (IOException e) { - Statics.LOGGER.error("An exception occurred!", e); - Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!"); - } - - Statics.LOGGER.sendInfo(ctx, "Compression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now()))); - } -} diff --git a/src/main/java/net/szum123321/textile_backup/core/create/compressors/ParallelZipCompressor.java b/src/main/java/net/szum123321/textile_backup/core/create/compressors/ParallelZipCompressor.java index 6923424..b7a0c9e 100644 --- a/src/main/java/net/szum123321/textile_backup/core/create/compressors/ParallelZipCompressor.java +++ b/src/main/java/net/szum123321/textile_backup/core/create/compressors/ParallelZipCompressor.java @@ -19,17 +19,13 @@ package net.szum123321.textile_backup.core.create.compressors; import net.szum123321.textile_backup.Statics; -import net.szum123321.textile_backup.core.Utilities; +import net.szum123321.textile_backup.core.NoSpaceLeftOnDeviceException; import net.szum123321.textile_backup.core.create.BackupContext; import org.apache.commons.compress.archivers.zip.*; import org.apache.commons.compress.parallel.InputStreamSupplier; import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalDateTime; +import java.util.Objects; import java.util.concurrent.*; import java.util.zip.ZipEntry; @@ -39,66 +35,105 @@ import java.util.zip.ZipEntry; answer by: https://stackoverflow.com/users/2987755/dkb */ -public class ParallelZipCompressor { - public static void createArchive(File inputFile, File outputFile, BackupContext ctx, int coreLimit) { - Statics.LOGGER.sendInfo(ctx, "Starting compression..."); +public class ParallelZipCompressor extends ZipCompressor { + //These fields are used to discriminate against the issue #51 + private final static SimpleStackTraceElement[] STACKTRACE = { + new SimpleStackTraceElement("sun.nio.ch.FileDispatcherImpl", "write0", true), + new SimpleStackTraceElement("sun.nio.ch.FileDispatcherImpl", "write", false), + new SimpleStackTraceElement("sun.nio.ch.IOUtil", "writeFromNativeBuffer", false), + new SimpleStackTraceElement("sun.nio.ch.IOUtil", "write", false), + new SimpleStackTraceElement("sun.nio.ch.FileChannelImpl", "write", false), + new SimpleStackTraceElement("java.nio.channels.Channels", "writeFullyImpl", false), + new SimpleStackTraceElement("java.nio.channels.Channels", "writeFully", false), + new SimpleStackTraceElement("java.nio.channels.Channels$1", "write", false), + new SimpleStackTraceElement("org.apache.commons.compress.parallel.FileBasedScatterGatherBackingStore", "writeOut", false) + }; - Instant start = Instant.now(); + private ParallelScatterZipCreator scatterZipCreator; - Path rootPath = inputFile.toPath(); - - try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile); - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream); - ZipArchiveOutputStream arc = new ZipArchiveOutputStream(bufferedOutputStream)) { - - ParallelScatterZipCreator scatterZipCreator = new ParallelScatterZipCreator(Executors.newFixedThreadPool(coreLimit)); - - arc.setMethod(ZipArchiveOutputStream.DEFLATED); - arc.setUseZip64(Zip64Mode.AsNeeded); - arc.setLevel(Statics.CONFIG.compression); - arc.setComment("Created on: " + Utilities.getDateTimeFormatter().format(LocalDateTime.now())); - - Files.walk(inputFile.toPath()) - .filter(path -> !Utilities.isBlacklisted(inputFile.toPath().relativize(path))) - .map(Path::toFile) - .filter(File::isFile) - .forEach(file -> { - try { //IOException gets thrown only when arc is closed - ZipArchiveEntry entry = (ZipArchiveEntry)arc.createArchiveEntry(file, rootPath.relativize(file.toPath()).toString()); - - entry.setMethod(ZipEntry.DEFLATED); - scatterZipCreator.addArchiveEntry(entry, new FileInputStreamSupplier(file)); - } catch (IOException e) { - Statics.LOGGER.error("An exception occurred while trying to compress: {}", file.getName(), e); - Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!"); - } - }); - - scatterZipCreator.writeTo(arc); - } catch (IOException | InterruptedException | ExecutionException e) { - Statics.LOGGER.error("An exception occurred!", e); - Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!"); - } - - Statics.LOGGER.sendInfo(ctx, "Compression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now()))); + public static ParallelZipCompressor getInstance() { + return new ParallelZipCompressor(); } - static class FileInputStreamSupplier implements InputStreamSupplier { - private final File sourceFile; - private InputStream stream; + @Override + protected OutputStream createArchiveOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) { + scatterZipCreator = new ParallelScatterZipCreator(Executors.newFixedThreadPool(coreLimit)); + return super.createArchiveOutputStream(stream, ctx, coreLimit); + } - FileInputStreamSupplier(File sourceFile) { - this.sourceFile = sourceFile; - } + @Override + protected void addEntry(File file, String entryName, OutputStream arc) throws IOException { + ZipArchiveEntry entry = (ZipArchiveEntry)((ZipArchiveOutputStream)arc).createArchiveEntry(file, entryName); - public InputStream get() { - try { - stream = new BufferedInputStream(new FileInputStream(sourceFile)); - } catch (IOException e) { - Statics.LOGGER.error("An exception occurred while trying to create input stream!", e); + if(ZipCompressor.isDotDat(file.getName())) { + entry.setMethod(ZipArchiveOutputStream.STORED); + entry.setSize(file.length()); + entry.setCompressedSize(file.length()); + entry.setCrc(getCRC(file)); + } else entry.setMethod(ZipEntry.DEFLATED); + + entry.setTime(System.currentTimeMillis()); + + scatterZipCreator.addArchiveEntry(entry, new FileInputStreamSupplier(file)); + } + + @Override + protected void finish(OutputStream arc) throws InterruptedException, IOException, ExecutionException { + /* + This is perhaps the most dreadful line of this whole mess + This line causes the infamous Out of space error + */ + try { + scatterZipCreator.writeTo((ZipArchiveOutputStream) arc); + } catch (ExecutionException e) { + Throwable cause; + if((cause = e.getCause()).getClass().equals(IOException.class)) { + //The out of space exception is thrown at sun.nio.ch.FileDispatcherImpl.write0(Native Method) + boolean match = (cause.getStackTrace().length >= STACKTRACE.length); + if(match) { + for(int i = 0; i < STACKTRACE.length && match; i++) + if(!STACKTRACE[i].equals(cause.getStackTrace()[i])) { + //Statics.LOGGER.error("Mismatch at: {}, classname: {}, methodname: {}, {}", i, cause.getStackTrace()[i].getClassName(), cause.getStackTrace()[i].getMethodName()); + match = false; + } + + //For clarity sake let's not throw the ExecutionException itself rather only the cause, as the EE is just the wrapper + if(match) throw new NoSpaceLeftOnDeviceException(cause); + } } - return stream; + throw e; + } + } + + private static record SimpleStackTraceElement ( + String className, + String methodName, + boolean isNative + ) { + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + if(o.getClass() == StackTraceElement.class) { + StackTraceElement that = (StackTraceElement) o; + return (isNative == that.isNativeMethod()) && Objects.equals(className, that.getClassName()) && Objects.equals(methodName, that.getMethodName()); + } + if(getClass() != o.getClass()) return false; + SimpleStackTraceElement that = (SimpleStackTraceElement) o; + return isNative == that.isNative && Objects.equals(className, that.className) && Objects.equals(methodName, that.methodName); + } + } + + record FileInputStreamSupplier(File sourceFile) implements InputStreamSupplier { + public InputStream get() { + try { + return new FileInputStream(sourceFile); + } catch (IOException e) { + Statics.LOGGER.error("An exception occurred while trying to create an input stream from file: {}!", sourceFile.getName(), e); + } + + return null; } } } diff --git a/src/main/java/net/szum123321/textile_backup/core/create/compressors/ZipCompressor.java b/src/main/java/net/szum123321/textile_backup/core/create/compressors/ZipCompressor.java new file mode 100644 index 0000000..46f2c35 --- /dev/null +++ b/src/main/java/net/szum123321/textile_backup/core/create/compressors/ZipCompressor.java @@ -0,0 +1,91 @@ +/* + * A simple backup mod for Fabric + * Copyright (C) 2020 Szum123321 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.szum123321.textile_backup.core.create.compressors; + +import net.szum123321.textile_backup.Statics; +import net.szum123321.textile_backup.core.Utilities; +import net.szum123321.textile_backup.core.create.BackupContext; +import org.apache.commons.compress.archivers.zip.Zip64Mode; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.apache.commons.compress.utils.IOUtils; + +import java.io.*; +import java.nio.file.Files; +import java.time.LocalDateTime; +import java.util.zip.CRC32; +import java.util.zip.Checksum; + +public class ZipCompressor extends AbstractCompressor { + public static ZipCompressor getInstance() { + return new ZipCompressor(); + } + + @Override + protected OutputStream createArchiveOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) { + ZipArchiveOutputStream arc = new ZipArchiveOutputStream(stream); + + arc.setMethod(ZipArchiveOutputStream.DEFLATED); + arc.setUseZip64(Zip64Mode.AsNeeded); + arc.setLevel(Statics.CONFIG.compression); + arc.setComment("Created on: " + Utilities.getDateTimeFormatter().format(LocalDateTime.now())); + + return arc; + } + + @Override + protected void addEntry(File file, String entryName, OutputStream arc) throws IOException { + try (FileInputStream fileInputStream = new FileInputStream(file)){ + ZipArchiveEntry entry = (ZipArchiveEntry)((ZipArchiveOutputStream)arc).createArchiveEntry(file, entryName); + + if(isDotDat(file.getName())) { + entry.setMethod(ZipArchiveOutputStream.STORED); + entry.setSize(file.length()); + entry.setCompressedSize(file.length()); + entry.setCrc(getCRC(file)); + } + + ((ZipArchiveOutputStream)arc).putArchiveEntry(entry); + + IOUtils.copy(fileInputStream, arc); + + ((ZipArchiveOutputStream)arc).closeArchiveEntry(); + } + } + + //*.dat files are already compressed with gzip which uses the same algorithm as zip so there's no point in compressing it again + protected static boolean isDotDat(String filename) { + String[] arr = filename.split("\\."); + return arr[arr.length - 1].contains("dat"); //includes dat_old + } + + protected static long getCRC(File file) throws IOException { + Checksum sum = new CRC32(); + byte[] buffer = new byte[8192]; + int len; + + try (InputStream stream = new FileInputStream(file)) { + while ((len = stream.read(buffer)) != -1) sum.update(buffer, 0, len); + } catch (IOException e) { + throw new IOException("Error while calculating CRC of: " + file.getAbsolutePath(), e); + } + + return sum.getValue(); + } +} diff --git a/src/main/java/net/szum123321/textile_backup/core/create/compressors/tar/AbstractTarArchiver.java b/src/main/java/net/szum123321/textile_backup/core/create/compressors/tar/AbstractTarArchiver.java new file mode 100644 index 0000000..fe066bb --- /dev/null +++ b/src/main/java/net/szum123321/textile_backup/core/create/compressors/tar/AbstractTarArchiver.java @@ -0,0 +1,57 @@ +/* + * A simple backup mod for Fabric + * Copyright (C) 2020 Szum123321 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.szum123321.textile_backup.core.create.compressors.tar; + +import net.szum123321.textile_backup.core.create.BackupContext; +import net.szum123321.textile_backup.core.create.compressors.AbstractCompressor; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.utils.IOUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class AbstractTarArchiver extends AbstractCompressor { + protected OutputStream getCompressorOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException { + return stream; + } + + @Override + protected OutputStream createArchiveOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException { + TarArchiveOutputStream tar = new TarArchiveOutputStream(getCompressorOutputStream(stream, ctx, coreLimit)); + tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + tar.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); + + return tar; + } + + @Override + protected void addEntry(File file, String entryName, OutputStream arc) throws IOException { + try (FileInputStream fileInputStream = new FileInputStream(file)){ + TarArchiveEntry entry = (TarArchiveEntry)((TarArchiveOutputStream) arc).createArchiveEntry(file, entryName); + ((TarArchiveOutputStream)arc).putArchiveEntry(entry); + + IOUtils.copy(fileInputStream, arc); + + ((TarArchiveOutputStream)arc).closeArchiveEntry(); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/szum123321/textile_backup/core/create/compressors/ParallelBZip2Compressor.java b/src/main/java/net/szum123321/textile_backup/core/create/compressors/tar/ParallelBZip2Compressor.java similarity index 65% rename from src/main/java/net/szum123321/textile_backup/core/create/compressors/ParallelBZip2Compressor.java rename to src/main/java/net/szum123321/textile_backup/core/create/compressors/tar/ParallelBZip2Compressor.java index b938f3f..e3e9e05 100644 --- a/src/main/java/net/szum123321/textile_backup/core/create/compressors/ParallelBZip2Compressor.java +++ b/src/main/java/net/szum123321/textile_backup/core/create/compressors/tar/ParallelBZip2Compressor.java @@ -16,22 +16,21 @@ * along with this program. If not, see . */ -package net.szum123321.textile_backup.core.create.compressors; +package net.szum123321.textile_backup.core.create.compressors.tar; +import net.szum123321.textile_backup.core.create.BackupContext; import org.at4j.comp.bzip2.BZip2OutputStream; import org.at4j.comp.bzip2.BZip2OutputStreamSettings; import java.io.*; -public class ParallelBZip2Compressor extends AbstractTarCompressor { - private static final ParallelBZip2Compressor INSTANCE = new ParallelBZip2Compressor(); - +public class ParallelBZip2Compressor extends AbstractTarArchiver { public static ParallelBZip2Compressor getInstance() { - return INSTANCE; + return new ParallelBZip2Compressor(); } @Override - protected OutputStream openCompressorStream(OutputStream outputStream, int coreCountLimit) throws IOException { - return new BZip2OutputStream(outputStream, new BZip2OutputStreamSettings().setNumberOfEncoderThreads(coreCountLimit)); + protected OutputStream getCompressorOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException { + return new BZip2OutputStream(stream, new BZip2OutputStreamSettings().setNumberOfEncoderThreads(coreLimit)); } } \ No newline at end of file diff --git a/src/main/java/net/szum123321/textile_backup/core/create/compressors/ParallelGzipCompressor.java b/src/main/java/net/szum123321/textile_backup/core/create/compressors/tar/ParallelGzipCompressor.java similarity index 54% rename from src/main/java/net/szum123321/textile_backup/core/create/compressors/ParallelGzipCompressor.java rename to src/main/java/net/szum123321/textile_backup/core/create/compressors/tar/ParallelGzipCompressor.java index 5ba0f3a..dcf9f1c 100644 --- a/src/main/java/net/szum123321/textile_backup/core/create/compressors/ParallelGzipCompressor.java +++ b/src/main/java/net/szum123321/textile_backup/core/create/compressors/tar/ParallelGzipCompressor.java @@ -16,22 +16,32 @@ * along with this program. If not, see . */ -package net.szum123321.textile_backup.core.create.compressors; +package net.szum123321.textile_backup.core.create.compressors.tar; +import net.szum123321.textile_backup.core.create.BackupContext; import org.anarres.parallelgzip.ParallelGZIPOutputStream; import java.io.*; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -public class ParallelGzipCompressor extends AbstractTarCompressor { - private static final ParallelGzipCompressor INSTANCE = new ParallelGzipCompressor(); +public class ParallelGzipCompressor extends AbstractTarArchiver { + private ExecutorService executorService; public static ParallelGzipCompressor getInstance() { - return INSTANCE; + return new ParallelGzipCompressor(); } @Override - protected OutputStream openCompressorStream(OutputStream outputStream, int coreCountLimit) throws IOException { - return new ParallelGZIPOutputStream(outputStream, Executors.newFixedThreadPool(coreCountLimit)); + protected OutputStream getCompressorOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException { + executorService = Executors.newFixedThreadPool(coreLimit); + + return new ParallelGZIPOutputStream(stream, executorService); + } + + @Override + protected void close() { + //it seems like ParallelGZIPOutputStream doesn't shut down its ExecutorService, so to not leave garbage I shut it down + executorService.shutdown(); } } diff --git a/src/main/java/net/szum123321/textile_backup/core/restore/AwaitThread.java b/src/main/java/net/szum123321/textile_backup/core/restore/AwaitThread.java index 27bb8cc..6c60ab7 100644 --- a/src/main/java/net/szum123321/textile_backup/core/restore/AwaitThread.java +++ b/src/main/java/net/szum123321/textile_backup/core/restore/AwaitThread.java @@ -20,14 +20,20 @@ package net.szum123321.textile_backup.core.restore; import net.szum123321.textile_backup.Statics; +import java.util.concurrent.atomic.AtomicInteger; + /* This thread waits some amount of time and then starts a new, independent thread */ public class AwaitThread extends Thread { + private final static AtomicInteger threadCounter = new AtomicInteger(0); + private final int delay; + private final int thisThreadId = threadCounter.getAndIncrement(); private final Runnable taskRunnable; public AwaitThread(int delay, Runnable taskRunnable) { + this.setName("Textile Backup await thread nr. " + thisThreadId); this.delay = delay; this.taskRunnable = taskRunnable; } @@ -38,7 +44,7 @@ public class AwaitThread extends Thread { // 𝄞 This is final count down! Tu ruru Tu, Tu Ru Tu Tu ♪ try { - Thread.sleep(delay * 1000); + Thread.sleep(delay * 1000L); } catch (InterruptedException e) { Statics.LOGGER.info("Backup restoration cancelled."); return; @@ -49,6 +55,6 @@ public class AwaitThread extends Thread { But still it's farewell And maybe we'll come back */ - new Thread(taskRunnable).start(); + new Thread(taskRunnable, "Textile Backup restore thread nr. " + thisThreadId).start(); } } diff --git a/src/main/java/net/szum123321/textile_backup/core/restore/RestoreBackupRunnable.java b/src/main/java/net/szum123321/textile_backup/core/restore/RestoreBackupRunnable.java index 8336591..5cbaca3 100644 --- a/src/main/java/net/szum123321/textile_backup/core/restore/RestoreBackupRunnable.java +++ b/src/main/java/net/szum123321/textile_backup/core/restore/RestoreBackupRunnable.java @@ -18,8 +18,8 @@ package net.szum123321.textile_backup.core.restore; -import net.minecraft.server.MinecraftServer; import net.szum123321.textile_backup.ConfigHandler; +import net.szum123321.textile_backup.core.ActionInitiator; import net.szum123321.textile_backup.core.LivingServer; import net.szum123321.textile_backup.Statics; import net.szum123321.textile_backup.core.Utilities; @@ -29,38 +29,38 @@ import net.szum123321.textile_backup.core.restore.decompressors.GenericTarDecomp import net.szum123321.textile_backup.core.restore.decompressors.ZipDecompressor; import java.io.File; -import java.util.NoSuchElementException; public class RestoreBackupRunnable implements Runnable { - private final MinecraftServer server; - private final File backupFile; - private final String finalBackupComment; + private final RestoreContext ctx; - public RestoreBackupRunnable(MinecraftServer server, File backupFile, String finalBackupComment) { - this.server = server; - this.backupFile = backupFile; - this.finalBackupComment = finalBackupComment; + public RestoreBackupRunnable(RestoreContext ctx) { + this.ctx = ctx; } @Override public void run() { + Statics.globalShutdownBackupFlag.set(false); + Statics.LOGGER.info("Shutting down server..."); - server.stop(false); + + ctx.getServer().stop(false); awaitServerShutdown(); if(Statics.CONFIG.backupOldWorlds) { BackupHelper.create( - new BackupContext.Builder() - .setServer(server) - .setInitiator(BackupContext.BackupInitiator.Restore) - .setComment("Old_World" + (finalBackupComment != null ? "_" + finalBackupComment : "")) + BackupContext.Builder + .newBackupContextBuilder() + .setServer(ctx.getServer()) + .setInitiator(ActionInitiator.Restore) + .setComment("Old_World" + (ctx.getComment() != null ? "_" + ctx.getComment() : "")) .build() ).run(); } - File worldFile = Utilities.getWorldFolder(server); + File worldFile = Utilities.getWorldFolder(ctx.getServer()); Statics.LOGGER.info("Deleting old world..."); + if(!deleteDirectory(worldFile)) Statics.LOGGER.error("Something went wrong while deleting old world!"); @@ -68,24 +68,26 @@ public class RestoreBackupRunnable implements Runnable { Statics.LOGGER.info("Starting decompression..."); - if(Utilities.getFileExtension(backupFile).orElseThrow(() -> new NoSuchElementException("Couldn't get file extension!")) == ConfigHandler.ArchiveFormat.ZIP) { - ZipDecompressor.decompress(backupFile, worldFile); - } else { - GenericTarDecompressor.decompress(backupFile, worldFile); - } + if(ctx.getFile().getArchiveFormat() == ConfigHandler.ArchiveFormat.ZIP) + ZipDecompressor.decompress(ctx.getFile().getFile(), worldFile); + else + GenericTarDecompressor.decompress(ctx.getFile().getFile(), worldFile); if(Statics.CONFIG.deleteOldBackupAfterRestore) { Statics.LOGGER.info("Deleting old backup"); - if(!backupFile.delete()) + if(!ctx.getFile().getFile().delete()) Statics.LOGGER.info("Something went wrong while deleting old backup"); } + //in case we're playing on client + Statics.globalShutdownBackupFlag.set(true); + Statics.LOGGER.info("Done!"); } private void awaitServerShutdown() { - while(((LivingServer)server).isAlive()) { + while(((LivingServer)ctx.getServer()).isAlive()) { try { Thread.sleep(100); } catch (InterruptedException e) { diff --git a/src/main/java/net/szum123321/textile_backup/core/restore/RestoreContext.java b/src/main/java/net/szum123321/textile_backup/core/restore/RestoreContext.java new file mode 100644 index 0000000..a3719bd --- /dev/null +++ b/src/main/java/net/szum123321/textile_backup/core/restore/RestoreContext.java @@ -0,0 +1,102 @@ +/* + * A simple backup mod for Fabric + * Copyright (C) 2020 Szum123321 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.szum123321.textile_backup.core.restore; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.ServerCommandSource; +import net.szum123321.textile_backup.core.ActionInitiator; + +import javax.annotation.Nullable; + +public record RestoreContext(RestoreHelper.RestoreableFile file, + MinecraftServer server, @Nullable String comment, + ActionInitiator initiator, + ServerCommandSource commandSource) { + public RestoreContext(RestoreHelper.RestoreableFile file, MinecraftServer server, @Nullable String comment, ActionInitiator initiator, ServerCommandSource commandSource) { + this.file = file; + this.server = server; + this.comment = comment; + this.initiator = initiator; + this.commandSource = commandSource; + } + + public RestoreHelper.RestoreableFile getFile() { + return file; + } + + public MinecraftServer getServer() { + return server; + } + + @Nullable + public String getComment() { + return comment; + } + + public ActionInitiator getInitiator() { + return initiator; + } + + public ServerCommandSource getCommandSource() { + return commandSource; + } + + public static final class Builder { + private RestoreHelper.RestoreableFile file; + private MinecraftServer server; + private String comment; + private ServerCommandSource serverCommandSource; + + private Builder() { + } + + public static Builder newRestoreContextBuilder() { + return new Builder(); + } + + public Builder setFile(RestoreHelper.RestoreableFile file) { + this.file = file; + return this; + } + + public Builder setServer(MinecraftServer server) { + this.server = server; + return this; + } + + public Builder setComment(@Nullable String comment) { + this.comment = comment; + return this; + } + + public Builder setCommandSource(ServerCommandSource commandSource) { + this.serverCommandSource = commandSource; + return this; + } + + public RestoreContext build() { + if (server == null) server = serverCommandSource.getMinecraftServer(); + + ActionInitiator initiator = serverCommandSource.getEntity() instanceof PlayerEntity ? ActionInitiator.Player : ActionInitiator.ServerConsole; + + return new RestoreContext(file, server, comment, initiator, serverCommandSource); + } + } +} diff --git a/src/main/java/net/szum123321/textile_backup/core/restore/RestoreHelper.java b/src/main/java/net/szum123321/textile_backup/core/restore/RestoreHelper.java index f76bf4a..44b7c42 100644 --- a/src/main/java/net/szum123321/textile_backup/core/restore/RestoreHelper.java +++ b/src/main/java/net/szum123321/textile_backup/core/restore/RestoreHelper.java @@ -24,43 +24,67 @@ import net.minecraft.text.LiteralText; import net.minecraft.text.MutableText; import net.minecraft.util.Formatting; import net.minecraft.util.Util; +import net.szum123321.textile_backup.ConfigHandler; import net.szum123321.textile_backup.Statics; +import net.szum123321.textile_backup.core.ActionInitiator; import net.szum123321.textile_backup.core.Utilities; +import org.jetbrains.annotations.NotNull; import java.io.File; import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; public class RestoreHelper { - public static Optional findFileAndLockIfPresent(LocalDateTime backupTime, MinecraftServer server) { + public static Optional findFileAndLockIfPresent(LocalDateTime backupTime, MinecraftServer server) { File root = Utilities.getBackupRootPath(Utilities.getLevelName(server)); - Optional optionalFile = Arrays.stream(root.listFiles()) - .filter(Utilities::isValidBackup) - .filter(file -> Utilities.getFileCreationTime(file).get().equals(backupTime)) + Optional optionalFile = Arrays.stream(root.listFiles()) + .map(RestoreableFile::newInstance) + .flatMap(Optional::stream) + .filter(rf -> rf.getCreationTime().equals(backupTime)) .findFirst(); - optionalFile.ifPresent(file -> Statics.untouchableFile = file); + Statics.untouchableFile = optionalFile.map(RestoreableFile::getFile); return optionalFile; } - public static AwaitThread create(File backupFile, MinecraftServer server, String comment) { - MutableText msg = new LiteralText("Warning! The server is going to shut down in " + Statics.CONFIG.restoreDelay + " seconds!"); - msg.formatted(Formatting.WHITE); - msg = Statics.LOGGER.getPrefixText().append(msg); + public static AwaitThread create(RestoreContext ctx) { + if(ctx.getInitiator() == ActionInitiator.Player) + Statics.LOGGER.info("Backup restoration was initiated by: {}", ctx.getCommandSource().getName()); + else + Statics.LOGGER.info("Backup restoration was initiated form Server Console"); - server.getPlayerManager().broadcastChatMessage(msg, MessageType.SYSTEM, Util.NIL_UUID); - - Statics.globalShutdownBackupFlag.set(false); + notifyPlayers(ctx); return new AwaitThread( Statics.CONFIG.restoreDelay, - new RestoreBackupRunnable(server, backupFile, comment) + new RestoreBackupRunnable(ctx) + ); + } + + private static void notifyPlayers(RestoreContext ctx) { + MutableText message = Statics.LOGGER.getPrefixText(); + message.append( + new LiteralText( + "Warning! The server is going to shut down in " + + Statics.CONFIG.restoreDelay + + " seconds!" + ).formatted(Formatting.WHITE) + ); + + UUID uuid; + + if(ctx.getInitiator().equals(ActionInitiator.Player) && ctx.getCommandSource().getEntity() != null) + uuid = ctx.getCommandSource().getEntity().getUuid(); + else + uuid = Util.NIL_UUID; + + ctx.getServer().getPlayerManager().broadcastChatMessage( + message, + MessageType.SYSTEM, + uuid ); } @@ -69,17 +93,22 @@ public class RestoreHelper { return Arrays.stream(root.listFiles()) .filter(Utilities::isValidBackup) - .map(RestoreableFile::new) + .map(RestoreableFile::newInstance) + .flatMap(Optional::stream) .collect(Collectors.toList()); } - public static class RestoreableFile { + public static class RestoreableFile implements Comparable { + private final File file; + private final ConfigHandler.ArchiveFormat archiveFormat; private final LocalDateTime creationTime; private final String comment; - protected RestoreableFile(File file) { - String extension = Utilities.getFileExtension(file).orElseThrow(() -> new NoSuchElementException("Couldn't get file extention")).getString(); - this.creationTime = Utilities.getFileCreationTime(file).orElseThrow(() -> new NoSuchElementException("Couldn't get file creation time.")); + private RestoreableFile(File file) throws NoSuchElementException { + this.file = file; + archiveFormat = Utilities.getArchiveExtension(file).orElseThrow(() -> new NoSuchElementException("Couldn't get file extension!")); + String extension = archiveFormat.getCompleteString(); + creationTime = Utilities.getFileCreationTime(file).orElseThrow(() -> new NoSuchElementException("Couldn't get file creation time!")); final String filename = file.getName(); @@ -90,6 +119,22 @@ public class RestoreHelper { } } + public static Optional newInstance(File file) { + try { + return Optional.of(new RestoreableFile(file)); + } catch (NoSuchElementException ignored) {} + + return Optional.empty(); + } + + public File getFile() { + return file; + } + + public ConfigHandler.ArchiveFormat getArchiveFormat() { + return archiveFormat; + } + public LocalDateTime getCreationTime() { return creationTime; } @@ -98,8 +143,13 @@ public class RestoreHelper { return comment; } + @Override + public int compareTo(@NotNull RestoreHelper.RestoreableFile o) { + return creationTime.compareTo(o.creationTime); + } + public String toString() { return this.getCreationTime().format(Statics.defaultDateTimeFormatter) + (comment != null ? "#" + comment : ""); } } -} +} \ No newline at end of file diff --git a/src/main/java/net/szum123321/textile_backup/core/restore/decompressors/GenericTarDecompressor.java b/src/main/java/net/szum123321/textile_backup/core/restore/decompressors/GenericTarDecompressor.java index f8985d8..a305b5b 100644 --- a/src/main/java/net/szum123321/textile_backup/core/restore/decompressors/GenericTarDecompressor.java +++ b/src/main/java/net/szum123321/textile_backup/core/restore/decompressors/GenericTarDecompressor.java @@ -23,7 +23,6 @@ import net.szum123321.textile_backup.core.Utilities; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.compressors.CompressorException; -import org.apache.commons.compress.compressors.CompressorInputStream; import org.apache.commons.compress.compressors.CompressorStreamFactory; import org.apache.commons.compress.utils.IOUtils; @@ -36,15 +35,15 @@ public class GenericTarDecompressor { public static void decompress(File input, File target) { Instant start = Instant.now(); - try (FileInputStream fileInputStream = new FileInputStream(input); - BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream); - CompressorInputStream compressorInputStream = new CompressorStreamFactory().createCompressorInputStream(bufferedInputStream); + try (InputStream fileInputStream = new FileInputStream(input); + InputStream bufferedInputStream = new BufferedInputStream(fileInputStream); + InputStream compressorInputStream = getCompressorInputStream(bufferedInputStream); TarArchiveInputStream archiveInputStream = new TarArchiveInputStream(compressorInputStream)) { TarArchiveEntry entry; while ((entry = archiveInputStream.getNextTarEntry()) != null) { if(!archiveInputStream.canReadEntryData(entry)) { - Statics.LOGGER.warn("Something when wrong while trying to decompress {}", entry.getName()); + Statics.LOGGER.error("Something when wrong while trying to decompress {}", entry.getName()); continue; } @@ -73,4 +72,26 @@ public class GenericTarDecompressor { Statics.LOGGER.info("Decompression took {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now()))); } + + private static InputStream getCompressorInputStream(InputStream inputStream) throws CompressorException { + try { + return new CompressorStreamFactory().createCompressorInputStream(inputStream); + } catch (CompressorException e) { + final byte[] tarHeader = new byte[512]; + int signatureLength; + + inputStream.mark(tarHeader.length); + + try { + signatureLength = IOUtils.readFully(inputStream, tarHeader); + inputStream.reset(); + } catch (IOException e1) { + throw new CompressorException("IOException while reading tar signature", e1); + } + + if(TarArchiveInputStream.matches(tarHeader, signatureLength)) return inputStream; + + throw e; + } + } } \ No newline at end of file diff --git a/src/main/java/net/szum123321/textile_backup/core/restore/decompressors/ZipDecompressor.java b/src/main/java/net/szum123321/textile_backup/core/restore/decompressors/ZipDecompressor.java index 1ba3f2e..a07e952 100644 --- a/src/main/java/net/szum123321/textile_backup/core/restore/decompressors/ZipDecompressor.java +++ b/src/main/java/net/szum123321/textile_backup/core/restore/decompressors/ZipDecompressor.java @@ -40,7 +40,7 @@ public class ZipDecompressor { while ((entry = zipInputStream.getNextZipEntry()) != null) { if(!zipInputStream.canReadEntryData(entry)){ - Statics.LOGGER.warn("Something when wrong while trying to decompress {}", entry.getName()); + Statics.LOGGER.error("Something when wrong while trying to decompress {}", entry.getName()); continue; } @@ -51,14 +51,15 @@ public class ZipDecompressor { } else { File parent = file.getParentFile(); - if (!parent.isDirectory() && !parent.mkdirs()) - throw new IOException("Failed to create directory " + parent); - - try (OutputStream outputStream = Files.newOutputStream(file.toPath()); - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) { - IOUtils.copy(zipInputStream, bufferedOutputStream); - } catch (IOException e) { - Statics.LOGGER.error("An exception occurred while trying to decompress file: {}", file.getName(), e); + if (!parent.isDirectory() && !parent.mkdirs()) { + Statics.LOGGER.error("Failed to create {}", parent); + } else { + try (OutputStream outputStream = Files.newOutputStream(file.toPath()); + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) { + IOUtils.copy(zipInputStream, bufferedOutputStream); + } catch (IOException e) { + Statics.LOGGER.error("An exception occurred while trying to decompress file: {}", file.getName(), e); + } } } } diff --git a/src/main/java/net/szum123321/textile_backup/mixin/DedicatedServerWatchdogMixin.java b/src/main/java/net/szum123321/textile_backup/mixin/DedicatedServerWatchdogMixin.java new file mode 100644 index 0000000..91393b2 --- /dev/null +++ b/src/main/java/net/szum123321/textile_backup/mixin/DedicatedServerWatchdogMixin.java @@ -0,0 +1,17 @@ +package net.szum123321.textile_backup.mixin; + +import net.minecraft.server.dedicated.DedicatedServerWatchdog; +import net.minecraft.util.Util; +import net.szum123321.textile_backup.Statics; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +@Mixin(DedicatedServerWatchdog.class) +public class DedicatedServerWatchdogMixin { + + @ModifyVariable(method = "run()V", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/util/Util;getMeasuringTimeMs()J"), ordinal = 0, name = "l") + private long redirectedCall(long original) { + return Statics.disableWatchdog ? Util.getMeasuringTimeMs() : original; + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index c2f10e1..d7e5138 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -8,6 +8,12 @@ "authors": [ "Szum123321" ], + "contributors": [ + "1a2s3d4f1", + "pm709", + "Harveykang", + "66Leo66" + ], "contact": { "homepage": "https://www.curseforge.com/minecraft/mc-mods/textile-backup", "issues": "https://github.com/Szum123321/textile_backup/issues", @@ -28,9 +34,10 @@ ], "depends": { - "fabricloader": ">=0.8.8", + "fabricloader": ">=0.11", "fabric": "*", - "minecraft": "1.17.*" + "minecraft": "1.17.*", + "java": ">=16" }, "custom": { diff --git a/src/main/resources/textile_backup.mixins.json b/src/main/resources/textile_backup.mixins.json index c53eeb8..82c2d00 100644 --- a/src/main/resources/textile_backup.mixins.json +++ b/src/main/resources/textile_backup.mixins.json @@ -1,8 +1,9 @@ { "required": true, "package": "net.szum123321.textile_backup.mixin", - "compatibilityLevel": "JAVA_8", + "compatibilityLevel": "JAVA_16", "mixins": [ + "DedicatedServerWatchdogMixin", "MinecraftServerMixin", "MinecraftServerSessionAccessor" ],