commit
d991dc0339
|
@ -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:
|
|
@ -13,10 +13,11 @@ Commands look like that: `/backup <operation> [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 <version> [comment]`
|
||||
* restore - restores backup. Note that the current world will be backuped, and you can add comment to it. `/backup restore <creation date> [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*
|
||||
|
||||
|
|
22
build.gradle
22
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]);
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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<String> fileBlacklist = new HashSet<>();
|
||||
public List<String> 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<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<File> untouchableFile = Optional.empty();
|
||||
|
||||
public static boolean tmpAvailable;
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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("^");
|
||||
});
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<ServerCommandSource> {
|
||||
private static final FileSuggestionProvider INSTANCE = new FileSuggestionProvider();
|
||||
|
||||
public static FileSuggestionProvider Instance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Suggestions> getSuggestions(CommandContext<ServerCommandSource> 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();
|
||||
}
|
||||
}
|
|
@ -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<ServerCommandSource> 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<ServerCommandSource> ctx) {
|
||||
if(!Statics.executorService.isShutdown())
|
||||
private static int execute(ServerCommandSource source, @Nullable String comment) {
|
||||
if(!Statics.executorService.isShutdown()) {
|
||||
try {
|
||||
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()
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<ServerCommandSource> 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<File> 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;
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<ServerCommandSource> 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<RestoreHelper.RestoreableFile> iterator = backups.iterator();
|
||||
builder.append("Available backups: ");
|
||||
builder.append("Available backups:\n");
|
||||
|
||||
builder.append(iterator.next());
|
||||
|
|
@ -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;
|
|
@ -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<ServerCommandSource> 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");
|
||||
}
|
||||
|
|
|
@ -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<ServerCommandSource> 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 {
|
||||
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");
|
||||
|
||||
for(int i = 0; i < e.getErrorIndex(); i++)
|
||||
message.append(" ");
|
||||
|
||||
message.append("^");
|
||||
|
||||
throw new CommandSyntaxException(new SimpleCommandExceptionType(message), message);
|
||||
throw CommandExceptions.DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE.create(e);
|
||||
}
|
||||
|
||||
Optional<File> backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getMinecraftServer());
|
||||
Optional<RestoreHelper.RestoreableFile> backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getMinecraftServer());
|
||||
|
||||
if(backupFile.isPresent()) {
|
||||
Statics.LOGGER.info("Found file to restore {}", backupFile.get().getName());
|
||||
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));
|
||||
|
||||
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<ServerCommandSource> {
|
||||
@Override
|
||||
public CompletableFuture<Suggestions> getSuggestions(CommandContext<ServerCommandSource> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,21 +16,29 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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();
|
||||
|
||||
public static LZMACompressor getInstance() {
|
||||
return INSTANCE;
|
||||
ActionInitiator(String name, String prefix) {
|
||||
this.name = name;
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OutputStream openCompressorStream(OutputStream outputStream, int coreCountLimit) throws IOException {
|
||||
return new XZCompressorOutputStream(outputStream);
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getPrefix() {
|
||||
return prefix + ": ";
|
||||
}
|
||||
}
|
|
@ -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 sendHint(ServerCommandSource source, String msg, Object... args) {
|
||||
sendFeedback(Level.TRACE, source, msg, args);
|
||||
}
|
||||
|
||||
public void sendInfo(ServerCommandSource source, String msg, Object... args) {
|
||||
sendToPlayer(Level.INFO, source, msg, args);
|
||||
}
|
||||
|
||||
public void sendError(ServerCommandSource source, String msg, Object... args) {
|
||||
sendToPlayer(Level.ERROR, source, msg, 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
@ -51,21 +46,41 @@ public class Utilities {
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isWindows() {
|
||||
return System.getProperty("os.name").toLowerCase().contains("win");
|
||||
|
@ -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<ConfigHandler.ArchiveFormat> getFileExtension(String fileName) {
|
||||
public static Optional<ConfigHandler.ArchiveFormat> 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<ConfigHandler.ArchiveFormat> getFileExtension(File f) {
|
||||
return getFileExtension(f.getName());
|
||||
public static Optional<ConfigHandler.ArchiveFormat> getArchiveExtension(File f) {
|
||||
return getArchiveExtension(f.getName());
|
||||
}
|
||||
|
||||
public static Optional<LocalDateTime> 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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
@ -112,9 +115,9 @@ public class BackupContext {
|
|||
|
||||
public BackupContext build() {
|
||||
if (guessInitiator) {
|
||||
initiator = commandSource.getEntity() == null ? BackupInitiator.ServerConsole : BackupInitiator.Player;
|
||||
initiator = commandSource.getEntity() instanceof PlayerEntity ? ActionInitiator.Player : ActionInitiator.ServerConsole;
|
||||
} else if (initiator == null) {
|
||||
initiator = BackupInitiator.Null;
|
||||
initiator = ActionInitiator.Null;
|
||||
}
|
||||
|
||||
if (server == null) {
|
||||
|
@ -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 + ": ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<File> 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<File> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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);
|
||||
if (context.getInitiator() == ActionInitiator.Player)
|
||||
Statics.LOGGER.sendError(context.getCommandSource(), "Error! No correct compression format specified! Using default compressor!");
|
||||
|
||||
ParallelZipCompressor.createArchive(world, outFile, context, coreCount);
|
||||
break;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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())));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
public static ParallelZipCompressor getInstance() {
|
||||
return new ParallelZipCompressor();
|
||||
}
|
||||
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
|
||||
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
|
||||
ZipArchiveOutputStream arc = new ZipArchiveOutputStream(bufferedOutputStream)) {
|
||||
@Override
|
||||
protected OutputStream createArchiveOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) {
|
||||
scatterZipCreator = new ParallelScatterZipCreator(Executors.newFixedThreadPool(coreLimit));
|
||||
return super.createArchiveOutputStream(stream, ctx, coreLimit);
|
||||
}
|
||||
|
||||
ParallelScatterZipCreator scatterZipCreator = new ParallelScatterZipCreator(Executors.newFixedThreadPool(coreLimit));
|
||||
@Override
|
||||
protected void addEntry(File file, String entryName, OutputStream arc) throws IOException {
|
||||
ZipArchiveEntry entry = (ZipArchiveEntry)((ZipArchiveOutputStream)arc).createArchiveEntry(file, entryName);
|
||||
|
||||
arc.setMethod(ZipArchiveOutputStream.DEFLATED);
|
||||
arc.setUseZip64(Zip64Mode.AsNeeded);
|
||||
arc.setLevel(Statics.CONFIG.compression);
|
||||
arc.setComment("Created on: " + Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
|
||||
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);
|
||||
|
||||
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.setTime(System.currentTimeMillis());
|
||||
|
||||
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())));
|
||||
@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;
|
||||
}
|
||||
|
||||
static class FileInputStreamSupplier implements InputStreamSupplier {
|
||||
private final File sourceFile;
|
||||
private InputStream stream;
|
||||
|
||||
FileInputStreamSupplier(File sourceFile) {
|
||||
this.sourceFile = sourceFile;
|
||||
//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);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
stream = new BufferedInputStream(new FileInputStream(sourceFile));
|
||||
return new FileInputStream(sourceFile);
|
||||
} catch (IOException e) {
|
||||
Statics.LOGGER.error("An exception occurred while trying to create input stream!", e);
|
||||
Statics.LOGGER.error("An exception occurred while trying to create an input stream from file: {}!", sourceFile.getName(), e);
|
||||
}
|
||||
|
||||
return stream;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,22 +16,21 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
|
@ -16,22 +16,32 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<File> findFileAndLockIfPresent(LocalDateTime backupTime, MinecraftServer server) {
|
||||
public static Optional<RestoreableFile> findFileAndLockIfPresent(LocalDateTime backupTime, MinecraftServer server) {
|
||||
File root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
|
||||
|
||||
Optional<File> optionalFile = Arrays.stream(root.listFiles())
|
||||
.filter(Utilities::isValidBackup)
|
||||
.filter(file -> Utilities.getFileCreationTime(file).get().equals(backupTime))
|
||||
Optional<RestoreableFile> 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<RestoreableFile> {
|
||||
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<RestoreableFile> 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,6 +143,11 @@ 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 : "");
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,9 +51,9 @@ public class ZipDecompressor {
|
|||
} else {
|
||||
File parent = file.getParentFile();
|
||||
|
||||
if (!parent.isDirectory() && !parent.mkdirs())
|
||||
throw new IOException("Failed to create directory " + parent);
|
||||
|
||||
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);
|
||||
|
@ -62,6 +62,7 @@ public class ZipDecompressor {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Statics.LOGGER.error("An exception occurred! ", e);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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": {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
{
|
||||
"required": true,
|
||||
"package": "net.szum123321.textile_backup.mixin",
|
||||
"compatibilityLevel": "JAVA_8",
|
||||
"compatibilityLevel": "JAVA_16",
|
||||
"mixins": [
|
||||
"DedicatedServerWatchdogMixin",
|
||||
"MinecraftServerMixin",
|
||||
"MinecraftServerSessionAccessor"
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue