Merge pull request #68 from Szum123321/2.1.0

2.1.0
2.x-1.16
Szum123321 2021-06-26 13:25:02 +02:00 committed by GitHub
commit d991dc0339
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1198 additions and 563 deletions

View File

@ -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:

View File

@ -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*

View File

@ -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]);
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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())
));
}

View File

@ -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("^");
});
}

View File

@ -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();
}
}

View File

@ -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())
Statics.executorService.submit(
BackupHelper.create(
new BackupContext.Builder()
.setCommandSource(ctx.getSource())
.setComment(StringArgumentType.getString(ctx, "comment"))
.guessInitiator()
.saveServer()
.build()
)
);
return 1;
}
private static int execute(ServerCommandSource source){
if(!Statics.executorService.isShutdown())
Statics.executorService.submit(
BackupHelper.create(
new BackupContext.Builder()
.setCommandSource(source)
.guessInitiator()
.saveServer()
.build()
)
);
private static int execute(ServerCommandSource source, @Nullable String comment) {
if(!Statics.executorService.isShutdown()) {
try {
Statics.executorService.submit(
BackupHelper.create(
BackupContext.Builder
.newBackupContextBuilder()
.setCommandSource(source)
.setComment(comment)
.guessInitiator()
.saveServer()
.build()
)
);
} catch (Exception e) {
Statics.LOGGER.error("Something went wrong while executing command!", e);
throw e;
}
}
return 1;
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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());

View File

@ -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;

View File

@ -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");
}

View File

@ -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 {
LocalDateTime dateTime;
private static int execute(String file, @Nullable String comment, ServerCommandSource source) throws CommandSyntaxException {
if(Statics.restoreAwaitThread == null || (Statics.restoreAwaitThread != null && !Statics.restoreAwaitThread.isAlive())) {
LocalDateTime dateTime;
try {
dateTime = LocalDateTime.from(Statics.defaultDateTimeFormatter.parse(file));
} catch (DateTimeParseException e) {
LiteralText message = new LiteralText("An exception occurred while trying to parse:\n");
message.append(e.getParsedString())
.append("\n");
try {
dateTime = LocalDateTime.from(Statics.defaultDateTimeFormatter.parse(file));
} catch (DateTimeParseException e) {
throw CommandExceptions.DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE.create(e);
}
for(int i = 0; i < e.getErrorIndex(); i++)
message.append(" ");
Optional<RestoreHelper.RestoreableFile> backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getMinecraftServer());
message.append("^");
if(backupFile.isPresent()) {
Statics.LOGGER.info("Found file to restore {}", backupFile.get().getFile().getName());
} else {
Statics.LOGGER.sendInfo(source, "No file created on {} was found!", dateTime.format(Statics.defaultDateTimeFormatter));
throw new CommandSyntaxException(new SimpleCommandExceptionType(message), message);
}
return 0;
}
Optional<File> backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getMinecraftServer());
if(backupFile.isPresent()) {
Statics.LOGGER.info("Found file to restore {}", backupFile.get().getName());
} else {
Statics.LOGGER.sendInfo(source, "No file created on {} was found!", dateTime.format(Statics.defaultDateTimeFormatter));
return 0;
}
if(Statics.restoreAwaitThread == null || !Statics.restoreAwaitThread.isAlive()) {
if(source.getEntity() != null)
Statics.LOGGER.info("Backup restoration was initiated by: {}", source.getName());
else
Statics.LOGGER.info("Backup restoration was initiated form Server Console");
Statics.restoreAwaitThread = RestoreHelper.create(backupFile.get(), source.getMinecraftServer(), comment);
Statics.restoreAwaitThread = RestoreHelper.create(
RestoreContext.Builder.newRestoreContextBuilder()
.setCommandSource(source)
.setFile(backupFile.get())
.setComment(comment)
.build()
);
Statics.restoreAwaitThread.start();
} else if(Statics.restoreAwaitThread != null && Statics.restoreAwaitThread.isAlive()) {
return 1;
} else {
Statics.LOGGER.sendInfo(source, "Someone has already started another restoration.");
return 0;
}
return 1;
}
private static final class FileSuggestionProvider implements SuggestionProvider<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();
}
}
}

View File

@ -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();
ActionInitiator(String name, String prefix) {
this.name = name;
this.prefix = prefix;
}
public static LZMACompressor getInstance() {
return INSTANCE;
}
public String getName() {
return name;
}
@Override
protected OutputStream openCompressorStream(OutputStream outputStream, int coreCountLimit) throws IOException {
return new XZCompressorOutputStream(outputStream);
}
public String getPrefix() {
return prefix + ": ";
}
}

View File

@ -18,11 +18,10 @@
package net.szum123321.textile_backup.core;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.LiteralText;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.szum123321.textile_backup.core.create.BackupContext;
import org.apache.logging.log4j.Level;
@ -30,19 +29,18 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.MessageFactory;
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
import org.apache.logging.log4j.spi.StandardLevel;
/*
This is practically just a copy-pate of Cotton's ModLogger with few changes
This is practically just a copy-pate of Cotton's ModLogger with a few changes
*/
public class CustomLogger {
private final boolean isDev = FabricLoader.getInstance().isDevelopmentEnvironment();
//private final boolean isDev = FabricLoader.getInstance().isDevelopmentEnvironment();
private final MessageFactory messageFactory;
private final Logger logger;
private final String prefix;
private final Text prefixText;
private final MutableText prefixText;
public CustomLogger(String name, String prefix) {
this.messageFactory = ParameterizedMessageFactory.INSTANCE;
@ -83,54 +81,66 @@ public class CustomLogger {
log(Level.FATAL, msg, data);
}
public void devError(String msg, Object... data) {
if (isDev) error(msg, data);
}
public void devWarn(String msg, Object... data) {
if (isDev) warn(msg, data);
}
public void devInfo(String msg, Object... data) {
if (isDev) info(msg, data);
}
public void devDebug(String msg, Object... data) {
if (isDev) debug(msg, data);
}
public void devTrace(String msg, Object... data) {
if(isDev) trace(msg, data);
}
private void sendToPlayer(Level level, ServerCommandSource source, String msg, Object... args) {
if(source != null && source.getEntity() != null) {
boolean sendFeedback(Level level, ServerCommandSource source, String msg, Object... args) {
if(source != null && source.getEntity() instanceof PlayerEntity) {
LiteralText text = new LiteralText(messageFactory.newMessage(msg, args).getFormattedMessage());
if(level.intLevel() <= StandardLevel.WARN.intLevel())
if(level.intLevel() == Level.TRACE.intLevel())
text.formatted(Formatting.GREEN);
else if(level.intLevel() <= Level.WARN.intLevel())
text.formatted(Formatting.RED);
else
text.formatted(Formatting.WHITE);
source.sendFeedback(prefixText.shallowCopy().append(text), false);
return true;
} else {
logger.log(level, msg, args);
log(level, msg, args);
return false;
}
}
public void sendInfo(ServerCommandSource source, String msg, Object... args) {
sendToPlayer(Level.INFO, source, msg, args);
public void sendHint(ServerCommandSource source, String msg, Object... args) {
sendFeedback(Level.TRACE, source, msg, args);
}
public void sendError(ServerCommandSource source, String msg, Object... args) {
sendToPlayer(Level.ERROR, source, msg, args);
public void sendInfo(ServerCommandSource source, String msg, Object... args) {
sendFeedback(Level.INFO, source, msg, args);
}
public void sendInfo(BackupContext context, String msg, Object... args) {
sendInfo(context.getCommandSource(), msg, args);
}
public void sendError(ServerCommandSource source, String msg, Object... args) {
sendFeedback(Level.ERROR, source, msg, args);
}
public void sendError(BackupContext context, String msg, Object... args) {
sendError(context.getCommandSource(), msg, args);
}
public void sendToPlayerAndLog(Level level, ServerCommandSource source, String msg, Object... args) {
if(sendFeedback(level, source, msg, args))
log(level, msg, args);
}
//send info and log
public void sendInfoAL(ServerCommandSource source, String msg, Object... args) {
sendToPlayerAndLog(Level.INFO, source, msg, args);
}
public void sendInfoAL(BackupContext context, String msg, Object... args) {
sendInfoAL(context.getCommandSource(), msg, args);
}
public void sendErrorAL(ServerCommandSource source, String msg, Object... args) {
sendToPlayerAndLog(Level.ERROR, source, msg, args);
}
public void sendErrorAL(BackupContext context, String msg, Object... args) {
sendErrorAL(context.getCommandSource(), msg, args);
}
}

View File

@ -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);
}
}

View File

@ -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,19 +46,39 @@ 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;
}
}
}
@ -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);
}

View File

@ -18,18 +18,17 @@
package net.szum123321.textile_backup.core.create;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.core.ActionInitiator;
import org.jetbrains.annotations.NotNull;
public class BackupContext {
private final MinecraftServer server;
private final ServerCommandSource commandSource;
private final BackupInitiator initiator;
private final boolean save;
private final String comment;
protected BackupContext(@NotNull MinecraftServer server, ServerCommandSource commandSource, @NotNull BackupInitiator initiator, boolean save, String comment) {
public record BackupContext(MinecraftServer server,
ServerCommandSource commandSource,
ActionInitiator initiator, boolean save,
String comment) {
public BackupContext(@NotNull MinecraftServer server, ServerCommandSource commandSource, @NotNull ActionInitiator initiator, boolean save, String comment) {
this.server = server;
this.commandSource = commandSource;
this.initiator = initiator;
@ -45,12 +44,12 @@ public class BackupContext {
return commandSource;
}
public BackupInitiator getInitiator() {
public ActionInitiator getInitiator() {
return initiator;
}
public boolean startedByPlayer() {
return initiator == BackupInitiator.Player;
return initiator == ActionInitiator.Player;
}
public boolean shouldSave() {
@ -64,7 +63,7 @@ public class BackupContext {
public static class Builder {
private MinecraftServer server;
private ServerCommandSource commandSource;
private BackupInitiator initiator;
private ActionInitiator initiator;
private boolean save;
private String comment;
@ -80,6 +79,10 @@ public class BackupContext {
guessInitiator = false;
}
public static Builder newBackupContextBuilder() {
return new Builder();
}
public Builder setCommandSource(ServerCommandSource commandSource) {
this.commandSource = commandSource;
return this;
@ -90,7 +93,7 @@ public class BackupContext {
return this;
}
public Builder setInitiator(BackupInitiator initiator) {
public Builder setInitiator(ActionInitiator initiator) {
this.initiator = initiator;
return this;
}
@ -111,14 +114,14 @@ public class BackupContext {
}
public BackupContext build() {
if(guessInitiator) {
initiator = commandSource.getEntity() == null ? BackupInitiator.ServerConsole : BackupInitiator.Player;
} else if(initiator == null) {
initiator = BackupInitiator.Null;
if (guessInitiator) {
initiator = commandSource.getEntity() instanceof PlayerEntity ? ActionInitiator.Player : ActionInitiator.ServerConsole;
} else if (initiator == null) {
initiator = ActionInitiator.Null;
}
if(server == null) {
if(commandSource != null)
if (server == null) {
if (commandSource != null)
setServer(commandSource.getMinecraftServer());
else
throw new RuntimeException("Both MinecraftServer and ServerCommandSource weren't provided!");
@ -128,28 +131,4 @@ public class BackupContext {
}
}
public enum BackupInitiator {
Player ("Player", "by"),
ServerConsole ("Server Console", "from"),
Timer ("Timer", "by"),
Shutdown ("Server Shutdown", "by"),
Restore ("Backup Restoration", "because of"),
Null ("Null (That shouldn't have happened)", "form");
private final String name;
private final String prefix;
BackupInitiator(String name, String prefix) {
this.name = name;
this.prefix = prefix;
}
public String getName() {
return name;
}
public String getPrefix() {
return prefix + ": ";
}
}
}

View File

@ -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());
}
}

View File

@ -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()
)

View File

@ -19,11 +19,17 @@
package net.szum123321.textile_backup.core.create;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.create.compressors.*;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.create.compressors.tar.AbstractTarArchiver;
import net.szum123321.textile_backup.core.create.compressors.tar.ParallelBZip2Compressor;
import net.szum123321.textile_backup.core.create.compressors.tar.ParallelGzipCompressor;
import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.time.LocalDateTime;
public class MakeBackupRunnable implements Runnable {
@ -36,8 +42,10 @@ public class MakeBackupRunnable implements Runnable {
@Override
public void run() {
try {
Statics.LOGGER.sendInfo(context.getCommandSource(), "Starting backup");
Statics.LOGGER.info("Starting backup");
Utilities.disableWorldSaving(context.getServer());
Statics.disableWatchdog = true;
Statics.LOGGER.sendInfoAL(context, "Starting backup");
File world = Utilities.getWorldFolder(context.getServer());
@ -57,7 +65,9 @@ public class MakeBackupRunnable implements Runnable {
outFile.createNewFile();
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred when trying to create new backup file!", e);
Statics.LOGGER.sendError(context.getCommandSource(), "An exception occurred when trying to create new backup file!");
if(context.getInitiator() == ActionInitiator.Player)
Statics.LOGGER.sendError(context, "An exception occurred when trying to create new backup file!");
return;
}
@ -70,39 +80,37 @@ public class MakeBackupRunnable implements Runnable {
coreCount = Math.min(Statics.CONFIG.compressionCoreCountLimit, Runtime.getRuntime().availableProcessors());
}
Statics.LOGGER.trace("Running compression on {} threads. Available cores = {}", coreCount, Runtime.getRuntime().availableProcessors());
Statics.LOGGER.trace("Running compression on {} threads. Available cores: {}", coreCount, Runtime.getRuntime().availableProcessors());
switch (Statics.CONFIG.format) {
case ZIP:
ParallelZipCompressor.createArchive(world, outFile, context, coreCount);
break;
case BZIP2:
ParallelBZip2Compressor.getInstance().createArchive(world, outFile, context, coreCount);
break;
case GZIP:
ParallelGzipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
break;
case LZMA:
LZMACompressor.getInstance().createArchive(world, outFile, context, coreCount);
break;
default:
case ZIP -> {
if (Statics.tmpAvailable && coreCount > 1)
ParallelZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
else
ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
}
case BZIP2 -> ParallelBZip2Compressor.getInstance().createArchive(world, outFile, context, coreCount);
case GZIP -> ParallelGzipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
case LZMA -> new AbstractTarArchiver() {
protected OutputStream getCompressorOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException {
return new LZMACompressorOutputStream(stream);
}
}.createArchive(world, outFile, context, coreCount);
case TAR -> new AbstractTarArchiver().createArchive(world, outFile, context, coreCount);
default -> {
Statics.LOGGER.warn("Specified compressor ({}) is not supported! Zip will be used instead!", Statics.CONFIG.format);
Statics.LOGGER.sendError(context.getCommandSource(), "Error! No correct compression format specified! Using default compressor!");
ParallelZipCompressor.createArchive(world, outFile, context, coreCount);
break;
if (context.getInitiator() == ActionInitiator.Player)
Statics.LOGGER.sendError(context.getCommandSource(), "Error! No correct compression format specified! Using default compressor!");
ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
}
}
BackupHelper.executeFileLimit(context.getCommandSource(), Utilities.getLevelName(context.getServer()));
Statics.LOGGER.sendInfo(context, "Done!");
Statics.LOGGER.info("Done!");
Statics.LOGGER.sendInfoAL(context, "Done!");
} finally {
Utilities.enableWorldSaving(context.getServer());
Statics.disableWatchdog = false;
}
}
@ -111,6 +119,6 @@ public class MakeBackupRunnable implements Runnable {
return Utilities.getDateTimeFormatter().format(now) +
(context.getComment() != null ? "#" + context.getComment().replace("#", "") : "") +
Statics.CONFIG.format.getString();
Statics.CONFIG.format.getCompleteString();
}
}

View File

@ -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
}
}

View File

@ -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())));
}
}

View File

@ -19,17 +19,13 @@
package net.szum123321.textile_backup.core.create.compressors;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.NoSpaceLeftOnDeviceException;
import net.szum123321.textile_backup.core.create.BackupContext;
import org.apache.commons.compress.archivers.zip.*;
import org.apache.commons.compress.parallel.InputStreamSupplier;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.zip.ZipEntry;
@ -39,66 +35,105 @@ import java.util.zip.ZipEntry;
answer by:
https://stackoverflow.com/users/2987755/dkb
*/
public class ParallelZipCompressor {
public static void createArchive(File inputFile, File outputFile, BackupContext ctx, int coreLimit) {
Statics.LOGGER.sendInfo(ctx, "Starting compression...");
public class ParallelZipCompressor extends ZipCompressor {
//These fields are used to discriminate against the issue #51
private final static SimpleStackTraceElement[] STACKTRACE = {
new SimpleStackTraceElement("sun.nio.ch.FileDispatcherImpl", "write0", true),
new SimpleStackTraceElement("sun.nio.ch.FileDispatcherImpl", "write", false),
new SimpleStackTraceElement("sun.nio.ch.IOUtil", "writeFromNativeBuffer", false),
new SimpleStackTraceElement("sun.nio.ch.IOUtil", "write", false),
new SimpleStackTraceElement("sun.nio.ch.FileChannelImpl", "write", false),
new SimpleStackTraceElement("java.nio.channels.Channels", "writeFullyImpl", false),
new SimpleStackTraceElement("java.nio.channels.Channels", "writeFully", false),
new SimpleStackTraceElement("java.nio.channels.Channels$1", "write", false),
new SimpleStackTraceElement("org.apache.commons.compress.parallel.FileBasedScatterGatherBackingStore", "writeOut", false)
};
Instant start = Instant.now();
private ParallelScatterZipCreator scatterZipCreator;
Path rootPath = inputFile.toPath();
try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
ZipArchiveOutputStream arc = new ZipArchiveOutputStream(bufferedOutputStream)) {
ParallelScatterZipCreator scatterZipCreator = new ParallelScatterZipCreator(Executors.newFixedThreadPool(coreLimit));
arc.setMethod(ZipArchiveOutputStream.DEFLATED);
arc.setUseZip64(Zip64Mode.AsNeeded);
arc.setLevel(Statics.CONFIG.compression);
arc.setComment("Created on: " + Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
Files.walk(inputFile.toPath())
.filter(path -> !Utilities.isBlacklisted(inputFile.toPath().relativize(path)))
.map(Path::toFile)
.filter(File::isFile)
.forEach(file -> {
try { //IOException gets thrown only when arc is closed
ZipArchiveEntry entry = (ZipArchiveEntry)arc.createArchiveEntry(file, rootPath.relativize(file.toPath()).toString());
entry.setMethod(ZipEntry.DEFLATED);
scatterZipCreator.addArchiveEntry(entry, new FileInputStreamSupplier(file));
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred while trying to compress: {}", file.getName(), e);
Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!");
}
});
scatterZipCreator.writeTo(arc);
} catch (IOException | InterruptedException | ExecutionException e) {
Statics.LOGGER.error("An exception occurred!", e);
Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!");
}
Statics.LOGGER.sendInfo(ctx, "Compression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
public static ParallelZipCompressor getInstance() {
return new ParallelZipCompressor();
}
static class FileInputStreamSupplier implements InputStreamSupplier {
private final File sourceFile;
private InputStream stream;
@Override
protected OutputStream createArchiveOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) {
scatterZipCreator = new ParallelScatterZipCreator(Executors.newFixedThreadPool(coreLimit));
return super.createArchiveOutputStream(stream, ctx, coreLimit);
}
FileInputStreamSupplier(File sourceFile) {
this.sourceFile = sourceFile;
}
@Override
protected void addEntry(File file, String entryName, OutputStream arc) throws IOException {
ZipArchiveEntry entry = (ZipArchiveEntry)((ZipArchiveOutputStream)arc).createArchiveEntry(file, entryName);
public InputStream get() {
try {
stream = new BufferedInputStream(new FileInputStream(sourceFile));
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred while trying to create input stream!", e);
if(ZipCompressor.isDotDat(file.getName())) {
entry.setMethod(ZipArchiveOutputStream.STORED);
entry.setSize(file.length());
entry.setCompressedSize(file.length());
entry.setCrc(getCRC(file));
} else entry.setMethod(ZipEntry.DEFLATED);
entry.setTime(System.currentTimeMillis());
scatterZipCreator.addArchiveEntry(entry, new FileInputStreamSupplier(file));
}
@Override
protected void finish(OutputStream arc) throws InterruptedException, IOException, ExecutionException {
/*
This is perhaps the most dreadful line of this whole mess
This line causes the infamous Out of space error
*/
try {
scatterZipCreator.writeTo((ZipArchiveOutputStream) arc);
} catch (ExecutionException e) {
Throwable cause;
if((cause = e.getCause()).getClass().equals(IOException.class)) {
//The out of space exception is thrown at sun.nio.ch.FileDispatcherImpl.write0(Native Method)
boolean match = (cause.getStackTrace().length >= STACKTRACE.length);
if(match) {
for(int i = 0; i < STACKTRACE.length && match; i++)
if(!STACKTRACE[i].equals(cause.getStackTrace()[i])) {
//Statics.LOGGER.error("Mismatch at: {}, classname: {}, methodname: {}, {}", i, cause.getStackTrace()[i].getClassName(), cause.getStackTrace()[i].getMethodName());
match = false;
}
//For clarity sake let's not throw the ExecutionException itself rather only the cause, as the EE is just the wrapper
if(match) throw new NoSpaceLeftOnDeviceException(cause);
}
}
return stream;
throw e;
}
}
private static record SimpleStackTraceElement (
String className,
String methodName,
boolean isNative
) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if(o.getClass() == StackTraceElement.class) {
StackTraceElement that = (StackTraceElement) o;
return (isNative == that.isNativeMethod()) && Objects.equals(className, that.getClassName()) && Objects.equals(methodName, that.getMethodName());
}
if(getClass() != o.getClass()) return false;
SimpleStackTraceElement that = (SimpleStackTraceElement) o;
return isNative == that.isNative && Objects.equals(className, that.className) && Objects.equals(methodName, that.methodName);
}
}
record FileInputStreamSupplier(File sourceFile) implements InputStreamSupplier {
public InputStream get() {
try {
return new FileInputStream(sourceFile);
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred while trying to create an input stream from file: {}!", sourceFile.getName(), e);
}
return null;
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -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));
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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) {

View File

@ -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);
}
}
}

View File

@ -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 : "");
}

View File

@ -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;
}
}
}

View File

@ -40,7 +40,7 @@ public class ZipDecompressor {
while ((entry = zipInputStream.getNextZipEntry()) != null) {
if(!zipInputStream.canReadEntryData(entry)){
Statics.LOGGER.warn("Something when wrong while trying to decompress {}", entry.getName());
Statics.LOGGER.error("Something when wrong while trying to decompress {}", entry.getName());
continue;
}
@ -51,14 +51,15 @@ public class ZipDecompressor {
} else {
File parent = file.getParentFile();
if (!parent.isDirectory() && !parent.mkdirs())
throw new IOException("Failed to create directory " + parent);
try (OutputStream outputStream = Files.newOutputStream(file.toPath());
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
IOUtils.copy(zipInputStream, bufferedOutputStream);
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred while trying to decompress file: {}", file.getName(), e);
if (!parent.isDirectory() && !parent.mkdirs()) {
Statics.LOGGER.error("Failed to create {}", parent);
} else {
try (OutputStream outputStream = Files.newOutputStream(file.toPath());
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
IOUtils.copy(zipInputStream, bufferedOutputStream);
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred while trying to decompress file: {}", file.getName(), e);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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": {

View File

@ -1,8 +1,9 @@
{
"required": true,
"package": "net.szum123321.textile_backup.mixin",
"compatibilityLevel": "JAVA_8",
"compatibilityLevel": "JAVA_16",
"mixins": [
"DedicatedServerWatchdogMixin",
"MinecraftServerMixin",
"MinecraftServerSessionAccessor"
],