Merge pull request #70 from Szum123321/auto_config

Auto config
This commit is contained in:
Szum123321 2021-08-03 23:44:37 +02:00 committed by GitHub
commit d24aa5c9e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 746 additions and 418 deletions

View File

@ -1,12 +1,14 @@
This project uses third party libraries as its dependencies and includes them in jar. Those are :
Apache Commons Compress licensed under Apache License Version 2.0 which can be found at http://www.apache.org/licenses/
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
Cloth config by Shedaniel(https://github.com/shedaniel/cloth-config) under GPL 3
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/
parallelgzip by shevek (https://github.com/shevek/parallelgzip) 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:
Parallel zip compression: https://stackoverflow.com/questions/54624695/how-to-implement-parallel-zip-creation-with-scatterzipoutputstream-with-zip64-su
answer by: https://stackoverflow.com/users/2987755/dkb
answer by: https://stackoverflow.com/users/2987755/dkb
Cotton logging by Cotton team licensed under MIT license which can be found at https://github.com/CottonMC/Cotton

View File

@ -1,5 +1,5 @@
plugins {
id 'fabric-loom' version '0.8-SNAPSHOT'
id 'fabric-loom' version '0.9-SNAPSHOT'
id 'maven-publish'
}
@ -16,6 +16,13 @@ minecraft {
repositories{
maven { url 'https://server.bbkr.space/artifactory/libs-release' }
maven { url 'https://jitpack.io' }
maven { url "https://maven.shedaniel.me/" }
maven {
url "https://maven.terraformersmc.com/releases/"
content {
includeGroup "com.terraformersmc"
}
}
}
dependencies {
@ -27,20 +34,30 @@ dependencies {
// Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
modImplementation "io.github.cottonmc.cotton:cotton-config:1.0.0-rc.7"
//General config library
modApi("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_version}") {
exclude(group: "net.fabricmc.fabric-api")
}
include "io.github.cottonmc:Jankson-Fabric:3.0.0+j1.2.0"
include "io.github.cottonmc.cotton:cotton-logging:1.0.0-rc.4"
include "io.github.cottonmc.cotton:cotton-config:1.0.0-rc.7"
//Mod menu
modImplementation("com.terraformersmc:modmenu:${project.modmenu_version}")
//General compression library
modImplementation "org.apache.commons:commons-compress:1.19"
include "org.apache.commons:commons-compress:1.19"
//LZMA support
modImplementation "org.tukaani:xz:1.8"
include "org.tukaani:xz:1.8"
modImplementation 'com.github.shevek:parallelgzip:master-SNAPSHOT'
include 'com.github.shevek:parallelgzip:master-SNAPSHOT'
//Gzip compression, parallel, GITHUB
modImplementation "com.github.shevek:parallelgzip:${project.pgzip_commit_hash}"
include "com.github.shevek:parallelgzip:${project.pgzip_commit_hash}"
// Lazy DFU makes the dev env start up much faster by loading DataFixerUpper lazily, which would otherwise take a long time. We rarely need it anyway.
modRuntime("com.github.astei:lazydfu:${project.lazydfu_version}") {
exclude(module: "fabric-loader")
}
}
processResources {

View File

@ -1,14 +1,26 @@
# Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G
minecraft_version=1.17
yarn_mappings=1.17+build.10
loader_version=0.11.5
minecraft_version=1.17.1
yarn_mappings=1.17.1+build.32
loader_version=0.11.6
#Fabric api
fabric_version=0.35.1+1.17
fabric_version=0.37.0+1.17
#Cloth Config
cloth_version=5.0.37
#ModMenu
modmenu_version=2.0.4
#Lazy DFU for faster dev start
lazydfu_version=0.1.2
#Hash of commit form which parallel gzip will be build
pgzip_commit_hash=af5f5c297e735f3f2df7aa4eb0e19a5810b8aff6
# Mod Properties
mod_version = 2.1.0
mod_version = 2.2.0
maven_group = net.szum123321
archives_base_name = textile_backup

View File

@ -1,154 +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;
import blue.endless.jankson.Comment;
import io.github.cottonmc.cotton.config.annotations.ConfigFile;
import java.io.File;
import java.time.format.DateTimeFormatter;
import java.util.*;
@ConfigFile(name = Statics.MOD_ID)
public class ConfigHandler {
@Comment("\nTime between automatic backups in seconds\n" +
"When set to 0 backups will not be performed automatically\n")
public long backupInterval = 3600;
@Comment("\nDelay in seconds between typing-in /backup restore and it actually starting\n")
public int restoreDelay = 30;
@Comment("\nShould backups be done even if there are no players?\n")
public boolean doBackupsOnEmptyServer = false;
@Comment("\nShould backup be made on server shutdown?\n")
public boolean shutdownBackup = true;
@Comment("\nShould world be backed up before restoring a backup?\n")
public boolean backupOldWorlds = true;
@Comment("\nShould every world have its own backup folder?\n")
public boolean perWorldBackup = true;
@Comment("\nA path to the backup folder\n")
public String path = "backup/";
@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 List<String> fileBlacklist = new ArrayList<>();
@Comment("\nShould backups be deleted after being restored?\n")
public boolean deleteOldBackupAfterRestore = true;
@Comment("\nMaximum number of backups to keep. If set to 0 then no backup will be deleted based their amount\n")
public int backupsToKeep = 10;
@Comment("\nMaximum age of backups to keep in seconds.\n If set to 0 then backups will not be deleted based their age \n")
public long maxAge = 0;
@Comment("\nMaximum size of backup folder in kilo bytes (1024).\n" +
"If set to 0 then backups will not be deleted\n")
public int maxSize = 0;
@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" +
"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" +
"TAR - .tar with no compression\n")
public ArchiveFormat format = ArchiveFormat.ZIP;
@Comment("\nMinimal permission level required to run commands\n")
public int permissionLevel = 4;
@Comment("\nPlayer on singleplayer is always allowed to run command. Warning! On lan party everyone will be allowed to run it.\n")
public boolean alwaysSingleplayerAllowed = true;
@Comment("\nPlayers allowed to run backup commands without sufficient permission level\n")
public Set<String> playerWhitelist = new HashSet<>();
@Comment("\nPlayers banned from running backup commands besides their sufficient permission level\n")
public Set<String> playerBlacklist = new HashSet<>();
@Comment("\nFormat of date&time used to name backup files.\n" +
"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 = "yyyy.MM.dd_HH-mm-ss";
public Optional<String> sanitize() {
if(compressionCoreCountLimit > Runtime.getRuntime().availableProcessors())
return Optional.of("compressionCoreCountLimit is too big! Your system only has: " + Runtime.getRuntime().availableProcessors() + " cores!");
try {
DateTimeFormatter.ofPattern(dateTimeFormat);
} catch (IllegalArgumentException e) {
return Optional.of("dateTimeFormat is wrong!\n" + e.getMessage() + "\n See: https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html");
}
File path = new File(Statics.CONFIG.path).getAbsoluteFile();
if (!path.exists()) {
try {
path.mkdirs();
} catch (Exception e) {
return Optional.of("Something went wrong while creating backup folder!\n" + e.getMessage());
}
}
return Optional.empty();
}
public enum ArchiveFormat {
ZIP("zip"),
GZIP("tar", "gz"),
BZIP2("tar", "bz2"),
LZMA("tar", "xz"),
TAR("tar");
private final List<String> extensionPieces;
ArchiveFormat(String... extensionParts) {
extensionPieces = Arrays.asList(extensionParts);
}
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

@ -18,8 +18,6 @@
package net.szum123321.textile_backup;
import net.szum123321.textile_backup.core.CustomLogger;
import net.szum123321.textile_backup.core.create.BackupScheduler;
import net.szum123321.textile_backup.core.restore.AwaitThread;
import java.io.File;
@ -30,20 +28,12 @@ import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
public class Statics {
public static final String MOD_ID = "textile_backup";
public static final String MOD_NAME = "Textile Backup";
public static final CustomLogger LOGGER = new CustomLogger(MOD_ID, MOD_NAME);
public static ConfigHandler CONFIG;
public static final BackupScheduler scheduler = new BackupScheduler();
public static ExecutorService executorService = Executors.newSingleThreadExecutor();
public final static DateTimeFormatter defaultDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss");
public static ExecutorService executorService = Executors.newSingleThreadExecutor();
public static final AtomicBoolean globalShutdownBackupFlag = new AtomicBoolean(true);
public static boolean disableWatchdog = false;
public static AwaitThread restoreAwaitThread = null;
public static Optional<File> untouchableFile = Optional.empty();
public static boolean tmpAvailable;
}

View File

@ -19,8 +19,9 @@
package net.szum123321.textile_backup;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import io.github.cottonmc.cotton.config.ConfigManager;
import me.shedaniel.autoconfig.AutoConfig;
import me.shedaniel.autoconfig.serializer.JanksonConfigSerializer;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
@ -34,40 +35,29 @@ import net.szum123321.textile_backup.commands.manage.WhitelistCommand;
import net.szum123321.textile_backup.commands.restore.KillRestoreCommand;
import net.szum123321.textile_backup.commands.manage.ListBackupsCommand;
import net.szum123321.textile_backup.commands.restore.RestoreBackupCommand;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.config.ConfigPOJO;
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;
import net.szum123321.textile_backup.core.create.BackupScheduler;
import java.util.Optional;
import java.util.concurrent.Executors;
public class TextileBackup implements ModInitializer {
public static final String MOD_NAME = "Textile Backup";
public static final String MOD_ID = "textile_backup";
private final static TextileLogger log = new TextileLogger(MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
@Override
public void onInitialize() {
Statics.LOGGER.info("Starting Textile Backup by Szum123321.");
log.info("Starting Textile Backup by Szum123321");
Statics.CONFIG = ConfigManager.loadConfig(ConfigHandler.class);
Optional<String> errorMessage = Statics.CONFIG.sanitize();
ConfigHelper.updateInstance(AutoConfig.register(ConfigPOJO.class, JanksonConfigSerializer::new));
if(errorMessage.isPresent()) {
Statics.LOGGER.fatal("TextileBackup config file has wrong settings!\n{}", errorMessage.get());
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);
ServerTickEvents.END_SERVER_TICK.register(new BackupScheduler()::tick);
//Restart Executor Service in singleplayer
ServerLifecycleEvents.SERVER_STARTING.register(ignored -> {
@ -77,7 +67,7 @@ public class TextileBackup implements ModInitializer {
ServerLifecycleEvents.SERVER_STOPPED.register(server -> {
Statics.executorService.shutdown();
if (Statics.CONFIG.shutdownBackup && Statics.globalShutdownBackupFlag.get()) {
if (config.get().shutdownBackup && Statics.globalShutdownBackupFlag.get()) {
BackupHelper.create(
BackupContext.Builder
.newBackupContextBuilder()
@ -93,11 +83,11 @@ public class TextileBackup implements ModInitializer {
LiteralArgumentBuilder.<ServerCommandSource>literal("backup")
.requires((ctx) -> {
try {
return ((Statics.CONFIG.playerWhitelist.contains(ctx.getEntityOrThrow().getEntityName()) ||
ctx.hasPermissionLevel(Statics.CONFIG.permissionLevel)) &&
!Statics.CONFIG.playerBlacklist.contains(ctx.getEntityOrThrow().getEntityName())) ||
(ctx.getMinecraftServer().isSinglePlayer() &&
Statics.CONFIG.alwaysSingleplayerAllowed);
return ((config.get().playerWhitelist.contains(ctx.getEntityOrThrow().getEntityName()) ||
ctx.hasPermissionLevel(config.get().permissionLevel)) &&
!config.get().playerBlacklist.contains(ctx.getEntityOrThrow().getEntityName())) ||
(ctx.getServer().isSinglePlayer() &&
config.get().alwaysSingleplayerAllowed);
} catch (Exception ignored) { //Command was called from server console.
return true;
}

View File

@ -1,6 +1,6 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2020 Szum123321
* Copyright (C) 2021 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
@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core;
package net.szum123321.textile_backup;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.command.ServerCommandSource;
@ -29,11 +29,12 @@ 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.util.StackLocatorUtil;
/*
This is practically just a copy-pate of Cotton's ModLogger with a few changes
*/
public class CustomLogger {
public class TextileLogger {
//private final boolean isDev = FabricLoader.getInstance().isDevelopmentEnvironment();
private final MessageFactory messageFactory;
@ -42,12 +43,19 @@ public class CustomLogger {
private final String prefix;
private final MutableText prefixText;
public CustomLogger(String name, String prefix) {
/* public TextileLogger(String name, String prefix) {
this.messageFactory = ParameterizedMessageFactory.INSTANCE;
this.logger = LogManager.getLogger(name, messageFactory);
this.prefix = "[" + prefix + "]" + " ";
this.prefixText = new LiteralText(this.prefix).styled(style -> style.withColor(0x5B23DA));
}
*/
public TextileLogger(String prefix) {
this.messageFactory = ParameterizedMessageFactory.INSTANCE;
this.logger = LogManager.getLogger(StackLocatorUtil.getCallerClass(2), messageFactory);
this.prefix = "[" + prefix + "]" + " ";
this.prefixText = new LiteralText(this.prefix).styled(style -> style.withColor(0x5B23DA));
}
public MutableText getPrefixText() {
return prefixText.shallowCopy();

View File

@ -0,0 +1,13 @@
package net.szum123321.textile_backup.client;
import com.terraformersmc.modmenu.api.ConfigScreenFactory;
import com.terraformersmc.modmenu.api.ModMenuApi;
import me.shedaniel.autoconfig.AutoConfig;
import net.szum123321.textile_backup.config.ConfigPOJO;
public class ModMenuEntry implements ModMenuApi {
@Override
public ConfigScreenFactory<?> getModConfigScreenFactory() {
return parent -> AutoConfig.getConfigScreen(ConfigPOJO.class, parent).get();
}
}

View File

@ -28,7 +28,6 @@ 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;
@ -43,7 +42,7 @@ public final class FileSuggestionProvider implements SuggestionProvider<ServerCo
public CompletableFuture<Suggestions> getSuggestions(CommandContext<ServerCommandSource> ctx, SuggestionsBuilder builder) throws CommandSyntaxException {
String remaining = builder.getRemaining();
for (RestoreHelper.RestoreableFile file : RestoreHelper.getAvailableBackups(ctx.getSource().getMinecraftServer())) {
for (RestoreHelper.RestoreableFile file : RestoreHelper.getAvailableBackups(ctx.getSource().getServer())) {
String formattedCreationTime = file.getCreationTime().format(Statics.defaultDateTimeFormatter);
if (formattedCreationTime.startsWith(remaining)) {

View File

@ -21,22 +21,24 @@ package net.szum123321.textile_backup.commands.create;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.LiteralText;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.create.BackupHelper;
import net.szum123321.textile_backup.core.Utilities;
public class CleanupCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("cleanup")
.executes(ctx -> execute(ctx.getSource()));
}
private static int execute(ServerCommandSource source) {
Statics.LOGGER.sendInfo(
log.sendInfo(
source,
"Deleted: {} files.",
BackupHelper.executeFileLimit(source, Utilities.getLevelName(source.getMinecraftServer()))
BackupHelper.executeFileLimit(source, Utilities.getLevelName(source.getServer()))
);
return 1;

View File

@ -23,12 +23,16 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.core.create.BackupHelper;
import javax.annotation.Nullable;
public class StartBackupCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("start")
.then(CommandManager.argument("comment", StringArgumentType.string())
@ -51,7 +55,7 @@ public class StartBackupCommand {
)
);
} catch (Exception e) {
Statics.LOGGER.error("Something went wrong while executing command!", e);
log.error("Something went wrong while executing command!", e);
throw e;
}
}

View File

@ -1,16 +1,38 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2021 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.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import io.github.cottonmc.cotton.config.ConfigManager;
import net.minecraft.command.argument.EntityArgumentType;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
public class BlacklistCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("blacklist")
.then(CommandManager.literal("add")
@ -27,7 +49,7 @@ public class BlacklistCommand {
}
private static int help(ServerCommandSource source) {
Statics.LOGGER.sendInfo(source, "Available command are: add [player], remove [player], list.");
log.sendInfo(source, "Available command are: add [player], remove [player], list.");
return 1;
}
@ -37,12 +59,12 @@ public class BlacklistCommand {
builder.append("Currently on the blacklist are: ");
for(String name : Statics.CONFIG.playerBlacklist){
for(String name : config.get().playerBlacklist){
builder.append(name);
builder.append(", ");
}
Statics.LOGGER.sendInfo(source, builder.toString());
log.sendInfo(source, builder.toString());
return 1;
}
@ -50,11 +72,11 @@ public class BlacklistCommand {
private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(Statics.CONFIG.playerBlacklist.contains(player.getEntityName())) {
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} is already blacklisted.", player.getEntityName());
if(config.get().playerBlacklist.contains(player.getEntityName())) {
log.sendInfo(ctx.getSource(), "Player: {} is already blacklisted.", player.getEntityName());
} else {
Statics.CONFIG.playerBlacklist.add(player.getEntityName());
ConfigManager.saveConfig(Statics.CONFIG);
config.get().playerBlacklist.add(player.getEntityName());
config.save();
StringBuilder builder = new StringBuilder();
@ -62,16 +84,17 @@ public class BlacklistCommand {
builder.append(player.getEntityName());
builder.append(" added to the blacklist");
if(Statics.CONFIG.playerWhitelist.contains(player.getEntityName())){
Statics.CONFIG.playerWhitelist.remove(player.getEntityName());
if(config.get().playerWhitelist.contains(player.getEntityName())){
config.get().playerWhitelist.remove(player.getEntityName());
config.save();
builder.append(" and removed form the whitelist");
}
builder.append(" successfully.");
ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player);
ctx.getSource().getServer().getCommandManager().sendCommandTree(player);
Statics.LOGGER.sendInfo(ctx.getSource(), builder.toString());
log.sendInfo(ctx.getSource(), builder.toString());
}
return 1;
@ -80,15 +103,15 @@ public class BlacklistCommand {
private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(!Statics.CONFIG.playerBlacklist.contains(player.getEntityName())) {
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} newer was blacklisted.", player.getEntityName());
if(!config.get().playerBlacklist.contains(player.getEntityName())) {
log.sendInfo(ctx.getSource(), "Player: {} newer was blacklisted.", player.getEntityName());
} else {
Statics.CONFIG.playerBlacklist.remove(player.getEntityName());
ConfigManager.saveConfig(Statics.CONFIG);
config.get().playerBlacklist.remove(player.getEntityName());
config.save();
ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player);
ctx.getSource().getServer().getCommandManager().sendCommandTree(player);
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} removed from the blacklist successfully.", player.getEntityName());
log.sendInfo(ctx.getSource(), "Player: {} removed from the blacklist successfully.", player.getEntityName());
}
return 1;

View File

@ -24,6 +24,8 @@ 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.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.commands.CommandExceptions;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.commands.FileSuggestionProvider;
@ -36,6 +38,8 @@ import java.util.Arrays;
import java.util.Optional;
public class DeleteCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("delete")
.then(CommandManager.argument("file", StringArgumentType.word())
@ -53,7 +57,7 @@ public class DeleteCommand {
throw CommandExceptions.DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE.create(e);
}
File root = Utilities.getBackupRootPath(Utilities.getLevelName(source.getMinecraftServer()));
File root = Utilities.getBackupRootPath(Utilities.getLevelName(source.getServer()));
Optional<File> optionalFile = Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)
@ -63,20 +67,20 @@ public class DeleteCommand {
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());
log.sendInfo(source, "File {} successfully deleted!", optionalFile.get().getName());
if(source.getEntity() instanceof PlayerEntity)
Statics.LOGGER.info("Player {} deleted {}.", source.getPlayer().getName(), optionalFile.get().getName());
log.info("Player {} deleted {}.", source.getPlayer().getName(), optionalFile.get().getName());
} else {
Statics.LOGGER.sendError(source, "Something went wrong while deleting file!");
log.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");
log.sendError(source, "Couldn't delete the file because it's being restored right now.");
log.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");
log.sendError(source, "Couldn't find file by this name.");
log.sendHint(source, "Maybe try /backup list");
}
return 0;

View File

@ -21,16 +21,19 @@ package net.szum123321.textile_backup.commands.manage;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.restore.RestoreHelper;
import java.util.*;
public class ListBackupsCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("list")
.executes(ctx -> { StringBuilder builder = new StringBuilder();
List<RestoreHelper.RestoreableFile> backups = RestoreHelper.getAvailableBackups(ctx.getSource().getMinecraftServer());
List<RestoreHelper.RestoreableFile> backups = RestoreHelper.getAvailableBackups(ctx.getSource().getServer());
if(backups.size() == 0) {
builder.append("There a no backups available for this world.");
@ -50,7 +53,7 @@ public class ListBackupsCommand {
}
}
Statics.LOGGER.sendInfo(ctx.getSource(), builder.toString());
log.sendInfo(ctx.getSource(), builder.toString());
return 1;
});

View File

@ -1,16 +1,38 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2021 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.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import io.github.cottonmc.cotton.config.ConfigManager;
import net.minecraft.command.argument.EntityArgumentType;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
public class WhitelistCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public static LiteralArgumentBuilder<ServerCommandSource> register(){
return CommandManager.literal("whitelist")
.then(CommandManager.literal("add")
@ -27,7 +49,7 @@ public class WhitelistCommand {
}
private static int help(ServerCommandSource source){
Statics.LOGGER.sendInfo(source, "Available command are: add [player], remove [player], list.");
log.sendInfo(source, "Available command are: add [player], remove [player], list.");
return 1;
}
@ -37,12 +59,12 @@ public class WhitelistCommand {
builder.append("Currently on the whitelist are: ");
for(String name : Statics.CONFIG.playerWhitelist){
for(String name : config.get().playerWhitelist){
builder.append(name);
builder.append(", ");
}
Statics.LOGGER.sendInfo(source, builder.toString());
log.sendInfo(source, builder.toString());
return 1;
}
@ -50,11 +72,11 @@ public class WhitelistCommand {
private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(Statics.CONFIG.playerWhitelist.contains(player.getEntityName())) {
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} is already whitelisted.", player.getEntityName());
if(config.get().playerWhitelist.contains(player.getEntityName())) {
log.sendInfo(ctx.getSource(), "Player: {} is already whitelisted.", player.getEntityName());
} else {
Statics.CONFIG.playerWhitelist.add(player.getEntityName());
ConfigManager.saveConfig(Statics.CONFIG);
config.get().playerWhitelist.add(player.getEntityName());
config.save();
StringBuilder builder = new StringBuilder();
@ -62,16 +84,17 @@ public class WhitelistCommand {
builder.append(player.getEntityName());
builder.append(" added to the whitelist");
if(Statics.CONFIG.playerBlacklist.contains(player.getEntityName())){
Statics.CONFIG.playerBlacklist.remove(player.getEntityName());
if(config.get().playerBlacklist.contains(player.getEntityName())){
config.get().playerBlacklist.remove(player.getEntityName());
config.save();
builder.append(" and removed form the blacklist");
}
builder.append(" successfully.");
ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player);
ctx.getSource().getServer().getCommandManager().sendCommandTree(player);
Statics.LOGGER.sendInfo(ctx.getSource(), builder.toString());
log.sendInfo(ctx.getSource(), builder.toString());
}
return 1;
@ -80,15 +103,15 @@ public class WhitelistCommand {
private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(!Statics.CONFIG.playerWhitelist.contains(player.getEntityName())) {
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} newer was whitelisted.", player.getEntityName());
if(!config.get().playerWhitelist.contains(player.getEntityName())) {
log.sendInfo(ctx.getSource(), "Player: {} newer was whitelisted.", player.getEntityName());
} else {
Statics.CONFIG.playerWhitelist.remove(player.getEntityName());
ConfigManager.saveConfig(Statics.CONFIG);
config.get().playerWhitelist.remove(player.getEntityName());
config.save();
ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player);
ctx.getSource().getServer().getCommandManager().sendCommandTree(player);
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} removed from the whitelist successfully.", player.getEntityName());
log.sendInfo(ctx.getSource(), "Player: {} removed from the whitelist successfully.", player.getEntityName());
}
return 1;

View File

@ -23,10 +23,13 @@ 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 net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import java.util.Optional;
public class KillRestoreCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("killR")
.executes(ctx -> {
@ -35,16 +38,16 @@ public class KillRestoreCommand {
Statics.globalShutdownBackupFlag.set(true);
Statics.untouchableFile = Optional.empty();
Statics.LOGGER.info("{} cancelled backup restoration.", ctx.getSource().getEntity() instanceof PlayerEntity ?
log.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.");
log.sendInfo(ctx.getSource(), "Backup restoration successfully stopped.");
} else {
Statics.LOGGER.sendInfo(ctx.getSource(), "Failed to stop backup restoration");
log.sendInfo(ctx.getSource(), "Failed to stop backup restoration");
}
return 1;
});

View File

@ -24,6 +24,8 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.commands.CommandExceptions;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.commands.FileSuggestionProvider;
@ -36,6 +38,8 @@ import java.time.format.DateTimeParseException;
import java.util.Optional;
public class RestoreBackupCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("restore")
.then(CommandManager.argument("file", StringArgumentType.word())
@ -57,9 +61,9 @@ public class RestoreBackupCommand {
).executes(context -> {
ServerCommandSource source = context.getSource();
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: /backup restore 2020-08-05_10.58.33");
log.sendInfo(source, "To restore given backup you have to provide exact creation time in format:");
log.sendInfo(source, "[YEAR]-[MONTH]-[DAY]_[HOUR].[MINUTE].[SECOND]");
log.sendInfo(source, "Example: /backup restore 2020-08-05_10.58.33");
return 1;
});
@ -75,12 +79,12 @@ public class RestoreBackupCommand {
throw CommandExceptions.DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE.create(e);
}
Optional<RestoreHelper.RestoreableFile> backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getMinecraftServer());
Optional<RestoreHelper.RestoreableFile> backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getServer());
if(backupFile.isPresent()) {
Statics.LOGGER.info("Found file to restore {}", backupFile.get().getFile().getName());
log.info("Found file to restore {}", backupFile.get().getFile().getName());
} else {
Statics.LOGGER.sendInfo(source, "No file created on {} was found!", dateTime.format(Statics.defaultDateTimeFormatter));
log.sendInfo(source, "No file created on {} was found!", dateTime.format(Statics.defaultDateTimeFormatter));
return 0;
}
@ -97,7 +101,7 @@ public class RestoreBackupCommand {
return 1;
} else {
Statics.LOGGER.sendInfo(source, "Someone has already started another restoration.");
log.sendInfo(source, "Someone has already started another restoration.");
return 0;
}

View File

@ -0,0 +1,32 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2021 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.config;
import me.shedaniel.autoconfig.ConfigHolder;
public class ConfigHelper {
public static final ConfigHelper INSTANCE = new ConfigHelper();
private ConfigHolder<ConfigPOJO> configHolder;
public static void updateInstance(ConfigHolder<ConfigPOJO> ch) { INSTANCE.configHolder = ch; }
public ConfigPOJO get() { return configHolder.get(); }
public void save() { configHolder.save(); }
}

View File

@ -0,0 +1,197 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2021 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.config;
import me.shedaniel.autoconfig.ConfigData;
import me.shedaniel.autoconfig.annotation.Config;
import me.shedaniel.autoconfig.annotation.ConfigEntry;
import me.shedaniel.cloth.clothconfig.shadowed.blue.endless.jankson.Comment;
import net.szum123321.textile_backup.TextileBackup;
import java.time.format.DateTimeFormatter;
import java.util.*;
@Config(name = TextileBackup.MOD_ID)
public class ConfigPOJO implements ConfigData {
@Comment("\nShould every world have its own backup folder?\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Gui.Excluded
public boolean perWorldBackup = true;
@Comment("""
\nTime between automatic backups in seconds
When set to 0 backups will not be performed automatically
""")
@ConfigEntry.Gui.Tooltip()
@ConfigEntry.Category("Create")
public long backupInterval = 3600;
@Comment("\nDelay in seconds between typing-in /backup restore and it actually starting\n")
@ConfigEntry.Gui.Tooltip()
@ConfigEntry.Category("Restore")
public int restoreDelay = 30;
@Comment("\nShould backups be done even if there are no players?\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Create")
public boolean doBackupsOnEmptyServer = false;
@Comment("\nShould backup be made on server shutdown?\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Create")
public boolean shutdownBackup = true;
@Comment("\nShould world be backed up before restoring a backup?\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Restore")
public boolean backupOldWorlds = true;
@Comment("\nA path to the backup folder\n")
@ConfigEntry.Gui.NoTooltip()
public String path = "backup/";
@Comment("""
\nThis setting allows you to exclude files form being backed-up.
Be very careful when setting it, as it is easy corrupt your world!
""")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Create")
public List<String> fileBlacklist = new ArrayList<>();
@Comment("\nShould backups be deleted after being restored?\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Restore")
public boolean deleteOldBackupAfterRestore = true;
@Comment("\nMaximum number of backups to keep. If set to 0 then no backup will be deleted based their amount\n")
@ConfigEntry.Gui.NoTooltip()
public int backupsToKeep = 10;
@Comment("\nMaximum age of backups to keep in seconds.\n If set to 0 then backups will not be deleted based their age \n")
@ConfigEntry.Gui.NoTooltip()
public long maxAge = 0;
@Comment("""
\nMaximum size of backup folder in kilo bytes (1024).
If set to 0 then backups will not be deleted
""")
@ConfigEntry.Gui.Tooltip()
public int maxSize = 0;
@Comment("\nCompression level \n0 - 9\n Only affects zip compression.\n")
@ConfigEntry.Gui.Tooltip()
@ConfigEntry.BoundedDiscrete(max = 9)
@ConfigEntry.Category("Create")
public int compression = 7;
@Comment("""
\nLimit how many cores can be used for compression.
0 means that all available cores will be used
""")
@ConfigEntry.Gui.Tooltip()
@ConfigEntry.Category("Create")
public int compressionCoreCountLimit = 0;
@Comment("""
\nAvailable formats are:
ZIP - normal zip archive using standard deflate compression
GZIP - tar.gz using gzip compression
BZIP2 - tar.bz2 archive using bzip2 compression
LZMA - tar.xz using lzma compression
TAR - .tar with no compression
""")
@ConfigEntry.Gui.Tooltip()
@ConfigEntry.Category("Create")
@ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
public ArchiveFormat format = ArchiveFormat.ZIP;
@Comment("\nMinimal permission level required to run commands\n")
@ConfigEntry.Category("Manage")
@ConfigEntry.Gui.NoTooltip()
public int permissionLevel = 4;
@Comment("\nPlayer on singleplayer is always allowed to run command. Warning! On lan party everyone will be allowed to run it.\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Manage")
public boolean alwaysSingleplayerAllowed = true;
@Comment("\nPlayers allowed to run backup commands without sufficient permission level\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Manage")
public List<String> playerWhitelist = new ArrayList<>();
@Comment("\nPlayers banned from running backup commands besides their sufficient permission level\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Manage")
public List<String> playerBlacklist = new ArrayList<>();
@Comment("""
\nFormat of date&time used to name backup files.
Remember not to use '#' symbol or any other character that is not allowed by your operating system such as:
':', '\\', etc...
For more info: https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html
""")
@ConfigEntry.Gui.Tooltip()
public String dateTimeFormat = "yyyy.MM.dd_HH-mm-ss";
@Override
public void validatePostLoad() throws ValidationException {
if(compressionCoreCountLimit > Runtime.getRuntime().availableProcessors())
throw new ValidationException("compressionCoreCountLimit is too high! Your system only has: " + Runtime.getRuntime().availableProcessors() + " cores!");
try {
DateTimeFormatter.ofPattern(dateTimeFormat);
} catch (IllegalArgumentException e) {
throw new ValidationException(
"dateTimeFormat is wrong! See: https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html",
e
);
}
}
public enum ArchiveFormat {
ZIP("zip"),
GZIP("tar", "gz"),
BZIP2("tar", "bz2"),
LZMA("tar", "xz"),
TAR("tar");
private final List<String> extensionPieces;
ArchiveFormat(String... extensionParts) {
extensionPieces = Arrays.asList(extensionParts);
}
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

@ -1,3 +1,21 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2021 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;
public interface LivingServer {

View File

@ -1,3 +1,21 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2021 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;
import java.io.IOException;

View File

@ -21,7 +21,8 @@ package net.szum123321.textile_backup.core;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.world.World;
import net.szum123321.textile_backup.ConfigHandler;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.config.ConfigPOJO;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.mixin.MinecraftServerSessionAccessor;
@ -36,6 +37,8 @@ import java.util.Arrays;
import java.util.Optional;
public class Utilities {
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public static String getLevelName(MinecraftServer server) {
return ((MinecraftServerSessionAccessor)server).getSession().getDirectoryName();
}
@ -47,27 +50,15 @@ public class Utilities {
}
public static File getBackupRootPath(String worldName) {
File path = new File(Statics.CONFIG.path).getAbsoluteFile();
File path = new File(config.get().path).getAbsoluteFile();
if (Statics.CONFIG.perWorldBackup)
path = path.toPath().resolve(worldName).toFile();
if (config.get().perWorldBackup) path = path.toPath().resolve(worldName).toFile();
if (!path.exists()) {
path.mkdirs();
}
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)
@ -88,26 +79,21 @@ public class Utilities {
public static boolean isBlacklisted(Path path) {
if(isWindows()) { //hotfix!
if (path.getFileName().toString().equals("session.lock")) {
Statics.LOGGER.trace("Skipping session.lock");
return true;
}
if (path.getFileName().toString().equals("session.lock")) return true;
}
for(String i : Statics.CONFIG.fileBlacklist) if(path.startsWith(i)) return true;
return false;
return config.get().fileBlacklist.stream().anyMatch(path::startsWith);
}
public static Optional<ConfigHandler.ArchiveFormat> getArchiveExtension(String fileName) {
public static Optional<ConfigPOJO.ArchiveFormat> getArchiveExtension(String fileName) {
String[] parts = fileName.split("\\.");
return Arrays.stream(ConfigHandler.ArchiveFormat.values())
return Arrays.stream(ConfigPOJO.ArchiveFormat.values())
.filter(format -> format.getLastPiece().equals(parts[parts.length - 1]))
.findAny();
}
public static Optional<ConfigHandler.ArchiveFormat> getArchiveExtension(File f) {
public static Optional<ConfigPOJO.ArchiveFormat> getArchiveExtension(File f) {
return getArchiveExtension(f.getName());
}
@ -155,7 +141,7 @@ public class Utilities {
}
public static DateTimeFormatter getDateTimeFormatter() {
return DateTimeFormatter.ofPattern(Statics.CONFIG.dateTimeFormat);
return DateTimeFormatter.ofPattern(config.get().dateTimeFormat);
}
public static DateTimeFormatter getBackupDateTimeFormatter() {

View File

@ -24,17 +24,11 @@ import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.core.ActionInitiator;
import org.jetbrains.annotations.NotNull;
public record BackupContext(MinecraftServer server,
public record BackupContext(@NotNull MinecraftServer server,
ServerCommandSource commandSource,
ActionInitiator initiator, boolean save,
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;
this.save = save;
this.comment = comment;
}
public MinecraftServer getServer() {
return server;
@ -121,14 +115,12 @@ public record BackupContext(MinecraftServer server,
}
if (server == null) {
if (commandSource != null)
setServer(commandSource.getMinecraftServer());
if (commandSource != null) setServer(commandSource.getServer());
else
throw new RuntimeException("Both MinecraftServer and ServerCommandSource weren't provided!");
throw new RuntimeException("Neither MinecraftServer or ServerCommandSource were provided!");
}
return new BackupContext(server, commandSource, initiator, save, comment);
}
}
}

View File

@ -25,6 +25,9 @@ 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.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.Utilities;
import org.apache.commons.io.FileUtils;
@ -37,6 +40,9 @@ import java.util.Comparator;
import java.util.UUID;
public class BackupHelper {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public static Runnable create(BackupContext ctx) {
notifyPlayers(ctx);
@ -46,26 +52,25 @@ public class BackupHelper {
builder.append(ctx.getInitiator().getPrefix());
if(ctx.startedByPlayer()) {
if(ctx.startedByPlayer())
builder.append(ctx.getCommandSource().getDisplayName().getString());
} else {
else
builder.append(ctx.getInitiator().getName());
}
builder.append(" on: ");
builder.append(Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
Statics.LOGGER.info(builder.toString());
log.info(builder.toString());
if (ctx.shouldSave()) {
Statics.LOGGER.sendInfoAL(ctx, "Saving server...");
log.sendInfoAL(ctx, "Saving server...");
ctx.getServer().getPlayerManager().saveAllPlayerData();
try {
ctx.getServer().save(false, true, true);
} catch (Exception e) {
Statics.LOGGER.sendErrorAL(ctx,"An exception occurred when trying to save the world!");
log.sendErrorAL(ctx,"An exception occurred when trying to save the world!");
}
}
@ -73,7 +78,7 @@ public class BackupHelper {
}
private static void notifyPlayers(BackupContext ctx) {
MutableText message = Statics.LOGGER.getPrefixText();
MutableText message = log.getPrefixText();
message.append(new LiteralText("Warning! Server backup will begin shortly. You may experience some lag.").formatted(Formatting.WHITE));
UUID uuid;
@ -94,30 +99,30 @@ public class BackupHelper {
int deletedFiles = 0;
if (root.isDirectory() && root.exists() && root.listFiles() != null) {
if (Statics.CONFIG.maxAge > 0) { // delete files older that configured
if (config.get().maxAge > 0) { // delete files older that configured
final LocalDateTime now = LocalDateTime.now();
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)
.filter(f -> now.toEpochSecond(ZoneOffset.UTC) - Utilities.getFileCreationTime(f).get().toEpochSecond(ZoneOffset.UTC) > config.get().maxAge)
.map(f -> deleteFile(f, ctx))
.filter(b -> b).count(); //a bit awkward
}
if (Statics.CONFIG.backupsToKeep > 0 && root.listFiles().length > Statics.CONFIG.backupsToKeep) {
if (config.get().backupsToKeep > 0 && root.listFiles().length > config.get().backupsToKeep) {
deletedFiles += Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime((File) f).get()).reversed())
.skip(Statics.CONFIG.backupsToKeep)
.skip(config.get().backupsToKeep)
.map(f -> deleteFile(f, ctx))
.filter(b -> b).count();
}
if (Statics.CONFIG.maxSize > 0 && FileUtils.sizeOfDirectory(root) / 1024 > Statics.CONFIG.maxSize) {
if (config.get().maxSize > 0 && FileUtils.sizeOfDirectory(root) / 1024 > config.get().maxSize) {
deletedFiles += Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get()))
.takeWhile(f -> FileUtils.sizeOfDirectory(root) / 1024 > Statics.CONFIG.maxSize)
.takeWhile(f -> FileUtils.sizeOfDirectory(root) / 1024 > config.get().maxSize)
.map(f -> deleteFile(f, ctx))
.filter(b -> b).count();
}
@ -129,10 +134,10 @@ public class BackupHelper {
private static boolean deleteFile(File f, ServerCommandSource ctx) {
if(Statics.untouchableFile.isEmpty()|| !Statics.untouchableFile.get().equals(f)) {
if(f.delete()) {
Statics.LOGGER.sendInfoAL(ctx, "Deleting: {}", f.getName());
log.sendInfoAL(ctx, "Deleting: {}", f.getName());
return true;
} else {
Statics.LOGGER.sendErrorAL(ctx, "Something went wrong while deleting: {}.", f.getName());
log.sendErrorAL(ctx, "Something went wrong while deleting: {}.", f.getName());
}
}

View File

@ -20,11 +20,14 @@ package net.szum123321.textile_backup.core.create;
import net.minecraft.server.MinecraftServer;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.ActionInitiator;
import java.time.Instant;
public class BackupScheduler {
private final static ConfigHelper config = ConfigHelper.INSTANCE;
private boolean scheduled;
private long nextBackup;
@ -34,9 +37,10 @@ public class BackupScheduler {
}
public void tick(MinecraftServer server) {
if(config.get().backupInterval < 1) return;
long now = Instant.now().getEpochSecond();
if(Statics.CONFIG.doBackupsOnEmptyServer || server.getPlayerManager().getCurrentPlayerCount() > 0) {
if(config.get().doBackupsOnEmptyServer || server.getPlayerManager().getCurrentPlayerCount() > 0) {
if(scheduled) {
if(nextBackup <= now) {
Statics.executorService.submit(
@ -50,13 +54,13 @@ public class BackupScheduler {
)
);
nextBackup = now + Statics.CONFIG.backupInterval;
nextBackup = now + config.get().backupInterval;
}
} else {
nextBackup = now + Statics.CONFIG.backupInterval;
nextBackup = now + config.get().backupInterval;
scheduled = true;
}
} else if(!Statics.CONFIG.doBackupsOnEmptyServer && server.getPlayerManager().getCurrentPlayerCount() == 0) {
} else if(!config.get().doBackupsOnEmptyServer && server.getPlayerManager().getCurrentPlayerCount() == 0) {
if(scheduled && nextBackup <= now) {
Statics.executorService.submit(
BackupHelper.create(

View File

@ -19,6 +19,9 @@
package net.szum123321.textile_backup.core.create;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.create.compressors.*;
import net.szum123321.textile_backup.core.Utilities;
@ -33,6 +36,9 @@ import java.io.OutputStream;
import java.time.LocalDateTime;
public class MakeBackupRunnable implements Runnable {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
private final BackupContext context;
public MakeBackupRunnable(BackupContext context){
@ -45,11 +51,11 @@ public class MakeBackupRunnable implements Runnable {
Utilities.disableWorldSaving(context.getServer());
Statics.disableWatchdog = true;
Statics.LOGGER.sendInfoAL(context, "Starting backup");
log.sendInfoAL(context, "Starting backup");
File world = Utilities.getWorldFolder(context.getServer());
Statics.LOGGER.trace("Minecraft world is: {}", world);
log.trace("Minecraft world is: {}", world);
File outFile = Utilities
.getBackupRootPath(Utilities.getLevelName(context.getServer()))
@ -57,34 +63,34 @@ public class MakeBackupRunnable implements Runnable {
.resolve(getFileName())
.toFile();
Statics.LOGGER.trace("Outfile is: {}", outFile);
log.trace("Outfile is: {}", outFile);
outFile.getParentFile().mkdirs();
try {
outFile.createNewFile();
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred when trying to create new backup file!", e);
log.error("An exception occurred when trying to create new backup file!", e);
if(context.getInitiator() == ActionInitiator.Player)
Statics.LOGGER.sendError(context, "An exception occurred when trying to create new backup file!");
log.sendError(context, "An exception occurred when trying to create new backup file!");
return;
}
int coreCount;
if(Statics.CONFIG.compressionCoreCountLimit <= 0) {
if(config.get().compressionCoreCountLimit <= 0) {
coreCount = Runtime.getRuntime().availableProcessors();
} else {
coreCount = Math.min(Statics.CONFIG.compressionCoreCountLimit, Runtime.getRuntime().availableProcessors());
coreCount = Math.min(config.get().compressionCoreCountLimit, Runtime.getRuntime().availableProcessors());
}
Statics.LOGGER.trace("Running compression on {} threads. Available cores: {}", coreCount, Runtime.getRuntime().availableProcessors());
log.trace("Running compression on {} threads. Available cores: {}", coreCount, Runtime.getRuntime().availableProcessors());
switch (Statics.CONFIG.format) {
switch (config.get().format) {
case ZIP -> {
if (Statics.tmpAvailable && coreCount > 1)
if (coreCount > 1)
ParallelZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
else
ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
@ -98,16 +104,16 @@ public class MakeBackupRunnable implements Runnable {
}.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);
log.warn("Specified compressor ({}) is not supported! Zip will be used instead!", config.get().format);
if (context.getInitiator() == ActionInitiator.Player)
Statics.LOGGER.sendError(context.getCommandSource(), "Error! No correct compression format specified! Using default compressor!");
log.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.sendInfoAL(context, "Done!");
log.sendInfoAL(context, "Done!");
} finally {
Utilities.enableWorldSaving(context.getServer());
Statics.disableWatchdog = false;
@ -119,6 +125,6 @@ public class MakeBackupRunnable implements Runnable {
return Utilities.getDateTimeFormatter().format(now) +
(context.getComment() != null ? "#" + context.getComment().replace("#", "") : "") +
Statics.CONFIG.format.getCompleteString();
config.get().format.getCompleteString();
}
}

View File

@ -18,7 +18,8 @@
package net.szum123321.textile_backup.core.create.compressors;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.NoSpaceLeftOnDeviceException;
import net.szum123321.textile_backup.core.Utilities;
@ -32,6 +33,8 @@ import java.time.Instant;
import java.util.concurrent.ExecutionException;
public abstract class AbstractCompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public void createArchive(File inputFile, File outputFile, BackupContext ctx, int coreLimit) {
Instant start = Instant.now();
@ -48,35 +51,38 @@ public abstract class AbstractCompressor {
//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);
log.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!");
log.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);
log.error("""
CRITICAL ERROR OCCURRED!
The backup is corrupt!
Don't panic! This is a known issue!
For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems
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");
log.sendError(ctx, "Backup failed. The file is corrupt.");
log.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);
log.error("An exception occurred!", e);
} catch (Exception e) {
if(ctx.getInitiator() == ActionInitiator.Player)
Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!");
log.sendError(ctx, "Something went wrong while compressing files!");
} finally {
close();
}
close();
// close();
Statics.LOGGER.sendInfoAL(ctx, "Compression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
log.sendInfoAL(ctx, "Compression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
}
protected abstract OutputStream createArchiveOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException;

View File

@ -18,7 +18,8 @@
package net.szum123321.textile_backup.core.create.compressors;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.NoSpaceLeftOnDeviceException;
import net.szum123321.textile_backup.core.create.BackupContext;
import org.apache.commons.compress.archivers.zip.*;
@ -36,6 +37,8 @@ import java.util.zip.ZipEntry;
https://stackoverflow.com/users/2987755/dkb
*/
public class ParallelZipCompressor extends ZipCompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
//These fields are used to discriminate against the issue #51
private final static SimpleStackTraceElement[] STACKTRACE = {
new SimpleStackTraceElement("sun.nio.ch.FileDispatcherImpl", "write0", true),
@ -130,7 +133,7 @@ public class ParallelZipCompressor extends ZipCompressor {
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);
log.error("An exception occurred while trying to create an input stream from file: {}!", sourceFile.getName(), e);
}
return null;

View File

@ -18,7 +18,7 @@
package net.szum123321.textile_backup.core.create.compressors;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.create.BackupContext;
import org.apache.commons.compress.archivers.zip.Zip64Mode;
@ -27,12 +27,13 @@ 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 {
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public static ZipCompressor getInstance() {
return new ZipCompressor();
}
@ -43,7 +44,7 @@ public class ZipCompressor extends AbstractCompressor {
arc.setMethod(ZipArchiveOutputStream.DEFLATED);
arc.setUseZip64(Zip64Mode.AsNeeded);
arc.setLevel(Statics.CONFIG.compression);
arc.setLevel(config.get().compression);
arc.setComment("Created on: " + Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
return arc;

View File

@ -18,7 +18,8 @@
package net.szum123321.textile_backup.core.restore;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import java.util.concurrent.atomic.AtomicInteger;
@ -26,6 +27,7 @@ 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 TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static AtomicInteger threadCounter = new AtomicInteger(0);
private final int delay;
@ -40,13 +42,13 @@ public class AwaitThread extends Thread {
@Override
public void run() {
Statics.LOGGER.info("Countdown begins... Waiting {} second.", delay);
log.info("Countdown begins... Waiting {} second.", delay);
// 𝄞 This is final count down! Tu ruru Tu, Tu Ru Tu Tu
try {
Thread.sleep(delay * 1000L);
} catch (InterruptedException e) {
Statics.LOGGER.info("Backup restoration cancelled.");
log.info("Backup restoration cancelled.");
return;
}

View File

@ -18,7 +18,10 @@
package net.szum123321.textile_backup.core.restore;
import net.szum123321.textile_backup.ConfigHandler;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.config.ConfigPOJO;
import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.LivingServer;
import net.szum123321.textile_backup.Statics;
@ -31,6 +34,9 @@ import net.szum123321.textile_backup.core.restore.decompressors.ZipDecompressor;
import java.io.File;
public class RestoreBackupRunnable implements Runnable {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
private final RestoreContext ctx;
public RestoreBackupRunnable(RestoreContext ctx) {
@ -41,12 +47,12 @@ public class RestoreBackupRunnable implements Runnable {
public void run() {
Statics.globalShutdownBackupFlag.set(false);
Statics.LOGGER.info("Shutting down server...");
log.info("Shutting down server...");
ctx.getServer().stop(false);
awaitServerShutdown();
if(Statics.CONFIG.backupOldWorlds) {
if(config.get().backupOldWorlds) {
BackupHelper.create(
BackupContext.Builder
.newBackupContextBuilder()
@ -59,31 +65,34 @@ public class RestoreBackupRunnable implements Runnable {
File worldFile = Utilities.getWorldFolder(ctx.getServer());
Statics.LOGGER.info("Deleting old world...");
log.info("Deleting old world...");
if(!deleteDirectory(worldFile))
Statics.LOGGER.error("Something went wrong while deleting old world!");
log.error("Something went wrong while deleting old world!");
worldFile.mkdirs();
Statics.LOGGER.info("Starting decompression...");
log.info("Starting decompression...");
if(ctx.getFile().getArchiveFormat() == ConfigHandler.ArchiveFormat.ZIP)
if(ctx.getFile().getArchiveFormat() == ConfigPOJO.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(config.get().deleteOldBackupAfterRestore) {
log.info("Deleting old backup");
if(!ctx.getFile().getFile().delete())
Statics.LOGGER.info("Something went wrong while deleting old backup");
if(!ctx.getFile().getFile().delete()) log.info("Something went wrong while deleting old backup");
}
//in case we're playing on client
Statics.globalShutdownBackupFlag.set(true);
Statics.LOGGER.info("Done!");
log.info("Done!");
//Might solve #37
//Idk if it's a good idea...
//Runtime.getRuntime().exit(0);
}
private void awaitServerShutdown() {
@ -91,7 +100,7 @@ public class RestoreBackupRunnable implements Runnable {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Statics.LOGGER.error("Exception occurred!", e);
log.error("Exception occurred!", e);
}
}
}

View File

@ -26,16 +26,10 @@ import net.szum123321.textile_backup.core.ActionInitiator;
import javax.annotation.Nullable;
public record RestoreContext(RestoreHelper.RestoreableFile file,
MinecraftServer server, @Nullable String comment,
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;
@ -92,7 +86,7 @@ public record RestoreContext(RestoreHelper.RestoreableFile file,
}
public RestoreContext build() {
if (server == null) server = serverCommandSource.getMinecraftServer();
if (server == null) server = serverCommandSource.getServer();
ActionInitiator initiator = serverCommandSource.getEntity() instanceof PlayerEntity ? ActionInitiator.Player : ActionInitiator.ServerConsole;

View File

@ -24,7 +24,10 @@ 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.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.config.ConfigPOJO;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.Utilities;
@ -36,6 +39,9 @@ import java.util.*;
import java.util.stream.Collectors;
public class RestoreHelper {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public static Optional<RestoreableFile> findFileAndLockIfPresent(LocalDateTime backupTime, MinecraftServer server) {
File root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
@ -52,24 +58,24 @@ public class RestoreHelper {
public static AwaitThread create(RestoreContext ctx) {
if(ctx.getInitiator() == ActionInitiator.Player)
Statics.LOGGER.info("Backup restoration was initiated by: {}", ctx.getCommandSource().getName());
log.info("Backup restoration was initiated by: {}", ctx.getCommandSource().getName());
else
Statics.LOGGER.info("Backup restoration was initiated form Server Console");
log.info("Backup restoration was initiated form Server Console");
notifyPlayers(ctx);
return new AwaitThread(
Statics.CONFIG.restoreDelay,
config.get().restoreDelay,
new RestoreBackupRunnable(ctx)
);
}
private static void notifyPlayers(RestoreContext ctx) {
MutableText message = Statics.LOGGER.getPrefixText();
MutableText message = log.getPrefixText();
message.append(
new LiteralText(
"Warning! The server is going to shut down in " +
Statics.CONFIG.restoreDelay +
config.get().restoreDelay +
" seconds!"
).formatted(Formatting.WHITE)
);
@ -100,7 +106,7 @@ public class RestoreHelper {
public static class RestoreableFile implements Comparable<RestoreableFile> {
private final File file;
private final ConfigHandler.ArchiveFormat archiveFormat;
private final ConfigPOJO.ArchiveFormat archiveFormat;
private final LocalDateTime creationTime;
private final String comment;
@ -131,7 +137,7 @@ public class RestoreHelper {
return file;
}
public ConfigHandler.ArchiveFormat getArchiveFormat() {
public ConfigPOJO.ArchiveFormat getArchiveFormat() {
return archiveFormat;
}

View File

@ -18,7 +18,8 @@
package net.szum123321.textile_backup.core.restore.decompressors;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.Utilities;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
@ -32,6 +33,8 @@ import java.time.Duration;
import java.time.Instant;
public class GenericTarDecompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static void decompress(File input, File target) {
Instant start = Instant.now();
@ -43,7 +46,7 @@ public class GenericTarDecompressor {
while ((entry = archiveInputStream.getNextTarEntry()) != null) {
if(!archiveInputStream.canReadEntryData(entry)) {
Statics.LOGGER.error("Something when wrong while trying to decompress {}", entry.getName());
log.error("Something when wrong while trying to decompress {}", entry.getName());
continue;
}
@ -55,22 +58,22 @@ public class GenericTarDecompressor {
File parent = file.getParentFile();
if (!parent.isDirectory() && !parent.mkdirs()) {
Statics.LOGGER.error("Failed to create {}", parent);
log.error("Failed to create {}", parent);
} else {
try (OutputStream outputStream = Files.newOutputStream(file.toPath());
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
IOUtils.copy(archiveInputStream, bufferedOutputStream);
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred while trying to decompress file: {}", file.getName(), e);
log.error("An exception occurred while trying to decompress file: {}", file.getName(), e);
}
}
}
}
} catch (IOException | CompressorException e) {
Statics.LOGGER.error("An exception occurred! ", e);
log.error("An exception occurred! ", e);
}
Statics.LOGGER.info("Decompression took {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
log.info("Decompression took {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
}
private static InputStream getCompressorInputStream(InputStream inputStream) throws CompressorException {

View File

@ -18,7 +18,8 @@
package net.szum123321.textile_backup.core.restore.decompressors;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.Utilities;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
@ -30,6 +31,8 @@ import java.time.Duration;
import java.time.Instant;
public class ZipDecompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static void decompress(File inputFile, File target) {
Instant start = Instant.now();
@ -40,7 +43,7 @@ public class ZipDecompressor {
while ((entry = zipInputStream.getNextZipEntry()) != null) {
if(!zipInputStream.canReadEntryData(entry)){
Statics.LOGGER.error("Something when wrong while trying to decompress {}", entry.getName());
log.error("Something when wrong while trying to decompress {}", entry.getName());
continue;
}
@ -52,21 +55,21 @@ public class ZipDecompressor {
File parent = file.getParentFile();
if (!parent.isDirectory() && !parent.mkdirs()) {
Statics.LOGGER.error("Failed to create {}", parent);
log.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);
log.error("An exception occurred while trying to decompress file: {}", file.getName(), e);
}
}
}
}
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred! ", e);
log.error("An exception occurred! ", e);
}
Statics.LOGGER.info("Decompression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
log.info("Decompression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
}
}

View File

@ -1,3 +1,21 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2021 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.mixin;
import net.minecraft.server.dedicated.DedicatedServerWatchdog;

View File

@ -1,3 +1,21 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2021 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.mixin;
import net.minecraft.server.MinecraftServer;

View File

@ -0,0 +1,56 @@
{
"text.autoconfig.textile_backup.title": "Textile Backup Configuration",
"text.autoconfig.textile_backup.category.default": "General",
"text.autoconfig.textile_backup.category.Create": "Backup settings",
"text.autoconfig.textile_backup.category.Restore": "Restore",
"text.autoconfig.textile_backup.category.Manage": "Management",
"text.autoconfig.textile_backup.option.backupInterval": "Backup Interval",
"text.autoconfig.textile_backup.option.backupInterval.@Tooltip": "Time between each automatic backup (in seconds)",
"text.autoconfig.textile_backup.option.restoreDelay": "Restore Delay",
"text.autoconfig.textile_backup.option.restoreDelay.@Tooltip": "In seconds",
"text.autoconfig.textile_backup.option.doBackupsOnEmptyServer": "Make automatic backups on empty server",
"text.autoconfig.textile_backup.option.shutdownBackup": "Make a backup on shutdown",
"text.autoconfig.textile_backup.option.backupOldWorlds": "Backup old worlds",
"text.autoconfig.textile_backup.option.perWorldBackup": "Use separate folders for different worlds",
"text.autoconfig.textile_backup.option.path": "Path to backup folder",
"text.autoconfig.textile_backup.option.fileBlacklist": "Blacklisted files",
"text.autoconfig.textile_backup.option.deleteOldBackupAfterRestore": "Delete restored backup",
"text.autoconfig.textile_backup.option.backupsToKeep": "Number of backups to keep",
"text.autoconfig.textile_backup.option.maxAge": "Max age of backup",
"text.autoconfig.textile_backup.option.maxAge.@Tooltip": "In seconds since creation",
"text.autoconfig.textile_backup.option.maxSize": "Max size of backup folder",
"text.autoconfig.textile_backup.option.maxSize.@Tooltip": "In KBytes",
"text.autoconfig.textile_backup.option.compression": "Compression level",
"text.autoconfig.textile_backup.option.compression.@Tooltip": "Only affects zip",
"text.autoconfig.textile_backup.option.compressionCoreCountLimit": "Max number of cores used for compression",
"text.autoconfig.textile_backup.option.compressionCoreCountLimit.@Tooltip": "Set to 0 to use all available cores",
"text.autoconfig.textile_backup.option.format": "Archive and compression format",
"text.autoconfig.textile_backup.option.format.@Tooltip": "See: https://github.com/Szum123321/textile_backup/wiki/Configuration#format",
"text.autoconfig.textile_backup.option.permissionLevel": "Min permission level",
"text.autoconfig.textile_backup.option.alwaysSingleplayerAllowed": "Always allow on single-player",
"text.autoconfig.textile_backup.option.playerWhitelist": "Admin Whitelist",
"text.autoconfig.textile_backup.option.playerBlacklist": "Admin Blacklist",
"text.autoconfig.textile_backup.option.dateTimeFormat": "Date&Time format",
"text.autoconfig.textile_backup.option.dateTimeFormat.@Tooltip": "See: https://github.com/Szum123321/textile_backup/wiki/Date-time-format"
}

View File

@ -27,6 +27,9 @@
"entrypoints": {
"main": [
"net.szum123321.textile_backup.TextileBackup"
],
"modmenu": [
"net.szum123321.textile_backup.client.ModMenuEntry"
]
},
"mixins": [
@ -37,8 +40,13 @@
"fabricloader": ">=0.11",
"fabric": "*",
"minecraft": "1.17.*",
"cloth-config2": "*",
"java": ">=16"
},
"recommends": {
"modmenu": "*"
},
"custom": {
"modupdater": {