-per world backup
-configurable datetime format
-configurable permission level
-player black and whitelist
Forge-1.14.4
Szum123321 2020-03-23 17:42:17 +01:00
parent b30e35a308
commit 312cb57692
11 changed files with 283 additions and 26 deletions

View File

@ -2,13 +2,13 @@
org.gradle.jvmargs=-Xmx1G org.gradle.jvmargs=-Xmx1G
minecraft_version=20w12a minecraft_version=20w12a
yarn_mappings=20w12a+build.18 yarn_mappings=20w12a+build.19
loader_version=0.7.8+build.189 loader_version=0.7.8+build.189
#Fabric api #Fabric api
fabric_version=0.5.5+build.311-1.16 fabric_version=0.5.5+build.311-1.16
# Mod Properties # Mod Properties
mod_version = 1.0.4-1.16 mod_version = 1.1.0-1.16
maven_group = net.szum123321 maven_group = net.szum123321
archives_base_name = textile_backup archives_base_name = textile_backup

View File

@ -21,6 +21,9 @@ package net.szum123321.textile_backup;
import blue.endless.jankson.Comment; import blue.endless.jankson.Comment;
import io.github.cottonmc.cotton.config.annotations.ConfigFile; import io.github.cottonmc.cotton.config.annotations.ConfigFile;
import java.util.HashSet;
import java.util.Set;
@ConfigFile(name = TextileBackup.MOD_ID) @ConfigFile(name = TextileBackup.MOD_ID)
public class ConfigHandler { public class ConfigHandler {
@Comment("\nTime between backups in seconds\n") @Comment("\nTime between backups in seconds\n")
@ -35,10 +38,13 @@ public class ConfigHandler {
@Comment("\nA path to backup folder\n") @Comment("\nA path to backup folder\n")
public String path = "backup/"; public String path = "backup/";
@Comment("\nMaximum number of backups to keep. if 0 then no backup will be deleted\n") @Comment("\nShould every world has its won backup folder?\n")
public boolean perWorldBackup = false;
@Comment("\nMaximum number of backups to keep. if 0 then no backup will be deleted based on its amount\n")
public int backupsToKeep = 10; public int backupsToKeep = 10;
@Comment("\nMaximum age of backups to keep in seconds.\n if 0 then backups will not be deleted based on age \n") @Comment("\nMaximum age of backups to keep in seconds.\n if 0 then backups will not be deleted based on its age \n")
public int maxAge = 0; public int maxAge = 0;
@Comment("\nMaximum size of backup folder in kilo bytes. \n") @Comment("\nMaximum size of backup folder in kilo bytes. \n")
@ -49,4 +55,16 @@ public class ConfigHandler {
@Comment("\nPrint info to game out\n") @Comment("\nPrint info to game out\n")
public boolean log = true; public boolean log = true;
@Comment("\nMinimal permission level required to run commands\n")
public int permissionLevel = 4;
@Comment("\nPlayers allowed to run backup commands without sufficient permission level\n")
public Set<String> whitelist = new HashSet<>();
@Comment("\nPlayers banned from running backup commands besides their sufficient permission level\n")
public Set<String> blacklist = new HashSet<>();
@Comment("\nFormat of date&time used to name backup files.\n")
public String dateTimeFormat = "dd.MM.yyyy_HH-mm-ss";
} }

View File

@ -25,8 +25,10 @@ import io.github.cottonmc.cotton.logging.ModLogger;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.registry.CommandRegistry; import net.fabricmc.fabric.api.registry.CommandRegistry;
import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.commands.BlacklistCommand;
import net.szum123321.textile_backup.commands.CleanupCommand; import net.szum123321.textile_backup.commands.CleanupCommand;
import net.szum123321.textile_backup.commands.StartBackupCommand; import net.szum123321.textile_backup.commands.StartBackupCommand;
import net.szum123321.textile_backup.commands.WhitelistCommand;
public class TextileBackup implements ModInitializer { public class TextileBackup implements ModInitializer {
public static final String MOD_ID = "textile_backup"; public static final String MOD_ID = "textile_backup";
@ -41,7 +43,6 @@ public class TextileBackup implements ModInitializer {
logger.info("Loading TextileBackup by Szum123321"); logger.info("Loading TextileBackup by Szum123321");
config = ConfigManager.loadConfig(ConfigHandler.class); config = ConfigManager.loadConfig(ConfigHandler.class);
config.backupInterval *= 1000;
registerCommands(); registerCommands();
} }
@ -49,8 +50,10 @@ public class TextileBackup implements ModInitializer {
private void registerCommands(){ private void registerCommands(){
CommandRegistry.INSTANCE.register(false, dispatcher -> dispatcher.register( CommandRegistry.INSTANCE.register(false, dispatcher -> dispatcher.register(
LiteralArgumentBuilder.<ServerCommandSource>literal("backup") LiteralArgumentBuilder.<ServerCommandSource>literal("backup")
.then(StartBackupCommand.register()) .then(BlacklistCommand.register())
.then(CleanupCommand.register()) .then(CleanupCommand.register())
.then(StartBackupCommand.register())
.then(WhitelistCommand.register())
)); ));
} }
} }

View File

@ -0,0 +1,103 @@
package net.szum123321.textile_backup.commands;
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.arguments.EntityArgumentType;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.TranslatableText;
import net.szum123321.textile_backup.TextileBackup;
public class BlacklistCommand {
public static LiteralArgumentBuilder<ServerCommandSource> register(){
return CommandManager.literal("whitelist")
.requires(ctx -> TextileBackup.config.whitelist.contains(ctx.getName()) ||
ctx.hasPermissionLevel(TextileBackup.config.permissionLevel) &&
!TextileBackup.config.blacklist.contains(ctx.getName()))
.then(CommandManager.literal("add")
.then(CommandManager.argument("Player", EntityArgumentType.player()))
.executes(BlacklistCommand::executeAdd)
)
.then(CommandManager.literal("remove")
.then(CommandManager.argument("Player", EntityArgumentType.player()))
.executes(BlacklistCommand::executeRemove)
)
.then(CommandManager.literal("list")
.executes(ctx -> executeList(ctx.getSource()))
)
.executes(ctx -> help(ctx.getSource()));
}
private static int help(ServerCommandSource source){
source.sendFeedback(new TranslatableText("Available command are: add [player], remove [player], list."), false);
return 1;
}
private static int executeList(ServerCommandSource source){
StringBuilder builder = new StringBuilder();
builder.append("Currently on the blacklist are: ");
for(String name : TextileBackup.config.blacklist){
builder.append(name);
builder.append(", ");
}
source.sendFeedback(new TranslatableText(builder.toString()), false);
return 1;
}
private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
PlayerEntity player = EntityArgumentType.getPlayer(ctx, "Player");
if(TextileBackup.config.blacklist.contains(player.getEntityName())) {
ctx.getSource().sendFeedback(new TranslatableText("Player: {} is already blacklisted.", player.getEntityName()), false);
}else{
TextileBackup.config.blacklist.add(player.getEntityName());
ConfigManager.saveConfig(TextileBackup.config);
StringBuilder builder = new StringBuilder();
builder.append("Player: ");
builder.append(player.getEntityName());
builder.append(" added to the blacklist");
if(TextileBackup.config.whitelist.contains(player.getEntityName())){
TextileBackup.config.whitelist.remove(player.getEntityName());
builder.append(" and removed form the whitelist");
}
builder.append(" successfully.");
ctx.getSource().sendFeedback(new TranslatableText(builder.toString()), false);
}
return 1;
}
private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
PlayerEntity player = EntityArgumentType.getPlayer(ctx, "Player");
if(!TextileBackup.config.blacklist.contains(player.getEntityName())) {
ctx.getSource().sendFeedback(new TranslatableText("Player: {} newer was blacklisted.", player.getEntityName()), false);
}else{
TextileBackup.config.blacklist.remove(player.getEntityName());
ConfigManager.saveConfig(TextileBackup.config);
StringBuilder builder = new StringBuilder();
builder.append("Player: ");
builder.append(player.getEntityName());
builder.append(" removed from the blacklist successfully.");
ctx.getSource().sendFeedback(new TranslatableText(builder.toString()), false);
}
return 1;
}
}

View File

@ -21,17 +21,22 @@ package net.szum123321.textile_backup.commands;
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.TranslatableText;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.core.BackupHelper; import net.szum123321.textile_backup.core.BackupHelper;
public class CleanupCommand { public class CleanupCommand {
public static LiteralArgumentBuilder<ServerCommandSource> register(){ public static LiteralArgumentBuilder<ServerCommandSource> register(){
return CommandManager.literal("cleanup") return CommandManager.literal("cleanup")
.requires(ctx -> ctx.hasPermissionLevel(4)) .requires(ctx -> TextileBackup.config.whitelist.contains(ctx.getName()) ||
ctx.hasPermissionLevel(TextileBackup.config.permissionLevel) &&
!TextileBackup.config.blacklist.contains(ctx.getName()))
.executes(ctx -> execute(ctx.getSource())); .executes(ctx -> execute(ctx.getSource()));
} }
private static int execute(ServerCommandSource source){ private static int execute(ServerCommandSource source){
BackupHelper.executeFileLimit(source); BackupHelper.executeFileLimit(source, source.getMinecraftServer().getLevelName());
source.sendFeedback(new TranslatableText("Done"), false);
return 1; return 1;
} }

View File

@ -23,12 +23,15 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
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.core.BackupHelper; import net.szum123321.textile_backup.core.BackupHelper;
public class StartBackupCommand { public class StartBackupCommand {
public static LiteralArgumentBuilder<ServerCommandSource> register(){ public static LiteralArgumentBuilder<ServerCommandSource> register(){
return CommandManager.literal("start") return CommandManager.literal("start")
.requires(ctx -> ctx.hasPermissionLevel(4)) .requires(ctx -> TextileBackup.config.whitelist.contains(ctx.getName()) ||
ctx.hasPermissionLevel(TextileBackup.config.permissionLevel) &&
!TextileBackup.config.blacklist.contains(ctx.getName()))
.then(CommandManager.argument("Comment", StringArgumentType.word()) .then(CommandManager.argument("Comment", StringArgumentType.word())
.executes(StartBackupCommand::executeWithComment) .executes(StartBackupCommand::executeWithComment)
).executes(ctx -> execute(ctx.getSource())); ).executes(ctx -> execute(ctx.getSource()));
@ -41,7 +44,7 @@ public class StartBackupCommand {
} }
private static int execute(ServerCommandSource source){ private static int execute(ServerCommandSource source){
BackupHelper.create(source.getMinecraftServer(), source, true, null); BackupHelper.create(source.getMinecraftServer(), source,true, null);
return 1; return 1;
} }

View File

@ -0,0 +1,103 @@
package net.szum123321.textile_backup.commands;
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.arguments.EntityArgumentType;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.TranslatableText;
import net.szum123321.textile_backup.TextileBackup;
import sun.security.krb5.Config;
public class WhitelistCommand {
public static LiteralArgumentBuilder<ServerCommandSource> register(){
return CommandManager.literal("whitelist")
.requires(ctx -> TextileBackup.config.whitelist.contains(ctx.getName()) ||
ctx.hasPermissionLevel(TextileBackup.config.permissionLevel) &&
!TextileBackup.config.blacklist.contains(ctx.getName()))
.then(CommandManager.literal("add")
.then(CommandManager.argument("Player", EntityArgumentType.player()))
.executes(WhitelistCommand::executeAdd)
)
.then(CommandManager.literal("remove")
.then(CommandManager.argument("Player", EntityArgumentType.player()))
.executes(WhitelistCommand::executeRemove)
)
.then(CommandManager.literal("list")
.executes(ctx -> executeList(ctx.getSource()))
)
.executes(ctx -> help(ctx.getSource()));
}
private static int help(ServerCommandSource source){
source.sendFeedback(new TranslatableText("Available command are: add [player], remove [player], list."), false);
return 1;
}
private static int executeList(ServerCommandSource source){
StringBuilder builder = new StringBuilder();
builder.append("Currently on the whitelist are: ");
for(String name : TextileBackup.config.whitelist){
builder.append(name);
builder.append(", ");
}
source.sendFeedback(new TranslatableText(builder.toString()), false);
return 1;
}
private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
PlayerEntity player = EntityArgumentType.getPlayer(ctx, "Player");
if(TextileBackup.config.whitelist.contains(player.getEntityName())) {
ctx.getSource().sendFeedback(new TranslatableText("Player: {} is already whitelisted.", player.getEntityName()), false);
}else{
TextileBackup.config.whitelist.add(player.getEntityName());
ConfigManager.saveConfig(TextileBackup.config);
StringBuilder builder = new StringBuilder();
builder.append("Player: ");
builder.append(player.getEntityName());
builder.append(" added to the whitelist");
if(TextileBackup.config.blacklist.contains(player.getEntityName())){
TextileBackup.config.blacklist.remove(player.getEntityName());
builder.append(" and removed form the blacklist");
}
builder.append(" successfully.");
ctx.getSource().sendFeedback(new TranslatableText(builder.toString()), false);
}
return 1;
}
private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
PlayerEntity player = EntityArgumentType.getPlayer(ctx, "Player");
if(!TextileBackup.config.whitelist.contains(player.getEntityName())) {
ctx.getSource().sendFeedback(new TranslatableText("Player: {} newer was on the whitelist.", player.getEntityName()), false);
}else{
TextileBackup.config.whitelist.remove(player.getEntityName());
ConfigManager.saveConfig(TextileBackup.config);
StringBuilder builder = new StringBuilder();
builder.append("Player: ");
builder.append(player.getEntityName());
builder.append(" removed from the whitelist successfully.");
ctx.getSource().sendFeedback(new TranslatableText(builder.toString()), false);
}
return 1;
}
}

View File

@ -74,12 +74,10 @@ public class BackupHelper {
MakeBackupThread thread = new MakeBackupThread(server, ctx, comment); MakeBackupThread thread = new MakeBackupThread(server, ctx, comment);
thread.start(); thread.start();
executeFileLimit(ctx);
} }
public static void executeFileLimit(ServerCommandSource ctx){ public static void executeFileLimit(ServerCommandSource ctx, String worldName){
File root = getBackupRootPath(); File root = getBackupRootPath(worldName);
FileFilter filter = f -> f.getName().endsWith("zip"); FileFilter filter = f -> f.getName().endsWith("zip");
@ -89,11 +87,25 @@ public class BackupHelper {
Arrays.stream(root.listFiles()).forEach(f ->{ Arrays.stream(root.listFiles()).forEach(f ->{
if(f.exists() && f.isFile()){ if(f.exists() && f.isFile()){
LocalDateTime creationTime = LocalDateTime.from( LocalDateTime creationTime;
getDateTimeFormatter().parse(
f.getName().split(".zip")[0].split("#")[0] try {
) creationTime = LocalDateTime.from(
); getDateTimeFormatter().parse(
f.getName().split(".zip")[0].split("#")[0]
)
);
}catch(Exception e){
System.out.println(e.getClass());
System.out.println(e.toString());
creationTime = LocalDateTime.from(
getBackupDateTimeFormatter().parse(
f.getName().split(".zip")[0].split("#")[0]
)
);
}
if(now.toEpochSecond(ZoneOffset.UTC) - creationTime.toEpochSecond(ZoneOffset.UTC) > TextileBackup.config.maxAge) { if(now.toEpochSecond(ZoneOffset.UTC) - creationTime.toEpochSecond(ZoneOffset.UTC) > TextileBackup.config.maxAge) {
log("Deleting: " + f.getName(), ctx); log("Deleting: " + f.getName(), ctx);
@ -128,9 +140,12 @@ public class BackupHelper {
} }
} }
public static File getBackupRootPath(){ public static File getBackupRootPath(String worldName){
File path = new File(TextileBackup.config.path); File path = new File(TextileBackup.config.path);
if(TextileBackup.config.perWorldBackup)
path = path.toPath().resolve(worldName).toFile();
if(!path.exists()){ if(!path.exists()){
try{ try{
path.mkdirs(); path.mkdirs();
@ -150,8 +165,15 @@ public class BackupHelper {
} }
public static DateTimeFormatter getDateTimeFormatter(){ public static DateTimeFormatter getDateTimeFormatter(){
String os = System.getProperty("os.name"); if(TextileBackup.config.dateTimeFormat != null)
if (os.toLowerCase().startsWith("win")) { return DateTimeFormatter.ofPattern(TextileBackup.config.dateTimeFormat);
else
return getBackupDateTimeFormatter();
}
public static DateTimeFormatter getBackupDateTimeFormatter(){
String os = System.getProperty("os.name");
if(os.toLowerCase().startsWith("win")){
return DateTimeFormatter.ofPattern("dd.MM.yyyy_HH-mm-ss"); return DateTimeFormatter.ofPattern("dd.MM.yyyy_HH-mm-ss");
} else { } else {
return DateTimeFormatter.ofPattern("dd.MM.yyyy_HH:mm:ss"); return DateTimeFormatter.ofPattern("dd.MM.yyyy_HH:mm:ss");

View File

@ -45,7 +45,7 @@ public class MakeBackupThread extends Thread {
.getWorldDir(); .getWorldDir();
File outFile = BackupHelper File outFile = BackupHelper
.getBackupRootPath() .getBackupRootPath(server.getLevelName())
.toPath() .toPath()
.resolve(getFileName()) .resolve(getFileName())
.toFile(); .toFile();
@ -61,7 +61,7 @@ public class MakeBackupThread extends Thread {
Compressor.createArchive(world, outFile, ctx); Compressor.createArchive(world, outFile, ctx);
BackupHelper.executeFileLimit(ctx); BackupHelper.executeFileLimit(ctx, server.getLevelName());
BackupHelper.log("Done!", ctx); BackupHelper.log("Done!", ctx);
} }

View File

@ -38,7 +38,7 @@ public abstract class MinecraftServerMixin {
@Inject(method = "tick", at = @At("HEAD")) @Inject(method = "tick", at = @At("HEAD"))
public void tick(CallbackInfo ci){ public void tick(CallbackInfo ci){
if(timeReference - lastBackup >= TextileBackup.config.backupInterval){ if(timeReference - lastBackup >= TextileBackup.config.backupInterval * 1000){
if(getPlayerManager().getCurrentPlayerCount() == 0 && !TextileBackup.config.doBackupsOnEmptyServer) if(getPlayerManager().getCurrentPlayerCount() == 0 && !TextileBackup.config.doBackupsOnEmptyServer)
return; return;

View File

@ -4,7 +4,7 @@
"version": "${version}", "version": "${version}",
"name": "Textile Backup", "name": "Textile Backup",
"description": "Small, configurable, fully server-side backup mod for Fabric", "description": "Small, configurable, fully server-sided backup mod for Fabric",
"authors": [ "authors": [
"Szum123321" "Szum123321"
], ],