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

View File

@ -8,6 +8,14 @@ loader_version=0.11.5
#Fabric api
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_version = 2.1.0
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;
import net.szum123321.textile_backup.core.CustomLogger;
import net.szum123321.textile_backup.core.create.BackupScheduler;
import net.szum123321.textile_backup.core.restore.AwaitThread;
import java.io.File;
@ -30,16 +28,10 @@ import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
public class Statics {
public static final String MOD_ID = "textile_backup";
public static final String MOD_NAME = "Textile Backup";
public static final CustomLogger LOGGER = new CustomLogger(MOD_ID, MOD_NAME);
public static ConfigHandler CONFIG;
public static final BackupScheduler scheduler = new BackupScheduler();
public static ExecutorService executorService = Executors.newSingleThreadExecutor();
public final static DateTimeFormatter defaultDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss");
public static ExecutorService executorService = Executors.newSingleThreadExecutor();
public static final AtomicBoolean globalShutdownBackupFlag = new AtomicBoolean(true);
public static boolean disableWatchdog = false;
public static AwaitThread restoreAwaitThread = null;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,32 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2021 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.config;
import me.shedaniel.autoconfig.ConfigHolder;
public class ConfigHelper {
public static final ConfigHelper INSTANCE = new ConfigHelper();
private ConfigHolder<ConfigPOJO> configHolder;
public static void updateInstance(ConfigHolder<ConfigPOJO> ch) { INSTANCE.configHolder = ch; }
public ConfigPOJO get() { return configHolder.get(); }
public void save() { configHolder.save(); }
}

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,8 @@
package net.szum123321.textile_backup.core.create.compressors;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.NoSpaceLeftOnDeviceException;
import net.szum123321.textile_backup.core.Utilities;
@ -32,6 +33,8 @@ import java.time.Instant;
import java.util.concurrent.ExecutionException;
public abstract class AbstractCompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public void createArchive(File inputFile, File outputFile, BackupContext ctx, int coreLimit) {
Instant start = Instant.now();
@ -48,35 +51,35 @@ public abstract class AbstractCompressor {
//hopefully one broken file won't spoil the whole archive
addEntry(file, inputFile.toPath().relativize(file.toPath()).toString(), arc);
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred while trying to compress: {}", inputFile.toPath().relativize(file.toPath()).toString(), e);
log.error("An exception occurred while trying to compress: {}", inputFile.toPath().relativize(file.toPath()).toString(), e);
if (ctx.getInitiator() == ActionInitiator.Player)
Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!");
log.sendError(ctx, "Something went wrong while compressing files!");
}
});
finish(arc);
} catch(NoSpaceLeftOnDeviceException e) {
Statics.LOGGER.error("CRITICAL ERROR OCCURRED!");
Statics.LOGGER.error("The backup is corrupted.");
Statics.LOGGER.error("Don't panic! This is a known issue!");
Statics.LOGGER.error("For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems");
Statics.LOGGER.error("In case this isn't it here's also the exception itself!", e);
log.error("CRITICAL ERROR OCCURRED!");
log.error("The backup is corrupted.");
log.error("Don't panic! This is a known issue!");
log.error("For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems");
log.error("In case this isn't it here's also the exception itself!", e);
if(ctx.getInitiator() == ActionInitiator.Player) {
Statics.LOGGER.sendError(ctx, "Backup failed. The file is corrupt.");
Statics.LOGGER.error("For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems");
log.sendError(ctx, "Backup failed. The file is corrupt.");
log.error("For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems");
}
} catch (IOException | InterruptedException | ExecutionException e) {
Statics.LOGGER.error("An exception occurred!", e);
log.error("An exception occurred!", e);
} catch (Exception e) {
if(ctx.getInitiator() == ActionInitiator.Player)
Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!");
log.sendError(ctx, "Something went wrong while compressing files!");
}
close();
Statics.LOGGER.sendInfoAL(ctx, "Compression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
log.sendInfoAL(ctx, "Compression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
}
protected abstract OutputStream createArchiveOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": {
"main": [
"net.szum123321.textile_backup.TextileBackup"
],
"modmenu": [
"net.szum123321.textile_backup.client.ModMenuEntry"
]
},
"mixins": [