Should work now. Only a ton of testing left...

2.x-1.16
szymon 2020-08-03 23:38:54 +02:00
parent 7c7c4c7493
commit 227faef84c
17 changed files with 317 additions and 116 deletions

View File

@ -92,6 +92,12 @@ public class ConfigHandler {
"For more info: https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html\n") "For more info: https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html\n")
public String dateTimeFormat = "dd.MM.yyyy_HH-mm-ss"; public String dateTimeFormat = "dd.MM.yyyy_HH-mm-ss";
@Comment("\nShould old world be backed-up?\n")
public boolean backupOldWorlds = true;
@Comment("\nDelay between typing-in /backup restore and it actually starting\n")
public int restoreDelay = 30;
public Optional<String> sanitize() { public Optional<String> sanitize() {
if(compressionCoreCountLimit > Runtime.getRuntime().availableProcessors()) if(compressionCoreCountLimit > Runtime.getRuntime().availableProcessors())
return Optional.of("compressionCoreCountLimit is too big! Your system only has: " + Runtime.getRuntime().availableProcessors() + " cores!"); return Optional.of("compressionCoreCountLimit is too big! Your system only has: " + Runtime.getRuntime().availableProcessors() + " cores!");

View File

@ -0,0 +1,5 @@
package net.szum123321.textile_backup;
public interface LivingServer {
boolean isAlive();
}

View File

@ -28,9 +28,9 @@ import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.commands.*; import net.szum123321.textile_backup.commands.*;
import net.szum123321.textile_backup.core.create.BackupScheduler; import net.szum123321.textile_backup.core.create.BackupScheduler;
import net.szum123321.textile_backup.core.restore.RestoreHelper;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -39,9 +39,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
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";
public static final Logger LOGGER = LogManager.getFormatterLogger("Textile Backup"); public static final Logger LOGGER = LogManager.getLogger("Textile Backup", ParameterizedMessageFactory.INSTANCE);
public static ConfigHandler config; public static ConfigHandler CONFIG;
public static final BackupScheduler scheduler = new BackupScheduler(); public static final BackupScheduler scheduler = new BackupScheduler();
public static ExecutorService executorService = Executors.newSingleThreadExecutor(); public static ExecutorService executorService = Executors.newSingleThreadExecutor();
@ -49,16 +49,16 @@ public class TextileBackup implements ModInitializer {
@Override @Override
public void onInitialize() { public void onInitialize() {
config = ConfigManager.loadConfig(ConfigHandler.class); CONFIG = ConfigManager.loadConfig(ConfigHandler.class);
Optional<String> errorMessage = config.sanitize(); Optional<String> errorMessage = CONFIG.sanitize();
if(errorMessage.isPresent()) { if(errorMessage.isPresent()) {
LOGGER.fatal("TextileBackup config file has wrong settings! \n" + errorMessage.get()); LOGGER.fatal("TextileBackup config file has wrong settings! \n {}", errorMessage.get());
System.exit(1); System.exit(1);
} }
if(TextileBackup.config.backupInterval > 0) if(TextileBackup.CONFIG.backupInterval > 0)
ServerTickEvents.END_SERVER_TICK.register(scheduler::tick); ServerTickEvents.END_SERVER_TICK.register(scheduler::tick);
ServerLifecycleEvents.SERVER_STARTING.register(ignored -> { ServerLifecycleEvents.SERVER_STARTING.register(ignored -> {
@ -72,11 +72,11 @@ public class TextileBackup implements ModInitializer {
LiteralArgumentBuilder.<ServerCommandSource>literal("backup") LiteralArgumentBuilder.<ServerCommandSource>literal("backup")
.requires((ctx) -> { .requires((ctx) -> {
try { try {
return ((config.playerWhitelist.contains(ctx.getEntityOrThrow().getEntityName()) || return ((CONFIG.playerWhitelist.contains(ctx.getEntityOrThrow().getEntityName()) ||
ctx.hasPermissionLevel(config.permissionLevel)) && ctx.hasPermissionLevel(CONFIG.permissionLevel)) &&
!config.playerBlacklist.contains(ctx.getEntityOrThrow().getEntityName())) || !CONFIG.playerBlacklist.contains(ctx.getEntityOrThrow().getEntityName())) ||
(ctx.getMinecraftServer().isSinglePlayer() && (ctx.getMinecraftServer().isSinglePlayer() &&
config.alwaysSingleplayerAllowed); CONFIG.alwaysSingleplayerAllowed);
} catch (Exception ignored) { //Command was called from server console. } catch (Exception ignored) { //Command was called from server console.
return true; return true;
} }
@ -86,6 +86,7 @@ public class TextileBackup implements ModInitializer {
.then(StartBackupCommand.register()) .then(StartBackupCommand.register())
.then(WhitelistCommand.register()) .then(WhitelistCommand.register())
.then(RestoreBackupCommand.register()) .then(RestoreBackupCommand.register())
.then(ListBackupsCommand.register())
)); ));
} }
} }

View File

@ -40,7 +40,7 @@ public class BlacklistCommand {
builder.append("Currently on the blacklist are: "); builder.append("Currently on the blacklist are: ");
for(String name : TextileBackup.config.playerBlacklist){ for(String name : TextileBackup.CONFIG.playerBlacklist){
builder.append(name); builder.append(name);
builder.append(", "); builder.append(", ");
} }
@ -53,11 +53,11 @@ public class BlacklistCommand {
private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException { private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player"); ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(TextileBackup.config.playerBlacklist.contains(player.getEntityName())) { if(TextileBackup.CONFIG.playerBlacklist.contains(player.getEntityName())) {
ctx.getSource().sendFeedback(new TranslatableText("Player: %s is already blacklisted.", player.getEntityName()), false); ctx.getSource().sendFeedback(new TranslatableText("Player: %s is already blacklisted.", player.getEntityName()), false);
}else{ }else{
TextileBackup.config.playerBlacklist.add(player.getEntityName()); TextileBackup.CONFIG.playerBlacklist.add(player.getEntityName());
ConfigManager.saveConfig(TextileBackup.config); ConfigManager.saveConfig(TextileBackup.CONFIG);
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
@ -65,8 +65,8 @@ public class BlacklistCommand {
builder.append(player.getEntityName()); builder.append(player.getEntityName());
builder.append(" added to the blacklist"); builder.append(" added to the blacklist");
if(TextileBackup.config.playerWhitelist.contains(player.getEntityName())){ if(TextileBackup.CONFIG.playerWhitelist.contains(player.getEntityName())){
TextileBackup.config.playerWhitelist.remove(player.getEntityName()); TextileBackup.CONFIG.playerWhitelist.remove(player.getEntityName());
builder.append(" and removed form the whitelist"); builder.append(" and removed form the whitelist");
} }
@ -83,11 +83,11 @@ public class BlacklistCommand {
private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException { private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player"); ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(!TextileBackup.config.playerBlacklist.contains(player.getEntityName())) { if(!TextileBackup.CONFIG.playerBlacklist.contains(player.getEntityName())) {
ctx.getSource().sendFeedback(new TranslatableText("Player: %s newer was blacklisted.", player.getEntityName()), false); ctx.getSource().sendFeedback(new TranslatableText("Player: %s newer was blacklisted.", player.getEntityName()), false);
}else{ }else{
TextileBackup.config.playerBlacklist.remove(player.getEntityName()); TextileBackup.CONFIG.playerBlacklist.remove(player.getEntityName());
ConfigManager.saveConfig(TextileBackup.config); ConfigManager.saveConfig(TextileBackup.CONFIG);
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();

View File

@ -0,0 +1,28 @@
package net.szum123321.textile_backup.commands;
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.core.Utilities;
import java.util.Arrays;
public class ListBackupsCommand {
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("list")
.executes(ctx -> {
StringBuilder builder = new StringBuilder();
builder.append("Available backups: ");
Arrays.stream(Utilities.getBackupRootPath(Utilities.getLevelName(ctx.getSource().getMinecraftServer()))
.listFiles())
.forEach(file -> builder.append(file.getName()).append(", "));
ctx.getSource().sendFeedback(new LiteralText(builder.toString()), false);
return 1;
});
}
}

View File

@ -19,23 +19,47 @@ import java.time.format.DateTimeFormatter;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
public class RestoreBackupCommand { public class RestoreBackupCommand {
private final static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss");
public static LiteralArgumentBuilder<ServerCommandSource> register() { public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("restore") return CommandManager.literal("restore")
.then(CommandManager.argument("file", StringArgumentType.greedyString()) .then(CommandManager.argument("file", StringArgumentType.word())
.suggests(new FileSuggestionProvider()) .suggests(new FileSuggestionProvider())
.executes(RestoreBackupCommand::execute)); .executes(RestoreBackupCommand::execute)
).then(CommandManager.argument("file", StringArgumentType.word())
.suggests(new FileSuggestionProvider())
.then(CommandManager.argument("comment", StringArgumentType.word())
.executes(RestoreBackupCommand::executeWithCommand)
)
);
} }
private static int execute(CommandContext<ServerCommandSource> ctx) { private static int execute(CommandContext<ServerCommandSource> ctx) {
String arg = StringArgumentType.getString(ctx, "file"); String file = StringArgumentType.getString(ctx, "file");
LocalDateTime dateTime = LocalDateTime.from(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").parse(arg)); LocalDateTime dateTime = LocalDateTime.from(dateTimeFormatter.parse(file));
if(ctx.getSource().getEntity() != null) if(ctx.getSource().getEntity() != null)
TextileBackup.LOGGER.info("Backup restoration was initiated by: {}", ctx.getSource().getName()); TextileBackup.LOGGER.info("Backup restoration was initiated by: {}", ctx.getSource().getName());
else else
TextileBackup.LOGGER.info("Backup restoration was initiated form Server Console"); TextileBackup.LOGGER.info("Backup restoration was initiated form Server Console");
new Thread(RestoreHelper.create(dateTime, ctx.getSource().getMinecraftServer())).start(); RestoreHelper.create(dateTime, ctx.getSource().getMinecraftServer(), null).start();
return 1;
}
private static int executeWithCommand(CommandContext<ServerCommandSource> ctx) {
String file = StringArgumentType.getString(ctx, "file");
String comment = StringArgumentType.getString(ctx, "comment");
LocalDateTime dateTime = LocalDateTime.from(dateTimeFormatter.parse(file));
if(ctx.getSource().getEntity() != null)
TextileBackup.LOGGER.info("Backup restoration was initiated by: {}", ctx.getSource().getName());
else
TextileBackup.LOGGER.info("Backup restoration was initiated form Server Console");
RestoreHelper.create(dateTime, ctx.getSource().getMinecraftServer(), comment).start();
return 1; return 1;
} }
@ -46,7 +70,7 @@ public class RestoreBackupCommand {
String remaining = builder.getRemaining(); String remaining = builder.getRemaining();
for(RestoreHelper.RestoreableFile file : RestoreHelper.getAvailableBackups(ctx.getSource().getMinecraftServer())) { for(RestoreHelper.RestoreableFile file : RestoreHelper.getAvailableBackups(ctx.getSource().getMinecraftServer())) {
String formattedCreationTime = file.getCreationTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); String formattedCreationTime = file.getCreationTime().format(dateTimeFormatter);
if(formattedCreationTime.startsWith(remaining)) { if(formattedCreationTime.startsWith(remaining)) {
if(ctx.getSource().getEntity() != null) { //was typed by player if(ctx.getSource().getEntity() != null) { //was typed by player

View File

@ -40,7 +40,7 @@ public class WhitelistCommand {
builder.append("Currently on the whitelist are: "); builder.append("Currently on the whitelist are: ");
for(String name : TextileBackup.config.playerWhitelist){ for(String name : TextileBackup.CONFIG.playerWhitelist){
builder.append(name); builder.append(name);
builder.append(", "); builder.append(", ");
} }
@ -53,11 +53,11 @@ public class WhitelistCommand {
private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException { private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player"); ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(TextileBackup.config.playerWhitelist.contains(player.getEntityName())) { if(TextileBackup.CONFIG.playerWhitelist.contains(player.getEntityName())) {
ctx.getSource().sendFeedback(new TranslatableText("Player: %s is already whitelisted.", player.getEntityName()), false); ctx.getSource().sendFeedback(new TranslatableText("Player: %s is already whitelisted.", player.getEntityName()), false);
}else{ }else{
TextileBackup.config.playerWhitelist.add(player.getEntityName()); TextileBackup.CONFIG.playerWhitelist.add(player.getEntityName());
ConfigManager.saveConfig(TextileBackup.config); ConfigManager.saveConfig(TextileBackup.CONFIG);
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
@ -65,8 +65,8 @@ public class WhitelistCommand {
builder.append(player.getEntityName()); builder.append(player.getEntityName());
builder.append(" added to the whitelist"); builder.append(" added to the whitelist");
if(TextileBackup.config.playerBlacklist.contains(player.getEntityName())){ if(TextileBackup.CONFIG.playerBlacklist.contains(player.getEntityName())){
TextileBackup.config.playerBlacklist.remove(player.getEntityName()); TextileBackup.CONFIG.playerBlacklist.remove(player.getEntityName());
builder.append(" and removed form the blacklist"); builder.append(" and removed form the blacklist");
} }
@ -83,11 +83,11 @@ public class WhitelistCommand {
private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException { private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player"); ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(!TextileBackup.config.playerWhitelist.contains(player.getEntityName())) { if(!TextileBackup.CONFIG.playerWhitelist.contains(player.getEntityName())) {
ctx.getSource().sendFeedback(new TranslatableText("Player: %s newer was on the whitelist.", player.getEntityName()), false); ctx.getSource().sendFeedback(new TranslatableText("Player: %s newer was on the whitelist.", player.getEntityName()), false);
}else{ }else{
TextileBackup.config.playerWhitelist.remove(player.getEntityName()); TextileBackup.CONFIG.playerWhitelist.remove(player.getEntityName());
ConfigManager.saveConfig(TextileBackup.config); ConfigManager.saveConfig(TextileBackup.CONFIG);
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append("Player: "); builder.append("Player: ");

View File

@ -1,3 +1,21 @@
/*
A simple backup mod for Fabric
Copyright (C) 2020 Szum123321
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core; package net.szum123321.textile_backup.core;
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
@ -37,7 +55,7 @@ public class Utilities {
} }
} }
for(String i : TextileBackup.config.fileBlacklist) { for(String i : TextileBackup.CONFIG.fileBlacklist) {
if(path.startsWith(i)) if(path.startsWith(i))
return true; return true;
} }
@ -72,9 +90,9 @@ public class Utilities {
} }
public static File getBackupRootPath(String worldName) { public static File getBackupRootPath(String worldName) {
File path = new File(TextileBackup.config.path).getAbsoluteFile(); File path = new File(TextileBackup.CONFIG.path).getAbsoluteFile();
if (TextileBackup.config.perWorldBackup) if (TextileBackup.CONFIG.perWorldBackup)
path = path.toPath().resolve(worldName).toFile(); path = path.toPath().resolve(worldName).toFile();
if (!path.exists()) { if (!path.exists()) {
@ -87,7 +105,7 @@ public class Utilities {
.getInstance() .getInstance()
.getGameDirectory() .getGameDirectory()
.toPath() .toPath()
.resolve(TextileBackup.config.path) .resolve(TextileBackup.CONFIG.path)
.toFile(); .toFile();
} }
} }
@ -137,8 +155,8 @@ public class Utilities {
} }
public static DateTimeFormatter getDateTimeFormatter(){ public static DateTimeFormatter getDateTimeFormatter(){
if(!TextileBackup.config.dateTimeFormat.equals("")) if(!TextileBackup.CONFIG.dateTimeFormat.equals(""))
return DateTimeFormatter.ofPattern(TextileBackup.config.dateTimeFormat); return DateTimeFormatter.ofPattern(TextileBackup.CONFIG.dateTimeFormat);
else else
return getBackupDateTimeFormatter(); return getBackupDateTimeFormatter();
} }
@ -164,7 +182,7 @@ public class Utilities {
if(ctx != null && ctx.getEntity() != null) if(ctx != null && ctx.getEntity() != null)
ctx.sendFeedback(new LiteralText(s), false); ctx.sendFeedback(new LiteralText(s), false);
if(TextileBackup.config.log) if(TextileBackup.CONFIG.log)
TextileBackup.LOGGER.info(s); TextileBackup.LOGGER.info(s);
} }

View File

@ -63,13 +63,13 @@ public class BackupHelper {
AtomicInteger deletedFiles = new AtomicInteger(); AtomicInteger deletedFiles = new AtomicInteger();
if (root.isDirectory() && root.exists() && root.listFiles() != null) { if (root.isDirectory() && root.exists() && root.listFiles() != null) {
if (TextileBackup.config.maxAge > 0) { // delete files older that configured if (TextileBackup.CONFIG.maxAge > 0) { // delete files older that configured
final LocalDateTime now = LocalDateTime.now(); final LocalDateTime now = LocalDateTime.now();
Arrays.stream(root.listFiles()) Arrays.stream(root.listFiles())
.filter(BackupHelper::isFileOk) .filter(BackupHelper::isFileOk)
.filter(f -> Utilities.getFileCreationTime(f).isPresent()) // We check if we can get file's creation date so that the next line won't throw an exception .filter(f -> Utilities.getFileCreationTime(f).isPresent()) // 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) > TextileBackup.config.maxAge) .filter(f -> now.toEpochSecond(ZoneOffset.UTC) - Utilities.getFileCreationTime(f).get().toEpochSecond(ZoneOffset.UTC) > TextileBackup.CONFIG.maxAge)
.forEach(f -> { .forEach(f -> {
if(f.delete()) { if(f.delete()) {
Utilities.info("Deleting: " + f.getName(), ctx); Utilities.info("Deleting: " + f.getName(), ctx);
@ -80,7 +80,7 @@ public class BackupHelper {
}); });
} }
if (TextileBackup.config.backupsToKeep > 0 && root.listFiles().length > TextileBackup.config.backupsToKeep) { if (TextileBackup.CONFIG.backupsToKeep > 0 && root.listFiles().length > TextileBackup.CONFIG.backupsToKeep) {
int i = root.listFiles().length; int i = root.listFiles().length;
Iterator<File> it = Arrays.stream(root.listFiles()) Iterator<File> it = Arrays.stream(root.listFiles())
@ -89,7 +89,7 @@ public class BackupHelper {
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get())) .sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get()))
.iterator(); .iterator();
while(i > TextileBackup.config.backupsToKeep && it.hasNext()) { while(i > TextileBackup.CONFIG.backupsToKeep && it.hasNext()) {
File f = it.next(); File f = it.next();
if(f.delete()) { if(f.delete()) {
@ -103,14 +103,14 @@ public class BackupHelper {
} }
} }
if (TextileBackup.config.maxSize > 0 && FileUtils.sizeOfDirectory(root) / 1024 > TextileBackup.config.maxSize) { if (TextileBackup.CONFIG.maxSize > 0 && FileUtils.sizeOfDirectory(root) / 1024 > TextileBackup.CONFIG.maxSize) {
Iterator<File> it = Arrays.stream(root.listFiles()) Iterator<File> it = Arrays.stream(root.listFiles())
.filter(BackupHelper::isFileOk) .filter(BackupHelper::isFileOk)
.filter(f -> Utilities.getFileCreationTime(f).isPresent()) .filter(f -> Utilities.getFileCreationTime(f).isPresent())
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get())) .sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get()))
.iterator(); .iterator();
while(FileUtils.sizeOfDirectory(root) / 1024 > TextileBackup.config.maxSize && it.hasNext()) { while(FileUtils.sizeOfDirectory(root) / 1024 > TextileBackup.CONFIG.maxSize && it.hasNext()) {
File f = it.next(); File f = it.next();
if(f.delete()) { if(f.delete()) {

View File

@ -17,7 +17,7 @@ public class BackupScheduler {
public void tick(MinecraftServer server) { public void tick(MinecraftServer server) {
long now = Instant.now().getEpochSecond(); long now = Instant.now().getEpochSecond();
if(TextileBackup.config.doBackupsOnEmptyServer || server.getPlayerManager().getCurrentPlayerCount() > 0) { if(TextileBackup.CONFIG.doBackupsOnEmptyServer || server.getPlayerManager().getCurrentPlayerCount() > 0) {
if(scheduled) { if(scheduled) {
if(nextBackup <= now) { if(nextBackup <= now) {
TextileBackup.executorService.submit( TextileBackup.executorService.submit(
@ -30,13 +30,13 @@ public class BackupScheduler {
) )
); );
nextBackup = now + TextileBackup.config.backupInterval; nextBackup = now + TextileBackup.CONFIG.backupInterval;
} }
} else { } else {
nextBackup = now + TextileBackup.config.backupInterval; nextBackup = now + TextileBackup.CONFIG.backupInterval;
scheduled = true; scheduled = true;
} }
} else if(!TextileBackup.config.doBackupsOnEmptyServer && server.getPlayerManager().getCurrentPlayerCount() == 0) { } else if(!TextileBackup.CONFIG.doBackupsOnEmptyServer && server.getPlayerManager().getCurrentPlayerCount() == 0) {
if(scheduled && nextBackup <= now) { if(scheduled && nextBackup <= now) {
TextileBackup.executorService.submit( TextileBackup.executorService.submit(
BackupHelper.create( BackupHelper.create(

View File

@ -72,15 +72,15 @@ public class MakeBackupRunnable implements Runnable {
int coreCount; int coreCount;
if(TextileBackup.config.compressionCoreCountLimit <= 0) { if(TextileBackup.CONFIG.compressionCoreCountLimit <= 0) {
coreCount = Runtime.getRuntime().availableProcessors(); coreCount = Runtime.getRuntime().availableProcessors();
} else { } else {
coreCount = Math.min(TextileBackup.config.compressionCoreCountLimit, Runtime.getRuntime().availableProcessors()); coreCount = Math.min(TextileBackup.CONFIG.compressionCoreCountLimit, Runtime.getRuntime().availableProcessors());
} }
TextileBackup.LOGGER.trace("Running compression on {} threads. Available cores = {}", coreCount, Runtime.getRuntime().availableProcessors()); TextileBackup.LOGGER.trace("Running compression on {} threads. Available cores = {}", coreCount, Runtime.getRuntime().availableProcessors());
switch (TextileBackup.config.format) { switch (TextileBackup.CONFIG.format) {
case ZIP: case ZIP:
ParallelZipCompressor.createArchive(world, outFile, commandSource, coreCount); ParallelZipCompressor.createArchive(world, outFile, commandSource, coreCount);
break; break;
@ -98,7 +98,7 @@ public class MakeBackupRunnable implements Runnable {
break; break;
default: default:
TextileBackup.LOGGER.warn("Specified compressor ({}) is not supported! Zip will be used instead!", TextileBackup.config.format); TextileBackup.LOGGER.warn("Specified compressor ({}) is not supported! Zip will be used instead!", TextileBackup.CONFIG.format);
Utilities.sendError("Error! No correct compression format specified! Using default compressor!", commandSource); Utilities.sendError("Error! No correct compression format specified! Using default compressor!", commandSource);
ParallelZipCompressor.createArchive(world, outFile, commandSource, coreCount); ParallelZipCompressor.createArchive(world, outFile, commandSource, coreCount);
@ -113,6 +113,6 @@ public class MakeBackupRunnable implements Runnable {
private String getFileName(){ private String getFileName(){
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
return Utilities.getDateTimeFormatter().format(now) + (comment != null ? "#" + comment.replace("#", "") : "") + TextileBackup.config.format.getString(); return Utilities.getDateTimeFormatter().format(now) + (comment != null ? "#" + comment.replace("#", "") : "") + TextileBackup.CONFIG.format.getString();
} }
} }

View File

@ -36,7 +36,7 @@ public class ParallelZipCompressor {
arc.setMethod(ZipArchiveOutputStream.DEFLATED); arc.setMethod(ZipArchiveOutputStream.DEFLATED);
arc.setUseZip64(Zip64Mode.AsNeeded); arc.setUseZip64(Zip64Mode.AsNeeded);
arc.setLevel(TextileBackup.config.compression); arc.setLevel(TextileBackup.CONFIG.compression);
arc.setComment("Created on: " + Utilities.getDateTimeFormatter().format(LocalDateTime.now())); arc.setComment("Created on: " + Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
File input = in.getCanonicalFile(); File input = in.getCanonicalFile();

View File

@ -1,8 +1,29 @@
/*
A simple backup mod for Fabric
Copyright (C) 2020 Szum123321
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.restore; package net.szum123321.textile_backup.core.restore;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.szum123321.textile_backup.LivingServer;
import net.szum123321.textile_backup.TextileBackup; import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.core.Utilities; import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.core.create.BackupHelper;
import net.szum123321.textile_backup.core.restore.decompressors.GenericTarDecompressor; import net.szum123321.textile_backup.core.restore.decompressors.GenericTarDecompressor;
import net.szum123321.textile_backup.core.restore.decompressors.ZipDecompressor; import net.szum123321.textile_backup.core.restore.decompressors.ZipDecompressor;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
@ -10,59 +31,106 @@ import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.channels.FileLock;
public class RestoreBackupRunnable implements Runnable { public class RestoreBackupRunnable implements Runnable {
private final MinecraftServer server; private final MinecraftServer server;
private final File backupFile; private final File backupFile;
private final String finalBackupComment;
public RestoreBackupRunnable(MinecraftServer server, File backupFile) { public RestoreBackupRunnable(MinecraftServer server, File backupFile, String finalBackupComment) {
this.server = server; this.server = server;
this.backupFile = backupFile; this.backupFile = backupFile;
this.finalBackupComment = finalBackupComment;
} }
@Override @Override
public void run() { public void run() {
while(server.isRunning()) { TextileBackup.LOGGER.info("Starting countdown...");
try { waitDelay();
Thread.sleep(500);
} catch (InterruptedException e) { TextileBackup.LOGGER.info("Shutting down server...");
TextileBackup.LOGGER.error("Exception occurred!", e); server.stop(false);
} awaitServerShutdown();
if(TextileBackup.CONFIG.backupOldWorlds) {
BackupHelper.create(
new BackupContext.Builder()
.setServer(server)
.setInitiator(BackupContext.BackupInitiator.Restore)
.setComment("Old_World" + (finalBackupComment != null ? "_" + finalBackupComment : ""))
.build()
).run();
} }
File worldFile = Utilities.getWorldFolder(server); File worldFile = Utilities.getWorldFolder(server);
deleteDirectory(worldFile); TextileBackup.LOGGER.info("Deleting old world...");
if(!deleteDirectory(worldFile))
TextileBackup.LOGGER.error("Something went wrong while deleting old world!");
worldFile.mkdirs(); worldFile.mkdirs();
switch(Utilities.getFileExtension(backupFile).get()) { try(FileInputStream fileInputStream = new FileInputStream(backupFile)) {
case ZIP: TextileBackup.LOGGER.info("Starting decompression...");
ZipDecompressor.decompress(backupFile, worldFile);
break;
case GZIP: switch(Utilities.getFileExtension(backupFile).orElseThrow()) {
GenericTarDecompressor.decompress(backupFile, worldFile, GzipCompressorInputStream.class); case ZIP:
break; ZipDecompressor.decompress(fileInputStream, worldFile);
break;
case BZIP2: case GZIP:
GenericTarDecompressor.decompress(backupFile, worldFile, BZip2CompressorInputStream.class); GenericTarDecompressor.decompress(fileInputStream, worldFile, GzipCompressorInputStream.class);
break; break;
case LZMA: case BZIP2:
GenericTarDecompressor.decompress(backupFile, worldFile, XZCompressorInputStream.class); GenericTarDecompressor.decompress(fileInputStream, worldFile, BZip2CompressorInputStream.class);
break; break;
case LZMA:
GenericTarDecompressor.decompress(fileInputStream, worldFile, XZCompressorInputStream.class);
break;
}
} catch (IOException e) {
TextileBackup.LOGGER.error("Exception occurred!", e);
} }
TextileBackup.LOGGER.info("Done."); TextileBackup.LOGGER.info("Done.");
} }
private static void deleteDirectory(File f) { private void waitDelay() {
int delay = TextileBackup.CONFIG.restoreDelay;
while(delay > 0) {
try {
Thread.sleep(1000);
delay--;
} catch (InterruptedException e) {
TextileBackup.LOGGER.error("Exception occurred!", e);
}
}
}
private void awaitServerShutdown() {
while(((LivingServer)server).isAlive()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
TextileBackup.LOGGER.error("Exception occurred!", e);
}
}
}
private static boolean deleteDirectory(File f) {
boolean state = true;
if(f.isDirectory()) { if(f.isDirectory()) {
for(File f2 : f.listFiles()) for(File f2 : f.listFiles())
deleteDirectory(f2); state &= deleteDirectory(f2);
} }
f.delete(); return f.delete() && state;
} }
} }

View File

@ -1,3 +1,21 @@
/*
A simple backup mod for Fabric
Copyright (C) 2020 Szum123321
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.restore; package net.szum123321.textile_backup.core.restore;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
@ -14,10 +32,7 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class RestoreHelper { public class RestoreHelper {
public static Runnable create(LocalDateTime backupTime, MinecraftServer server) { public static Thread create(LocalDateTime backupTime, MinecraftServer server, String comment) {
server.getPlayerManager().getPlayerList()
.forEach(serverPlayerEntity -> serverPlayerEntity.sendMessage(new LiteralText("Warning! The server is going to shut down in few moments!"), false));
File backupFile = Arrays.stream(Utilities.getBackupRootPath(Utilities.getLevelName(server)) File backupFile = Arrays.stream(Utilities.getBackupRootPath(Utilities.getLevelName(server))
.listFiles()) .listFiles())
.filter(file -> Utilities.getFileCreationTime(file).isPresent()) .filter(file -> Utilities.getFileCreationTime(file).isPresent())
@ -25,24 +40,12 @@ public class RestoreHelper {
.findFirst() .findFirst()
.orElseThrow(); .orElseThrow();
TextileBackup.LOGGER.info("Restoring: {}", backupFile.getName()); server.getPlayerManager().getPlayerList()
.forEach(serverPlayerEntity -> serverPlayerEntity.sendMessage(new LiteralText("Warning! The server is going to shut down in " + TextileBackup.CONFIG.restoreDelay + " seconds!"), false));
TextileBackup.globalShutdownBackupFlag.set(false); TextileBackup.globalShutdownBackupFlag.set(false);
BackupHelper.create( return new Thread(new RestoreBackupRunnable(server, backupFile, comment));
new BackupContext.Builder()
.setServer(server)
.setInitiator(BackupContext.BackupInitiator.Restore)
.setComment("Old_World")
.setSave()
.build()
).run();
TextileBackup.LOGGER.info("Shutting down server...");
server.stop(false);
return new RestoreBackupRunnable(server, backupFile);
} }
public static List<RestoreableFile> getAvailableBackups(MinecraftServer server) { public static List<RestoreableFile> getAvailableBackups(MinecraftServer server) {
@ -61,8 +64,8 @@ public class RestoreHelper {
private final String comment; private final String comment;
protected RestoreableFile(File file) { protected RestoreableFile(File file) {
String extension = Utilities.getFileExtension(file).get().getString(); String extension = Utilities.getFileExtension(file).orElseThrow().getString();
this.creationTime = Utilities.getFileCreationTime(file).get(); this.creationTime = Utilities.getFileCreationTime(file).orElseThrow();
final String filename = file.getName(); final String filename = file.getName();

View File

@ -1,3 +1,21 @@
/*
A simple backup mod for Fabric
Copyright (C) 2020 Szum123321
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.restore.decompressors; package net.szum123321.textile_backup.core.restore.decompressors;
import net.szum123321.textile_backup.TextileBackup; import net.szum123321.textile_backup.TextileBackup;
@ -14,18 +32,19 @@ import java.time.Duration;
import java.time.Instant; import java.time.Instant;
public class GenericTarDecompressor { public class GenericTarDecompressor {
public static void decompress(File archiveFile, File target, Class<? extends CompressorInputStream> DecompressorStream) { public static void decompress(FileInputStream fileInputStream, File target, Class<? extends CompressorInputStream> DecompressorStream) {
Instant start = Instant.now(); Instant start = Instant.now();
try (FileInputStream inputStream = new FileInputStream(archiveFile); try (BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
CompressorInputStream compressorInputStream = DecompressorStream.getDeclaredConstructor(InputStream.class).newInstance(bufferedInputStream); CompressorInputStream compressorInputStream = DecompressorStream.getDeclaredConstructor(InputStream.class).newInstance(bufferedInputStream);
TarArchiveInputStream archiveInputStream = new TarArchiveInputStream(compressorInputStream)) { TarArchiveInputStream archiveInputStream = new TarArchiveInputStream(compressorInputStream)) {
TarArchiveEntry entry; TarArchiveEntry entry;
while ((entry = archiveInputStream.getNextTarEntry()) != null) { while ((entry = archiveInputStream.getNextTarEntry()) != null) {
if(!archiveInputStream.canReadEntryData(entry)) if(!archiveInputStream.canReadEntryData(entry)) {
TextileBackup.LOGGER.warn("Something when wrong while trying to decompress {}", entry.getName());
continue; continue;
}
File file = target.toPath().resolve(entry.getName()).toFile(); File file = target.toPath().resolve(entry.getName()).toFile();
@ -41,7 +60,7 @@ public class GenericTarDecompressor {
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) { BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
IOUtils.copy(archiveInputStream, bufferedOutputStream); IOUtils.copy(archiveInputStream, bufferedOutputStream);
} catch (IOException e) { } catch (IOException e) {
TextileBackup.LOGGER.error("An exception occurred while trying to compress file: " + file.getName(), e); TextileBackup.LOGGER.error("An exception occurred while trying to decompress file: " + file.getName(), e);
} }
} }
} }

View File

@ -1,3 +1,21 @@
/*
A simple backup mod for Fabric
Copyright (C) 2020 Szum123321
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.restore.decompressors; package net.szum123321.textile_backup.core.restore.decompressors;
import net.szum123321.textile_backup.TextileBackup; import net.szum123321.textile_backup.TextileBackup;
@ -12,17 +30,18 @@ import java.time.Duration;
import java.time.Instant; import java.time.Instant;
public class ZipDecompressor { public class ZipDecompressor {
public static void decompress(File archiveFile, File target) { public static void decompress(FileInputStream fileInputStream, File target) {
Instant start = Instant.now(); Instant start = Instant.now();
try (FileInputStream inputStream = new FileInputStream(archiveFile); try (BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
ZipArchiveInputStream zipInputStream = new ZipArchiveInputStream((bufferedInputStream))) { ZipArchiveInputStream zipInputStream = new ZipArchiveInputStream((bufferedInputStream))) {
ZipArchiveEntry entry; ZipArchiveEntry entry;
while ((entry = zipInputStream.getNextZipEntry()) != null) { while ((entry = zipInputStream.getNextZipEntry()) != null) {
if(!zipInputStream.canReadEntryData(entry)) if(!zipInputStream.canReadEntryData(entry)){
TextileBackup.LOGGER.warn("Something when wrong while trying to decompress {}", entry.getName());
continue; continue;
}
File file = target.toPath().resolve(entry.getName()).toFile(); File file = target.toPath().resolve(entry.getName()).toFile();
@ -38,7 +57,7 @@ public class ZipDecompressor {
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) { BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
IOUtils.copy(zipInputStream, bufferedOutputStream); IOUtils.copy(zipInputStream, bufferedOutputStream);
} catch (IOException e) { } catch (IOException e) {
TextileBackup.LOGGER.error("An exception occurred while trying to compress file: " + file.getName(), e); TextileBackup.LOGGER.error("An exception occurred while trying to decompress file: " + file.getName(), e);
} }
} }
} }

View File

@ -19,6 +19,7 @@
package net.szum123321.textile_backup.mixin; package net.szum123321.textile_backup.mixin;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.szum123321.textile_backup.LivingServer;
import net.szum123321.textile_backup.TextileBackup; import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.core.create.BackupContext; import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.core.create.BackupHelper; import net.szum123321.textile_backup.core.create.BackupHelper;
@ -28,10 +29,12 @@ import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(MinecraftServer.class) @Mixin(MinecraftServer.class)
public abstract class MinecraftServerMixin { public class MinecraftServerMixin implements LivingServer {
private boolean isAlive = true;
@Inject(method = "shutdown", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/server/MinecraftServer;save(ZZZ)Z")) @Inject(method = "shutdown", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/server/MinecraftServer;save(ZZZ)Z"))
public void onFinalWorldSave(CallbackInfo ci) { public void onFinalWorldSave(CallbackInfo ci) {
if (TextileBackup.config.shutdownBackup && TextileBackup.globalShutdownBackupFlag.get()) if (TextileBackup.CONFIG.shutdownBackup && TextileBackup.globalShutdownBackupFlag.get())
TextileBackup.executorService.submit( TextileBackup.executorService.submit(
BackupHelper.create( BackupHelper.create(
new BackupContext.Builder() new BackupContext.Builder()
@ -41,5 +44,12 @@ public abstract class MinecraftServerMixin {
.build() .build()
) )
); );
isAlive = false;
}
@Override
public boolean isAlive() {
return isAlive;
} }
} }