Many, many changes:

- I migrated from cotton config to Cloth config + Auto config, that way it's easy to provide modmenu integration while keeping backward-compatibility. Because settings might change at runtime it's now necessary to keep the config reference up-to-date. I done this by using wrapper class around ConfigHolder<> and having a reference to main instance in each class accessing the config.

 - Switched form using one global logger to creating new instance in every class.

 - Removed BackupScheduler from Statics. Why did I put it there in the first place?
2.x-1.16
szymon 2021-06-26 13:10:35 +02:00
parent cc912d322e
commit a8f98c460e
31 changed files with 562 additions and 345 deletions

View File

@ -16,6 +16,13 @@ minecraft {
repositories{ repositories{
maven { url 'https://server.bbkr.space/artifactory/libs-release' } maven { url 'https://server.bbkr.space/artifactory/libs-release' }
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
maven { url "https://maven.shedaniel.me/" }
maven {
url "https://maven.terraformersmc.com/releases/"
content {
includeGroup "com.terraformersmc"
}
}
} }
dependencies { dependencies {
@ -27,20 +34,30 @@ dependencies {
// Fabric API. This is technically optional, but you probably want it anyway. // Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" 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" //Mod menu
include "io.github.cottonmc.cotton:cotton-logging:1.0.0-rc.4" modImplementation("com.terraformersmc:modmenu:${project.modmenu_version}")
include "io.github.cottonmc.cotton:cotton-config:1.0.0-rc.7"
//General compression library
modImplementation "org.apache.commons:commons-compress:1.19" modImplementation "org.apache.commons:commons-compress:1.19"
include "org.apache.commons:commons-compress:1.19" include "org.apache.commons:commons-compress:1.19"
//LZMA support
modImplementation "org.tukaani:xz:1.8" modImplementation "org.tukaani:xz:1.8"
include "org.tukaani:xz:1.8" include "org.tukaani:xz:1.8"
//Gzip compression, parallel, GITHUB
modImplementation 'com.github.shevek:parallelgzip:master-SNAPSHOT' modImplementation 'com.github.shevek:parallelgzip:master-SNAPSHOT'
include 'com.github.shevek:parallelgzip:master-SNAPSHOT' include 'com.github.shevek:parallelgzip:master-SNAPSHOT'
// 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 { processResources {

View File

@ -8,6 +8,14 @@ loader_version=0.11.5
#Fabric api #Fabric api
fabric_version=0.35.1+1.17 fabric_version=0.35.1+1.17
#Cloth Config
cloth_version=5.0.34
#ModMenu
modmenu_version=2.0.2
lazydfu_version=0.1.2
# Mod Properties # Mod Properties
mod_version = 2.1.0 mod_version = 2.1.0
maven_group = net.szum123321 maven_group = net.szum123321

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; 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 net.szum123321.textile_backup.core.restore.AwaitThread;
import java.io.File; import java.io.File;
@ -30,16 +28,10 @@ import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
public class Statics { 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 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 final AtomicBoolean globalShutdownBackupFlag = new AtomicBoolean(true);
public static boolean disableWatchdog = false; public static boolean disableWatchdog = false;
public static AwaitThread restoreAwaitThread = null; public static AwaitThread restoreAwaitThread = null;

View File

@ -19,8 +19,9 @@
package net.szum123321.textile_backup; package net.szum123321.textile_backup;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; 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.api.ModInitializer;
import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback; import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
@ -34,40 +35,41 @@ import net.szum123321.textile_backup.commands.manage.WhitelistCommand;
import net.szum123321.textile_backup.commands.restore.KillRestoreCommand; import net.szum123321.textile_backup.commands.restore.KillRestoreCommand;
import net.szum123321.textile_backup.commands.manage.ListBackupsCommand; import net.szum123321.textile_backup.commands.manage.ListBackupsCommand;
import net.szum123321.textile_backup.commands.restore.RestoreBackupCommand; 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.ActionInitiator;
import net.szum123321.textile_backup.core.Utilities; import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.create.BackupContext; import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.core.create.BackupHelper; 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; import java.util.concurrent.Executors;
public class TextileBackup implements ModInitializer { 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 @Override
public void onInitialize() { public void onInitialize() {
Statics.LOGGER.info("Starting Textile Backup by Szum123321."); log.info("Starting Textile Backup by Szum123321");
Statics.CONFIG = ConfigManager.loadConfig(ConfigHandler.class); ConfigHelper.updateInstance(AutoConfig.register(ConfigPOJO.class, JanksonConfigSerializer::new));
Optional<String> errorMessage = Statics.CONFIG.sanitize();
if(errorMessage.isPresent()) {
Statics.LOGGER.fatal("TextileBackup config file has wrong settings!\n{}", errorMessage.get());
System.exit(1);
}
//TODO: finish writing wiki //TODO: finish writing wiki
if(Statics.CONFIG.format == ConfigHandler.ArchiveFormat.ZIP) { if(config.get().format == ConfigPOJO.ArchiveFormat.ZIP) {
Statics.tmpAvailable = Utilities.isTmpAvailable(); Statics.tmpAvailable = Utilities.isTmpAvailable();
if(!Statics.tmpAvailable) { if(!Statics.tmpAvailable) {
Statics.LOGGER.warn(""" log.warn("""
WARNING! It seems like the temporary folder is not accessible on this system! 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. 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"""); For more info please read: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems""");
} }
} }
if(Statics.CONFIG.backupInterval > 0) ServerTickEvents.END_SERVER_TICK.register(new BackupScheduler()::tick);
ServerTickEvents.END_SERVER_TICK.register(Statics.scheduler::tick);
//Restart Executor Service in singleplayer //Restart Executor Service in singleplayer
ServerLifecycleEvents.SERVER_STARTING.register(ignored -> { ServerLifecycleEvents.SERVER_STARTING.register(ignored -> {
@ -77,7 +79,7 @@ public class TextileBackup implements ModInitializer {
ServerLifecycleEvents.SERVER_STOPPED.register(server -> { ServerLifecycleEvents.SERVER_STOPPED.register(server -> {
Statics.executorService.shutdown(); Statics.executorService.shutdown();
if (Statics.CONFIG.shutdownBackup && Statics.globalShutdownBackupFlag.get()) { if (config.get().shutdownBackup && Statics.globalShutdownBackupFlag.get()) {
BackupHelper.create( BackupHelper.create(
BackupContext.Builder BackupContext.Builder
.newBackupContextBuilder() .newBackupContextBuilder()
@ -93,11 +95,11 @@ public class TextileBackup implements ModInitializer {
LiteralArgumentBuilder.<ServerCommandSource>literal("backup") LiteralArgumentBuilder.<ServerCommandSource>literal("backup")
.requires((ctx) -> { .requires((ctx) -> {
try { try {
return ((Statics.CONFIG.playerWhitelist.contains(ctx.getEntityOrThrow().getEntityName()) || return ((config.get().playerWhitelist.contains(ctx.getEntityOrThrow().getEntityName()) ||
ctx.hasPermissionLevel(Statics.CONFIG.permissionLevel)) && ctx.hasPermissionLevel(config.get().permissionLevel)) &&
!Statics.CONFIG.playerBlacklist.contains(ctx.getEntityOrThrow().getEntityName())) || !config.get().playerBlacklist.contains(ctx.getEntityOrThrow().getEntityName())) ||
(ctx.getMinecraftServer().isSinglePlayer() && (ctx.getMinecraftServer().isSinglePlayer() &&
Statics.CONFIG.alwaysSingleplayerAllowed); config.get().alwaysSingleplayerAllowed);
} catch (Exception ignored) { //Command was called from server console. } catch (Exception ignored) { //Command was called from server console.
return true; return true;
} }

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * 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 * 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 * 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/>. * 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.entity.player.PlayerEntity;
import net.minecraft.server.command.ServerCommandSource; 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.Logger;
import org.apache.logging.log4j.message.MessageFactory; import org.apache.logging.log4j.message.MessageFactory;
import org.apache.logging.log4j.message.ParameterizedMessageFactory; 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 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 boolean isDev = FabricLoader.getInstance().isDevelopmentEnvironment();
private final MessageFactory messageFactory; private final MessageFactory messageFactory;
@ -42,12 +43,19 @@ public class CustomLogger {
private final String prefix; private final String prefix;
private final MutableText prefixText; private final MutableText prefixText;
public CustomLogger(String name, String prefix) { /* public TextileLogger(String name, String prefix) {
this.messageFactory = ParameterizedMessageFactory.INSTANCE; this.messageFactory = ParameterizedMessageFactory.INSTANCE;
this.logger = LogManager.getLogger(name, messageFactory); this.logger = LogManager.getLogger(name, messageFactory);
this.prefix = "[" + prefix + "]" + " "; this.prefix = "[" + prefix + "]" + " ";
this.prefixText = new LiteralText(this.prefix).styled(style -> style.withColor(0x5B23DA)); 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() { public MutableText getPrefixText() {
return prefixText.shallowCopy(); 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

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

View File

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

View File

@ -3,14 +3,18 @@ package net.szum123321.textile_backup.commands.manage;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.CommandSyntaxException;
import io.github.cottonmc.cotton.config.ConfigManager;
import net.minecraft.command.argument.EntityArgumentType; import net.minecraft.command.argument.EntityArgumentType;
import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity; 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 { 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() { public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("blacklist") return CommandManager.literal("blacklist")
.then(CommandManager.literal("add") .then(CommandManager.literal("add")
@ -27,7 +31,7 @@ public class BlacklistCommand {
} }
private static int help(ServerCommandSource source) { 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; return 1;
} }
@ -37,12 +41,12 @@ public class BlacklistCommand {
builder.append("Currently on the blacklist are: "); builder.append("Currently on the blacklist are: ");
for(String name : Statics.CONFIG.playerBlacklist){ for(String name : config.get().playerBlacklist){
builder.append(name); builder.append(name);
builder.append(", "); builder.append(", ");
} }
Statics.LOGGER.sendInfo(source, builder.toString()); log.sendInfo(source, builder.toString());
return 1; return 1;
} }
@ -50,11 +54,11 @@ public class BlacklistCommand {
private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException { private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player"); ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(Statics.CONFIG.playerBlacklist.contains(player.getEntityName())) { if(config.get().playerBlacklist.contains(player.getEntityName())) {
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} is already blacklisted.", player.getEntityName()); log.sendInfo(ctx.getSource(), "Player: {} is already blacklisted.", player.getEntityName());
} else { } else {
Statics.CONFIG.playerBlacklist.add(player.getEntityName()); config.get().playerBlacklist.add(player.getEntityName());
ConfigManager.saveConfig(Statics.CONFIG); config.save();
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
@ -62,8 +66,8 @@ public class BlacklistCommand {
builder.append(player.getEntityName()); builder.append(player.getEntityName());
builder.append(" added to the blacklist"); builder.append(" added to the blacklist");
if(Statics.CONFIG.playerWhitelist.contains(player.getEntityName())){ if(config.get().playerWhitelist.contains(player.getEntityName())){
Statics.CONFIG.playerWhitelist.remove(player.getEntityName()); config.get().playerWhitelist.remove(player.getEntityName());
builder.append(" and removed form the whitelist"); builder.append(" and removed form the whitelist");
} }
@ -71,7 +75,7 @@ public class BlacklistCommand {
ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player); ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player);
Statics.LOGGER.sendInfo(ctx.getSource(), builder.toString()); log.sendInfo(ctx.getSource(), builder.toString());
} }
return 1; return 1;
@ -80,15 +84,15 @@ public class BlacklistCommand {
private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException { private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player"); ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(!Statics.CONFIG.playerBlacklist.contains(player.getEntityName())) { if(!config.get().playerBlacklist.contains(player.getEntityName())) {
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} newer was blacklisted.", player.getEntityName()); log.sendInfo(ctx.getSource(), "Player: {} newer was blacklisted.", player.getEntityName());
} else { } else {
Statics.CONFIG.playerBlacklist.remove(player.getEntityName()); config.get().playerBlacklist.remove(player.getEntityName());
ConfigManager.saveConfig(Statics.CONFIG); config.save();
ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player); ctx.getSource().getMinecraftServer().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; return 1;

View File

@ -24,6 +24,8 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource; 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.commands.CommandExceptions;
import net.szum123321.textile_backup.Statics; import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.commands.FileSuggestionProvider; import net.szum123321.textile_backup.commands.FileSuggestionProvider;
@ -36,6 +38,8 @@ import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
public class DeleteCommand { public class DeleteCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() { public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("delete") return CommandManager.literal("delete")
.then(CommandManager.argument("file", StringArgumentType.word()) .then(CommandManager.argument("file", StringArgumentType.word())
@ -63,20 +67,20 @@ public class DeleteCommand {
if(optionalFile.isPresent()) { if(optionalFile.isPresent()) {
if(Statics.untouchableFile.isEmpty() || !Statics.untouchableFile.get().equals(optionalFile.get())) { if(Statics.untouchableFile.isEmpty() || !Statics.untouchableFile.get().equals(optionalFile.get())) {
if(optionalFile.get().delete()) { 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) 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 { } else {
Statics.LOGGER.sendError(source, "Something went wrong while deleting file!"); log.sendError(source, "Something went wrong while deleting file!");
} }
} else { } else {
Statics.LOGGER.sendError(source, "Couldn't delete the file because it's being restored right now."); log.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.sendHint(source, "If you want to abort restoration then use: /backup killR");
} }
} else { } else {
Statics.LOGGER.sendError(source, "Couldn't find file by this name."); log.sendError(source, "Couldn't find file by this name.");
Statics.LOGGER.sendHint(source, "Maybe try /backup list"); log.sendHint(source, "Maybe try /backup list");
} }
return 0; return 0;

View File

@ -21,12 +21,15 @@ package net.szum123321.textile_backup.commands.manage;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource; 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 net.szum123321.textile_backup.core.restore.RestoreHelper;
import java.util.*; import java.util.*;
public class ListBackupsCommand { public class ListBackupsCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() { public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("list") return CommandManager.literal("list")
.executes(ctx -> { StringBuilder builder = new StringBuilder(); .executes(ctx -> { StringBuilder builder = new StringBuilder();
@ -50,7 +53,7 @@ public class ListBackupsCommand {
} }
} }
Statics.LOGGER.sendInfo(ctx.getSource(), builder.toString()); log.sendInfo(ctx.getSource(), builder.toString());
return 1; return 1;
}); });

View File

@ -3,14 +3,18 @@ package net.szum123321.textile_backup.commands.manage;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.CommandSyntaxException;
import io.github.cottonmc.cotton.config.ConfigManager;
import net.minecraft.command.argument.EntityArgumentType; import net.minecraft.command.argument.EntityArgumentType;
import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity; 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 { 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(){ public static LiteralArgumentBuilder<ServerCommandSource> register(){
return CommandManager.literal("whitelist") return CommandManager.literal("whitelist")
.then(CommandManager.literal("add") .then(CommandManager.literal("add")
@ -27,7 +31,7 @@ public class WhitelistCommand {
} }
private static int help(ServerCommandSource source){ 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; return 1;
} }
@ -37,12 +41,12 @@ public class WhitelistCommand {
builder.append("Currently on the whitelist are: "); builder.append("Currently on the whitelist are: ");
for(String name : Statics.CONFIG.playerWhitelist){ for(String name : config.get().playerWhitelist){
builder.append(name); builder.append(name);
builder.append(", "); builder.append(", ");
} }
Statics.LOGGER.sendInfo(source, builder.toString()); log.sendInfo(source, builder.toString());
return 1; return 1;
} }
@ -50,11 +54,11 @@ public class WhitelistCommand {
private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException { private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player"); ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(Statics.CONFIG.playerWhitelist.contains(player.getEntityName())) { if(config.get().playerWhitelist.contains(player.getEntityName())) {
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} is already whitelisted.", player.getEntityName()); log.sendInfo(ctx.getSource(), "Player: {} is already whitelisted.", player.getEntityName());
} else { } else {
Statics.CONFIG.playerWhitelist.add(player.getEntityName()); config.get().playerWhitelist.add(player.getEntityName());
ConfigManager.saveConfig(Statics.CONFIG); config.save();
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
@ -62,8 +66,8 @@ public class WhitelistCommand {
builder.append(player.getEntityName()); builder.append(player.getEntityName());
builder.append(" added to the whitelist"); builder.append(" added to the whitelist");
if(Statics.CONFIG.playerBlacklist.contains(player.getEntityName())){ if(config.get().playerBlacklist.contains(player.getEntityName())){
Statics.CONFIG.playerBlacklist.remove(player.getEntityName()); config.get().playerBlacklist.remove(player.getEntityName());
builder.append(" and removed form the blacklist"); builder.append(" and removed form the blacklist");
} }
@ -71,7 +75,7 @@ public class WhitelistCommand {
ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player); ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player);
Statics.LOGGER.sendInfo(ctx.getSource(), builder.toString()); log.sendInfo(ctx.getSource(), builder.toString());
} }
return 1; return 1;
@ -80,15 +84,15 @@ public class WhitelistCommand {
private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException { private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player"); ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(!Statics.CONFIG.playerWhitelist.contains(player.getEntityName())) { if(!config.get().playerWhitelist.contains(player.getEntityName())) {
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} newer was whitelisted.", player.getEntityName()); log.sendInfo(ctx.getSource(), "Player: {} newer was whitelisted.", player.getEntityName());
} else { } else {
Statics.CONFIG.playerWhitelist.remove(player.getEntityName()); config.get().playerWhitelist.remove(player.getEntityName());
ConfigManager.saveConfig(Statics.CONFIG); config.save();
ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player); ctx.getSource().getMinecraftServer().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; 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.CommandManager;
import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Statics; import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import java.util.Optional; import java.util.Optional;
public class KillRestoreCommand { public class KillRestoreCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() { public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("killR") return CommandManager.literal("killR")
.executes(ctx -> { .executes(ctx -> {
@ -35,16 +38,16 @@ public class KillRestoreCommand {
Statics.globalShutdownBackupFlag.set(true); Statics.globalShutdownBackupFlag.set(true);
Statics.untouchableFile = Optional.empty(); 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() : "Player: " + ctx.getSource().getName() :
"SERVER" "SERVER"
); );
if(ctx.getSource().getEntity() instanceof PlayerEntity) if(ctx.getSource().getEntity() instanceof PlayerEntity)
Statics.LOGGER.sendInfo(ctx.getSource(), "Backup restoration successfully stopped."); log.sendInfo(ctx.getSource(), "Backup restoration successfully stopped.");
} else { } else {
Statics.LOGGER.sendInfo(ctx.getSource(), "Failed to stop backup restoration"); log.sendInfo(ctx.getSource(), "Failed to stop backup restoration");
} }
return 1; 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.CommandManager;
import net.minecraft.server.command.ServerCommandSource; 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.commands.CommandExceptions;
import net.szum123321.textile_backup.Statics; import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.commands.FileSuggestionProvider; import net.szum123321.textile_backup.commands.FileSuggestionProvider;
@ -36,6 +38,8 @@ import java.time.format.DateTimeParseException;
import java.util.Optional; import java.util.Optional;
public class RestoreBackupCommand { public class RestoreBackupCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() { public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("restore") return CommandManager.literal("restore")
.then(CommandManager.argument("file", StringArgumentType.word()) .then(CommandManager.argument("file", StringArgumentType.word())
@ -57,9 +61,9 @@ public class RestoreBackupCommand {
).executes(context -> { ).executes(context -> {
ServerCommandSource source = context.getSource(); ServerCommandSource source = context.getSource();
Statics.LOGGER.sendInfo(source, "To restore given backup you have to provide exact creation time in format:"); log.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]"); log.sendInfo(source, "[YEAR]-[MONTH]-[DAY]_[HOUR].[MINUTE].[SECOND]");
Statics.LOGGER.sendInfo(source, "Example: /backup restore 2020-08-05_10.58.33"); log.sendInfo(source, "Example: /backup restore 2020-08-05_10.58.33");
return 1; return 1;
}); });
@ -78,9 +82,9 @@ public class RestoreBackupCommand {
Optional<RestoreHelper.RestoreableFile> backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getMinecraftServer()); Optional<RestoreHelper.RestoreableFile> backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getMinecraftServer());
if(backupFile.isPresent()) { 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 { } 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; return 0;
} }
@ -97,7 +101,7 @@ public class RestoreBackupCommand {
return 1; return 1;
} else { } else {
Statics.LOGGER.sendInfo(source, "Someone has already started another restoration."); log.sendInfo(source, "Someone has already started another restoration.");
return 0; 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,174 @@
/*
* 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("""
Format 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""")
public String dateTimeFormat = "yyyy.MM.dd_HH-mm-ss";
@Comment("Should every world have its own backup folder?")
@ConfigEntry.Gui.Excluded
public boolean perWorldBackup = true;
@Comment("A path to the backup folder")
public String path = "backup/";
@Comment("""
This setting allows you to exclude files form being backed-up.
Be very careful when setting it, as it is easy corrupt your world!""")
public List<String> fileBlacklist = new ArrayList<>();
@Comment("Should backups be deleted after being restored?")
public boolean deleteOldBackupAfterRestore = true;
@Comment("Maximum number of backups to keep.\nIf set to 0 then no backup will be deleted based their amount")
public int backupsToKeep = 10;
@Comment("""
Maximum age of backups to keep in seconds.
If set to 0 then backups will not be deleted based their age""")
public long maxAge = 0;
@Comment("""
Maximum size of backup folder in kilo bytes (1024).
If set to 0 then backups will not be deleted""")
public int maxSize = 0;
@Comment("""
Time 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("Should backups be done even if there are no players?")
@ConfigEntry.Category("Create")
public boolean doBackupsOnEmptyServer = false;
@Comment("Should backup be made on server shutdown?")
@ConfigEntry.Category("Create")
public boolean shutdownBackup = true;
@Comment("Should world be backed up before restoring a backup?")
@ConfigEntry.Category("Create")
public boolean backupOldWorlds = true;
@Comment("Compression level 0 - 9 Only affects zip compression.")
@ConfigEntry.BoundedDiscrete(max = 9)
@ConfigEntry.Category("Create")
public int compression = 7;
@Comment("""
Limit how many cores can be used for compression.
0 means that all available cores will be used""")
@ConfigEntry.Category("Create")
public int compressionCoreCountLimit = 0;
@Comment(value = """
Available 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.Category("Create")
@ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
public ArchiveFormat format = ArchiveFormat.ZIP;
@Comment("Minimal permission level required to run commands")
@ConfigEntry.Category("Manage")
public int permissionLevel = 4;
@Comment("""
Player on singleplayer is always allowed to run command.
Warning! On lan party everyone will be allowed to run it.""")
@ConfigEntry.Category("Manage")
public boolean alwaysSingleplayerAllowed = true;
@Comment("Players allowed to run backup commands without sufficient permission level")
@ConfigEntry.Category("Manage")
public Set<String> playerWhitelist = new HashSet<>();
@Comment("Players banned from running backup commands besides their sufficient permission level")
@ConfigEntry.Category("Manage")
public Set<String> playerBlacklist = new HashSet<>();
@Comment("Delay in seconds between typing-in /backup restore and it actually starting")
@ConfigEntry.Category("Restore")
public int restoreDelay = 30;
@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

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

View File

@ -25,6 +25,9 @@ import net.minecraft.text.MutableText;
import net.minecraft.util.Formatting; import net.minecraft.util.Formatting;
import net.minecraft.util.Util; import net.minecraft.util.Util;
import net.szum123321.textile_backup.Statics; 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.ActionInitiator;
import net.szum123321.textile_backup.core.Utilities; import net.szum123321.textile_backup.core.Utilities;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
@ -37,6 +40,9 @@ import java.util.Comparator;
import java.util.UUID; import java.util.UUID;
public class BackupHelper { 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) { public static Runnable create(BackupContext ctx) {
notifyPlayers(ctx); notifyPlayers(ctx);
@ -55,17 +61,17 @@ public class BackupHelper {
builder.append(" on: "); builder.append(" on: ");
builder.append(Utilities.getDateTimeFormatter().format(LocalDateTime.now())); builder.append(Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
Statics.LOGGER.info(builder.toString()); log.info(builder.toString());
if (ctx.shouldSave()) { if (ctx.shouldSave()) {
Statics.LOGGER.sendInfoAL(ctx, "Saving server..."); log.sendInfoAL(ctx, "Saving server...");
ctx.getServer().getPlayerManager().saveAllPlayerData(); ctx.getServer().getPlayerManager().saveAllPlayerData();
try { try {
ctx.getServer().save(false, true, true); ctx.getServer().save(false, true, true);
} catch (Exception e) { } 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 +79,7 @@ public class BackupHelper {
} }
private static void notifyPlayers(BackupContext ctx) { 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)); message.append(new LiteralText("Warning! Server backup will begin shortly. You may experience some lag.").formatted(Formatting.WHITE));
UUID uuid; UUID uuid;
@ -94,30 +100,30 @@ public class BackupHelper {
int deletedFiles = 0; int deletedFiles = 0;
if (root.isDirectory() && root.exists() && root.listFiles() != null) { 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(); final LocalDateTime now = LocalDateTime.now();
deletedFiles += 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(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)) .map(f -> deleteFile(f, ctx))
.filter(b -> b).count(); //a bit awkward .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()) deletedFiles += Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup) .filter(Utilities::isValidBackup)
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime((File) f).get()).reversed()) .sorted(Comparator.comparing(f -> Utilities.getFileCreationTime((File) f).get()).reversed())
.skip(Statics.CONFIG.backupsToKeep) .skip(config.get().backupsToKeep)
.map(f -> deleteFile(f, ctx)) .map(f -> deleteFile(f, ctx))
.filter(b -> b).count(); .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()) deletedFiles += Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup) .filter(Utilities::isValidBackup)
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get())) .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)) .map(f -> deleteFile(f, ctx))
.filter(b -> b).count(); .filter(b -> b).count();
} }
@ -129,10 +135,10 @@ public class BackupHelper {
private static boolean deleteFile(File f, ServerCommandSource ctx) { private static boolean deleteFile(File f, ServerCommandSource ctx) {
if(Statics.untouchableFile.isEmpty()|| !Statics.untouchableFile.get().equals(f)) { if(Statics.untouchableFile.isEmpty()|| !Statics.untouchableFile.get().equals(f)) {
if(f.delete()) { if(f.delete()) {
Statics.LOGGER.sendInfoAL(ctx, "Deleting: {}", f.getName()); log.sendInfoAL(ctx, "Deleting: {}", f.getName());
return true; return true;
} else { } 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.minecraft.server.MinecraftServer;
import net.szum123321.textile_backup.Statics; import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.ActionInitiator; import net.szum123321.textile_backup.core.ActionInitiator;
import java.time.Instant; import java.time.Instant;
public class BackupScheduler { public class BackupScheduler {
private final static ConfigHelper config = ConfigHelper.INSTANCE;
private boolean scheduled; private boolean scheduled;
private long nextBackup; private long nextBackup;
@ -34,9 +37,10 @@ public class BackupScheduler {
} }
public void tick(MinecraftServer server) { public void tick(MinecraftServer server) {
if(config.get().backupInterval < 1) return;
long now = Instant.now().getEpochSecond(); long now = Instant.now().getEpochSecond();
if(Statics.CONFIG.doBackupsOnEmptyServer || server.getPlayerManager().getCurrentPlayerCount() > 0) { if(config.get().doBackupsOnEmptyServer || server.getPlayerManager().getCurrentPlayerCount() > 0) {
if(scheduled) { if(scheduled) {
if(nextBackup <= now) { if(nextBackup <= now) {
Statics.executorService.submit( Statics.executorService.submit(
@ -50,13 +54,13 @@ public class BackupScheduler {
) )
); );
nextBackup = now + Statics.CONFIG.backupInterval; nextBackup = now + config.get().backupInterval;
} }
} else { } else {
nextBackup = now + Statics.CONFIG.backupInterval; nextBackup = now + config.get().backupInterval;
scheduled = true; scheduled = true;
} }
} else if(!Statics.CONFIG.doBackupsOnEmptyServer && server.getPlayerManager().getCurrentPlayerCount() == 0) { } else if(!config.get().doBackupsOnEmptyServer && server.getPlayerManager().getCurrentPlayerCount() == 0) {
if(scheduled && nextBackup <= now) { if(scheduled && nextBackup <= now) {
Statics.executorService.submit( Statics.executorService.submit(
BackupHelper.create( BackupHelper.create(

View File

@ -19,6 +19,9 @@
package net.szum123321.textile_backup.core.create; package net.szum123321.textile_backup.core.create;
import net.szum123321.textile_backup.Statics; 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.ActionInitiator;
import net.szum123321.textile_backup.core.create.compressors.*; import net.szum123321.textile_backup.core.create.compressors.*;
import net.szum123321.textile_backup.core.Utilities; import net.szum123321.textile_backup.core.Utilities;
@ -33,6 +36,9 @@ import java.io.OutputStream;
import java.time.LocalDateTime; import java.time.LocalDateTime;
public class MakeBackupRunnable implements Runnable { 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; private final BackupContext context;
public MakeBackupRunnable(BackupContext context){ public MakeBackupRunnable(BackupContext context){
@ -45,11 +51,11 @@ public class MakeBackupRunnable implements Runnable {
Utilities.disableWorldSaving(context.getServer()); Utilities.disableWorldSaving(context.getServer());
Statics.disableWatchdog = true; Statics.disableWatchdog = true;
Statics.LOGGER.sendInfoAL(context, "Starting backup"); log.sendInfoAL(context, "Starting backup");
File world = Utilities.getWorldFolder(context.getServer()); File world = Utilities.getWorldFolder(context.getServer());
Statics.LOGGER.trace("Minecraft world is: {}", world); log.trace("Minecraft world is: {}", world);
File outFile = Utilities File outFile = Utilities
.getBackupRootPath(Utilities.getLevelName(context.getServer())) .getBackupRootPath(Utilities.getLevelName(context.getServer()))
@ -57,32 +63,32 @@ public class MakeBackupRunnable implements Runnable {
.resolve(getFileName()) .resolve(getFileName())
.toFile(); .toFile();
Statics.LOGGER.trace("Outfile is: {}", outFile); log.trace("Outfile is: {}", outFile);
outFile.getParentFile().mkdirs(); outFile.getParentFile().mkdirs();
try { try {
outFile.createNewFile(); outFile.createNewFile();
} catch (IOException e) { } 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) 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; return;
} }
int coreCount; int coreCount;
if(Statics.CONFIG.compressionCoreCountLimit <= 0) { if(config.get().compressionCoreCountLimit <= 0) {
coreCount = Runtime.getRuntime().availableProcessors(); coreCount = Runtime.getRuntime().availableProcessors();
} else { } 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 -> { case ZIP -> {
if (Statics.tmpAvailable && coreCount > 1) if (Statics.tmpAvailable && coreCount > 1)
ParallelZipCompressor.getInstance().createArchive(world, outFile, context, coreCount); ParallelZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
@ -98,16 +104,16 @@ public class MakeBackupRunnable implements Runnable {
}.createArchive(world, outFile, context, coreCount); }.createArchive(world, outFile, context, coreCount);
case TAR -> new AbstractTarArchiver().createArchive(world, outFile, context, coreCount); case TAR -> new AbstractTarArchiver().createArchive(world, outFile, context, coreCount);
default -> { 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) 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); ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
} }
} }
BackupHelper.executeFileLimit(context.getCommandSource(), Utilities.getLevelName(context.getServer())); BackupHelper.executeFileLimit(context.getCommandSource(), Utilities.getLevelName(context.getServer()));
Statics.LOGGER.sendInfoAL(context, "Done!"); log.sendInfoAL(context, "Done!");
} finally { } finally {
Utilities.enableWorldSaving(context.getServer()); Utilities.enableWorldSaving(context.getServer());
Statics.disableWatchdog = false; Statics.disableWatchdog = false;
@ -119,6 +125,6 @@ public class MakeBackupRunnable implements Runnable {
return Utilities.getDateTimeFormatter().format(now) + return Utilities.getDateTimeFormatter().format(now) +
(context.getComment() != null ? "#" + context.getComment().replace("#", "") : "") + (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; 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.ActionInitiator;
import net.szum123321.textile_backup.core.NoSpaceLeftOnDeviceException; import net.szum123321.textile_backup.core.NoSpaceLeftOnDeviceException;
import net.szum123321.textile_backup.core.Utilities; import net.szum123321.textile_backup.core.Utilities;
@ -32,6 +33,8 @@ import java.time.Instant;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
public abstract class AbstractCompressor { 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) { public void createArchive(File inputFile, File outputFile, BackupContext ctx, int coreLimit) {
Instant start = Instant.now(); Instant start = Instant.now();
@ -48,35 +51,35 @@ public abstract class AbstractCompressor {
//hopefully one broken file won't spoil the whole archive //hopefully one broken file won't spoil the whole archive
addEntry(file, inputFile.toPath().relativize(file.toPath()).toString(), arc); addEntry(file, inputFile.toPath().relativize(file.toPath()).toString(), arc);
} catch (IOException e) { } 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) 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); finish(arc);
} catch(NoSpaceLeftOnDeviceException e) { } catch(NoSpaceLeftOnDeviceException e) {
Statics.LOGGER.error("CRITICAL ERROR OCCURRED!"); log.error("CRITICAL ERROR OCCURRED!");
Statics.LOGGER.error("The backup is corrupted."); log.error("The backup is corrupted.");
Statics.LOGGER.error("Don't panic! This is a known issue!"); log.error("Don't panic! This is a known issue!");
Statics.LOGGER.error("For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems"); log.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("In case this isn't it here's also the exception itself!", e);
if(ctx.getInitiator() == ActionInitiator.Player) { if(ctx.getInitiator() == ActionInitiator.Player) {
Statics.LOGGER.sendError(ctx, "Backup failed. The file is corrupt."); log.sendError(ctx, "Backup failed. The file is corrupt.");
Statics.LOGGER.error("For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems"); log.error("For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems");
} }
} catch (IOException | InterruptedException | ExecutionException e) { } catch (IOException | InterruptedException | ExecutionException e) {
Statics.LOGGER.error("An exception occurred!", e); log.error("An exception occurred!", e);
} catch (Exception e) { } catch (Exception e) {
if(ctx.getInitiator() == ActionInitiator.Player) if(ctx.getInitiator() == ActionInitiator.Player)
Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!"); log.sendError(ctx, "Something went wrong while compressing files!");
} }
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; 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; 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.NoSpaceLeftOnDeviceException;
import net.szum123321.textile_backup.core.create.BackupContext; import net.szum123321.textile_backup.core.create.BackupContext;
import org.apache.commons.compress.archivers.zip.*; import org.apache.commons.compress.archivers.zip.*;
@ -36,6 +37,8 @@ import java.util.zip.ZipEntry;
https://stackoverflow.com/users/2987755/dkb https://stackoverflow.com/users/2987755/dkb
*/ */
public class ParallelZipCompressor extends ZipCompressor { 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 //These fields are used to discriminate against the issue #51
private final static SimpleStackTraceElement[] STACKTRACE = { private final static SimpleStackTraceElement[] STACKTRACE = {
new SimpleStackTraceElement("sun.nio.ch.FileDispatcherImpl", "write0", true), new SimpleStackTraceElement("sun.nio.ch.FileDispatcherImpl", "write0", true),
@ -130,7 +133,7 @@ public class ParallelZipCompressor extends ZipCompressor {
try { try {
return new FileInputStream(sourceFile); return new FileInputStream(sourceFile);
} catch (IOException e) { } 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; return null;

View File

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

View File

@ -18,7 +18,8 @@
package net.szum123321.textile_backup.core.restore; 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; 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 This thread waits some amount of time and then starts a new, independent thread
*/ */
public class AwaitThread extends 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 static AtomicInteger threadCounter = new AtomicInteger(0);
private final int delay; private final int delay;
@ -40,13 +42,13 @@ public class AwaitThread extends Thread {
@Override @Override
public void run() { 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 ♪ // 𝄞 This is final count down! Tu ruru Tu, Tu Ru Tu Tu ♪
try { try {
Thread.sleep(delay * 1000L); Thread.sleep(delay * 1000L);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Statics.LOGGER.info("Backup restoration cancelled."); log.info("Backup restoration cancelled.");
return; return;
} }

View File

@ -18,7 +18,10 @@
package net.szum123321.textile_backup.core.restore; 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.ActionInitiator;
import net.szum123321.textile_backup.core.LivingServer; import net.szum123321.textile_backup.core.LivingServer;
import net.szum123321.textile_backup.Statics; import net.szum123321.textile_backup.Statics;
@ -31,6 +34,9 @@ import net.szum123321.textile_backup.core.restore.decompressors.ZipDecompressor;
import java.io.File; import java.io.File;
public class RestoreBackupRunnable implements Runnable { 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; private final RestoreContext ctx;
public RestoreBackupRunnable(RestoreContext ctx) { public RestoreBackupRunnable(RestoreContext ctx) {
@ -41,12 +47,12 @@ public class RestoreBackupRunnable implements Runnable {
public void run() { public void run() {
Statics.globalShutdownBackupFlag.set(false); Statics.globalShutdownBackupFlag.set(false);
Statics.LOGGER.info("Shutting down server..."); log.info("Shutting down server...");
ctx.getServer().stop(false); ctx.getServer().stop(false);
awaitServerShutdown(); awaitServerShutdown();
if(Statics.CONFIG.backupOldWorlds) { if(config.get().backupOldWorlds) {
BackupHelper.create( BackupHelper.create(
BackupContext.Builder BackupContext.Builder
.newBackupContextBuilder() .newBackupContextBuilder()
@ -59,31 +65,35 @@ public class RestoreBackupRunnable implements Runnable {
File worldFile = Utilities.getWorldFolder(ctx.getServer()); File worldFile = Utilities.getWorldFolder(ctx.getServer());
Statics.LOGGER.info("Deleting old world..."); log.info("Deleting old world...");
if(!deleteDirectory(worldFile)) if(!deleteDirectory(worldFile))
Statics.LOGGER.error("Something went wrong while deleting old world!"); log.error("Something went wrong while deleting old world!");
worldFile.mkdirs(); 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); ZipDecompressor.decompress(ctx.getFile().getFile(), worldFile);
else else
GenericTarDecompressor.decompress(ctx.getFile().getFile(), worldFile); GenericTarDecompressor.decompress(ctx.getFile().getFile(), worldFile);
if(Statics.CONFIG.deleteOldBackupAfterRestore) { if(config.get().deleteOldBackupAfterRestore) {
Statics.LOGGER.info("Deleting old backup"); log.info("Deleting old backup");
if(!ctx.getFile().getFile().delete()) if(!ctx.getFile().getFile().delete())
Statics.LOGGER.info("Something went wrong while deleting old backup"); log.info("Something went wrong while deleting old backup");
} }
//in case we're playing on client //in case we're playing on client
Statics.globalShutdownBackupFlag.set(true); 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() { private void awaitServerShutdown() {
@ -91,7 +101,7 @@ public class RestoreBackupRunnable implements Runnable {
try { try {
Thread.sleep(100); Thread.sleep(100);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Statics.LOGGER.error("Exception occurred!", e); log.error("Exception occurred!", e);
} }
} }
} }

View File

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

View File

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

View File

@ -18,7 +18,8 @@
package net.szum123321.textile_backup.core.restore.decompressors; 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 net.szum123321.textile_backup.core.Utilities;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
@ -30,6 +31,8 @@ import java.time.Duration;
import java.time.Instant; import java.time.Instant;
public class ZipDecompressor { public class ZipDecompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static void decompress(File inputFile, File target) { public static void decompress(File inputFile, File target) {
Instant start = Instant.now(); Instant start = Instant.now();
@ -40,7 +43,7 @@ public class ZipDecompressor {
while ((entry = zipInputStream.getNextZipEntry()) != null) { while ((entry = zipInputStream.getNextZipEntry()) != null) {
if(!zipInputStream.canReadEntryData(entry)){ 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; continue;
} }
@ -52,21 +55,21 @@ public class ZipDecompressor {
File parent = file.getParentFile(); File parent = file.getParentFile();
if (!parent.isDirectory() && !parent.mkdirs()) { if (!parent.isDirectory() && !parent.mkdirs()) {
Statics.LOGGER.error("Failed to create {}", parent); log.error("Failed to create {}", parent);
} else { } else {
try (OutputStream outputStream = Files.newOutputStream(file.toPath()); try (OutputStream outputStream = Files.newOutputStream(file.toPath());
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) { BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
IOUtils.copy(zipInputStream, bufferedOutputStream); IOUtils.copy(zipInputStream, bufferedOutputStream);
} catch (IOException e) { } 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) { } 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

@ -0,0 +1,49 @@
{
"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": "AAAAAA",
"text.autoconfig.textile_backup.option.restoreDelay": "Restore Delay",
"text.autoconfig.textile_backup.option.doBackupsOnEmptyServer": "Do 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": "Blacklised 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.maxSize": "Max size of backup folder",
"text.autoconfig.textile_backup.option.compression": "Compression level (Zip only)",
"text.autoconfig.textile_backup.option.compressionCoreCountLimit": "Max number of cores used for compression",
"text.autoconfig.textile_backup.option.format": "Archive and compression format",
"text.autoconfig.textile_backup.option.permissionLevel": "Min permission level required to run any command",
"text.autoconfig.textile_backup.option.alwaysSingleplayerAllowed": "Always allow on sigle-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"
}

View File

@ -27,6 +27,9 @@
"entrypoints": { "entrypoints": {
"main": [ "main": [
"net.szum123321.textile_backup.TextileBackup" "net.szum123321.textile_backup.TextileBackup"
],
"modmenu": [
"net.szum123321.textile_backup.client.ModMenuEntry"
] ]
}, },
"mixins": [ "mixins": [