Merge pull request #111 from Szum123321/refactor_2022

Refactor 2022
This commit is contained in:
Szum123321 2022-11-06 11:05:22 +01:00 committed by GitHub
commit 64e8e06161
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 937 additions and 738 deletions

3
DEV_NOTES Normal file
View File

@ -0,0 +1,3 @@
NoClassDefFoundError in dev:
- Intellij appears to add Common Compress into the excluded path in dev environment.
To repair this go to Edit Configuration, select either server or client and remove common compress from Modify Classpath

View File

@ -1,5 +1,5 @@
plugins {
id 'fabric-loom' version '0.12-SNAPSHOT'
id 'fabric-loom' version '1.0-SNAPSHOT'
id 'maven-publish'
}
@ -10,16 +10,22 @@ archivesBaseName = project.archives_base_name
version = "${project.mod_version}-${getMcMinor(project.minecraft_version)}"
group = project.maven_group
repositories{
repositories {
maven { url 'https://jitpack.io' }
maven { url "https://maven.shedaniel.me/" }
maven {
url "https://maven.terraformersmc.com/releases/"
content {
includeGroup "com.terraformersmc"
maven { url "https://maven.terraformersmc.com/releases/" }
mavenCentral()
}
loom {
runs {
testServer {
server()
ideConfigGenerated project.rootProject == project
name = "Testmod Server"
source sourceSets.test
}
}
mavenCentral()
}
dependencies {
@ -40,15 +46,15 @@ dependencies {
modImplementation("com.terraformersmc:modmenu:${project.modmenu_version}")
//General compression library
modImplementation "org.apache.commons:commons-compress:1.21"
implementation "org.apache.commons:commons-compress:1.21"
include "org.apache.commons:commons-compress:1.21"
//LZMA support
modImplementation 'org.tukaani:xz:1.9'
implementation 'org.tukaani:xz:1.9'
include "org.tukaani:xz:1.9"
//Gzip compression, parallel, GITHUB
modImplementation "com.github.shevek:parallelgzip:${project.pgzip_commit_hash}"
implementation "com.github.shevek:parallelgzip:${project.pgzip_commit_hash}"
include "com.github.shevek:parallelgzip:${project.pgzip_commit_hash}"
// Lazy DFU makes the dev env start up much faster by loading DataFixerUpper lazily, which would otherwise take a long time. We rarely need it anyway.

View File

@ -2,17 +2,17 @@
org.gradle.jvmargs=-Xmx1G
minecraft_version=1.19.2
yarn_mappings=1.19.2+build.8
loader_version=0.14.9
yarn_mappings=1.19.2+build.28
loader_version=0.14.10
#Fabric api
fabric_version=0.60.0+1.19.2
fabric_version=0.64.0+1.19.2
#Cloth Config
cloth_version=8.0.75
cloth_version=8.2.88
#ModMenu
modmenu_version=4.0.5
modmenu_version=4.1.0
#Lazy DFU for faster dev start
lazydfu_version=v0.1.3
@ -21,6 +21,6 @@ lazydfu_version=v0.1.3
pgzip_commit_hash=af5f5c297e735f3f2df7aa4eb0e19a5810b8aff6
# Mod Properties
mod_version = 2.4.0
mod_version = 2.5.0
maven_group = net.szum123321
archives_base_name = textile_backup

Binary file not shown.

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

270
gradlew vendored
View File

@ -1,7 +1,7 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -17,78 +17,113 @@
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@ -105,84 +140,95 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

25
gradlew.bat vendored
View File

@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -51,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@ -61,28 +64,14 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell

View File

@ -1,10 +1,10 @@
pluginManagement {
repositories {
jcenter()
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
mavenCentral()
gradlePluginPortal()
}
}

View File

@ -0,0 +1,106 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 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 net.minecraft.server.MinecraftServer;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.create.MakeBackupRunnable;
import net.szum123321.textile_backup.core.restore.AwaitThread;
import org.apache.commons.io.FileUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class Globals {
public static final Globals INSTANCE = new Globals();
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public final static DateTimeFormatter defaultDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss");
private ExecutorService executorService = null;// = Executors.newSingleThreadExecutor();
public final AtomicBoolean globalShutdownBackupFlag = new AtomicBoolean(true);
public boolean disableWatchdog = false;
private boolean disableTMPFiles = false;
private AwaitThread restoreAwaitThread = null;
private Path lockedPath = null;
private Globals() {}
public ExecutorService getQueueExecutor() { return executorService; }
public void resetQueueExecutor() {
if(Objects.nonNull(executorService) && !executorService.isShutdown()) return;
executorService = Executors.newSingleThreadExecutor();
}
public void shutdownQueueExecutor(long timeout) {
if(executorService.isShutdown()) return;
executorService.shutdown();
try {
if(!executorService.awaitTermination(timeout, TimeUnit.MICROSECONDS)) {
log.error("Timeout occurred while waiting for currently running backups to finish!");
executorService.shutdownNow().stream()
.filter(r -> r instanceof MakeBackupRunnable)
.map(r -> (MakeBackupRunnable)r)
.forEach(r -> log.error("Dropping: {}", r.toString()));
if(!executorService.awaitTermination(1000, TimeUnit.MICROSECONDS))
log.error("Couldn't shut down the executor!");
}
} catch (InterruptedException e) {
log.error("An exception occurred!", e);
}
}
public Optional<AwaitThread> getAwaitThread() { return Optional.ofNullable(restoreAwaitThread); }
public void setAwaitThread(AwaitThread th) { restoreAwaitThread = th; }
public Optional<Path> getLockedFile() { return Optional.ofNullable(lockedPath); }
public void setLockedFile(Path p) { lockedPath = p; }
public boolean disableTMPFS() { return disableTMPFiles; }
public void updateTMPFSFlag(MinecraftServer server) {
disableTMPFiles = false;
Path tmp_dir = Path.of(System.getProperty("java.io.tmpdir"));
if(
FileUtils.sizeOfDirectory(Utilities.getWorldFolder(server).toFile()) >=
tmp_dir.toFile().getUsableSpace()
) {
log.error("Not enough space left in TMP directory! ({})", tmp_dir);
disableTMPFiles = true;
}
if(!Files.isWritable(tmp_dir)) {
log.error("TMP filesystem ({}) is read-only!", tmp_dir);
disableTMPFiles = true;
}
if(disableTMPFiles) log.error("Might cause: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems");
}
}

View File

@ -1,40 +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 net.szum123321.textile_backup.core.restore.AwaitThread;
import java.nio.file.Path;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
public class Statics {
public final static DateTimeFormatter defaultDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss");
public static ExecutorService executorService = Executors.newSingleThreadExecutor();
public static final AtomicBoolean globalShutdownBackupFlag = new AtomicBoolean(true);
public static boolean disableWatchdog = false;
public static AwaitThread restoreAwaitThread = null;
public static Optional<Path> untouchableFile = Optional.empty();
public static boolean disableTMPFiles = false;
}

View File

@ -38,12 +38,9 @@ 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.concurrent.Executors;
import net.szum123321.textile_backup.core.create.MakeBackupRunnableFactory;
public class TextileBackup implements ModInitializer {
public static final String MOD_NAME = "Textile Backup";
@ -58,20 +55,20 @@ public class TextileBackup implements ModInitializer {
ConfigHelper.updateInstance(AutoConfig.register(ConfigPOJO.class, JanksonConfigSerializer::new));
ServerTickEvents.END_SERVER_TICK.register(new BackupScheduler()::tick);
ServerTickEvents.END_SERVER_TICK.register(BackupScheduler::tick);
//Restart Executor Service in single-player
ServerLifecycleEvents.SERVER_STARTING.register(server -> {
if(Statics.executorService.isShutdown()) Statics.executorService = Executors.newSingleThreadExecutor();
Utilities.updateTMPFSFlag(server);
Globals.INSTANCE.resetQueueExecutor();
Globals.INSTANCE.updateTMPFSFlag(server);
});
//Wait 60s for already submited backups to finish. After that kill the bastards and run the one last if required
ServerLifecycleEvents.SERVER_STOPPED.register(server -> {
Statics.executorService.shutdown();
Globals.INSTANCE.shutdownQueueExecutor(60000);
if (config.get().shutdownBackup && Statics.globalShutdownBackupFlag.get()) {
BackupHelper.create(
if (config.get().shutdownBackup && Globals.INSTANCE.globalShutdownBackupFlag.get()) {
MakeBackupRunnableFactory.create(
BackupContext.Builder
.newBackupContextBuilder()
.setServer(server)

View File

@ -20,12 +20,12 @@ package net.szum123321.textile_backup.commands;
import com.mojang.brigadier.LiteralMessage;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.core.RestoreableFile;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.restore.RestoreHelper;
@ -34,33 +34,40 @@ import java.util.concurrent.CompletableFuture;
public final class FileSuggestionProvider implements SuggestionProvider<ServerCommandSource> {
private static final FileSuggestionProvider INSTANCE = new FileSuggestionProvider();
public static FileSuggestionProvider Instance() {
return INSTANCE;
}
public static FileSuggestionProvider Instance() { return INSTANCE; }
@Override
public CompletableFuture<Suggestions> getSuggestions(CommandContext<ServerCommandSource> ctx, SuggestionsBuilder builder) throws CommandSyntaxException {
public CompletableFuture<Suggestions> getSuggestions(CommandContext<ServerCommandSource> ctx, SuggestionsBuilder builder) {
String remaining = builder.getRemaining();
for (RestoreHelper.RestoreableFile file : RestoreHelper.getAvailableBackups(ctx.getSource().getServer())) {
String formattedCreationTime = file.getCreationTime().format(Statics.defaultDateTimeFormatter);
var files = RestoreHelper.getAvailableBackups(ctx.getSource().getServer());
for (RestoreableFile file: files) {
String formattedCreationTime = file.getCreationTime().format(Globals.defaultDateTimeFormatter);
if (formattedCreationTime.startsWith(remaining)) {
if (Utilities.wasSentByPlayer(ctx.getSource())) { //was typed by player
if (file.getComment() != null) {
builder.suggest(formattedCreationTime, new LiteralMessage("Comment: " + file.getComment()));
if (file.getComment().isPresent()) {
builder.suggest(formattedCreationTime, new LiteralMessage("Comment: " + file.getComment().get()));
} else {
builder.suggest(formattedCreationTime);
}
} else { //was typed from server console
if (file.getComment() != null) {
builder.suggest(file.getCreationTime() + "#" + file.getComment());
if (file.getComment().isPresent()) {
builder.suggest(file.getCreationTime() + "#" + file.getComment().get());
} else {
builder.suggest(formattedCreationTime);
}
}
}
}
if("latest".startsWith(remaining) && !files.isEmpty()) //suggest latest
builder.suggest("latest", new LiteralMessage (
files.getLast().getCreationTime().format(Globals.defaultDateTimeFormatter) +
(files.getLast().getComment().map(s -> "#" + s).orElse("")))
);
return builder.buildFuture();
}
}

View File

@ -23,7 +23,7 @@ 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.core.create.BackupHelper;
import net.szum123321.textile_backup.core.Cleanup;
import net.szum123321.textile_backup.core.Utilities;
public class CleanupCommand {
@ -38,7 +38,7 @@ public class CleanupCommand {
log.sendInfo(
source,
"Deleted: {} files.",
BackupHelper.executeFileLimit(source, Utilities.getLevelName(source.getServer()))
new Cleanup(source, Utilities.getLevelName(source.getServer())).call()
);
return 1;

View File

@ -22,11 +22,11 @@ import com.mojang.brigadier.arguments.StringArgumentType;
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.Globals;
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 net.szum123321.textile_backup.core.create.MakeBackupRunnableFactory;
import javax.annotation.Nullable;
@ -41,23 +41,21 @@ public class StartBackupCommand {
}
private static int execute(ServerCommandSource source, @Nullable String comment) {
if(!Statics.executorService.isShutdown()) {
try {
Statics.executorService.submit(
BackupHelper.create(
BackupContext.Builder
.newBackupContextBuilder()
.setCommandSource(source)
.setComment(comment)
.guessInitiator()
.saveServer()
.build()
)
);
} catch (Exception e) {
log.error("Something went wrong while executing command!", e);
throw e;
}
try {
Globals.INSTANCE.getQueueExecutor().submit(
MakeBackupRunnableFactory.create(
BackupContext.Builder
.newBackupContextBuilder()
.setCommandSource(source)
.setComment(comment)
.guessInitiator()
.saveServer()
.build()
)
);
} catch (Exception e) {
log.error("Something went wrong while executing command!", e);
throw e;
}
return 1;

View File

@ -23,11 +23,12 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Globals;
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;
import net.szum123321.textile_backup.core.RestoreableFile;
import net.szum123321.textile_backup.core.Utilities;
import java.io.IOException;
@ -35,7 +36,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
import java.util.stream.Stream;
import java.util.Optional;
public class DeleteCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
@ -52,37 +53,36 @@ public class DeleteCommand {
LocalDateTime dateTime;
try {
dateTime = LocalDateTime.from(Statics.defaultDateTimeFormatter.parse(fileName));
dateTime = LocalDateTime.from(Globals.defaultDateTimeFormatter.parse(fileName));
} catch (DateTimeParseException e) {
throw CommandExceptions.DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE.create(e);
}
Path root = Utilities.getBackupRootPath(Utilities.getLevelName(source.getServer()));
try (Stream<Path> stream = Files.list(root)) {
stream.filter(Utilities::isValidBackup)
.filter(file -> Utilities.getFileCreationTime(file).orElse(LocalDateTime.MIN).equals(dateTime))
.findFirst().ifPresent(file -> {
if(Statics.untouchableFile.isEmpty() || !Statics.untouchableFile.get().equals(file)) {
try {
Files.delete(file);
log.sendInfo(source, "File {} successfully deleted!", file);
RestoreableFile.applyOnFiles(root, Optional.empty(),
e -> log.sendErrorAL(source, "An exception occurred while trying to delete a file!", e),
stream -> stream.filter(f -> f.getCreationTime().equals(dateTime)).map(RestoreableFile::getFile).findFirst()
).ifPresentOrElse(file -> {
if(Globals.INSTANCE.getLockedFile().filter(p -> p == file).isEmpty()) {
try {
Files.delete((Path) file);
log.sendInfo(source, "File {} successfully deleted!", file);
if(Utilities.wasSentByPlayer(source))
log.info("Player {} deleted {}.", source.getPlayer().getName(), file);
} catch (IOException e) {
log.sendError(source, "Something went wrong while deleting file!");
}
} else {
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");
if(Utilities.wasSentByPlayer(source))
log.info("Player {} deleted {}.", source.getPlayer().getName(), file);
} catch (IOException e) {
log.sendError(source, "Something went wrong while deleting file!");
}
});
} catch (IOException ignored) {
log.sendError(source, "Couldn't find file by this name.");
log.sendHint(source, "Maybe try /backup list");
}
} else {
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");
}
}, () -> {
log.sendInfo(source, "Couldn't find file by this name.");
log.sendInfo(source, "Maybe try /backup list");
}
);
return 0;
}
}

View File

@ -23,6 +23,7 @@ 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.core.RestoreableFile;
import net.szum123321.textile_backup.core.restore.RestoreHelper;
import java.util.*;
@ -33,7 +34,7 @@ public class ListBackupsCommand {
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("list")
.executes(ctx -> { StringBuilder builder = new StringBuilder();
List<RestoreHelper.RestoreableFile> backups = RestoreHelper.getAvailableBackups(ctx.getSource().getServer());
var backups = RestoreHelper.getAvailableBackups(ctx.getSource().getServer());
if(backups.size() == 0) {
builder.append("There a no backups available for this world.");
@ -42,7 +43,7 @@ public class ListBackupsCommand {
builder.append(backups.get(0).toString());
} else {
backups.sort(null);
Iterator<RestoreHelper.RestoreableFile> iterator = backups.iterator();
Iterator<RestoreableFile> iterator = backups.iterator();
builder.append("Available backups:\n");
builder.append(iterator.next());

View File

@ -21,33 +21,36 @@ package net.szum123321.textile_backup.commands.restore;
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.Globals;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.Utilities;
import java.util.Optional;
import net.szum123321.textile_backup.core.restore.AwaitThread;
public class KillRestoreCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("killR")
.executes(ctx -> {
if(Statics.restoreAwaitThread != null && Statics.restoreAwaitThread.isAlive()) {
Statics.restoreAwaitThread.interrupt();
Statics.globalShutdownBackupFlag.set(true);
Statics.untouchableFile = Optional.empty();
log.info("{} cancelled backup restoration.", Utilities.wasSentByPlayer(ctx.getSource()) ?
"Player: " + ctx.getSource().getName() :
"SERVER"
);
if(Utilities.wasSentByPlayer(ctx.getSource()))
log.sendInfo(ctx.getSource(), "Backup restoration successfully stopped.");
} else {
if(Globals.INSTANCE.getAwaitThread().filter(Thread::isAlive).isEmpty()) {
log.sendInfo(ctx.getSource(), "Failed to stop backup restoration");
return -1;
}
AwaitThread thread = Globals.INSTANCE.getAwaitThread().get();
thread.interrupt();
Globals.INSTANCE.globalShutdownBackupFlag.set(true);
Globals.INSTANCE.setLockedFile(null);
log.info("{} cancelled backup restoration.", Utilities.wasSentByPlayer(ctx.getSource()) ?
"Player: " + ctx.getSource().getName() :
"SERVER"
);
if(Utilities.wasSentByPlayer(ctx.getSource()))
log.sendInfo(ctx.getSource(), "Backup restoration successfully stopped.");
return 1;
});
}

View File

@ -24,17 +24,19 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Globals;
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;
import net.szum123321.textile_backup.core.RestoreableFile;
import net.szum123321.textile_backup.core.restore.RestoreContext;
import net.szum123321.textile_backup.core.restore.RestoreHelper;
import javax.annotation.Nullable;
import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
import java.util.Objects;
import java.util.Optional;
public class RestoreBackupCommand {
@ -64,47 +66,54 @@ public class RestoreBackupCommand {
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");
log.sendInfo(source, "You may also type '/backup restore latest' to restore the freshest backup");
return 1;
});
}
private static int execute(String file, @Nullable String comment, ServerCommandSource source) throws CommandSyntaxException {
if(Statics.restoreAwaitThread == null || (Statics.restoreAwaitThread != null && !Statics.restoreAwaitThread.isAlive())) {
LocalDateTime dateTime;
if(Globals.INSTANCE.getAwaitThread().filter(Thread::isAlive).isPresent()) {
log.sendInfo(source, "Someone has already started another restoration.");
return -1;
}
LocalDateTime dateTime;
Optional<RestoreableFile> backupFile;
if(Objects.equals(file, "latest")) {
backupFile = RestoreHelper.getLatestAndLockIfPresent(source.getServer());
dateTime = backupFile.map(RestoreableFile::getCreationTime).orElse(LocalDateTime.now());
} else {
try {
dateTime = LocalDateTime.from(Statics.defaultDateTimeFormatter.parse(file));
dateTime = LocalDateTime.from(Globals.defaultDateTimeFormatter.parse(file));
} catch (DateTimeParseException e) {
throw CommandExceptions.DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE.create(e);
}
Optional<RestoreHelper.RestoreableFile> backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getServer());
backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getServer());
}
if(backupFile.isPresent()) {
log.info("Found file to restore {}", backupFile.get().getFile().getFileName().toString());
} else {
log.sendInfo(source, "No file created on {} was found!", dateTime.format(Statics.defaultDateTimeFormatter));
if(backupFile.isEmpty()) {
log.sendInfo(source, "No file created on {} was found!", dateTime.format(Globals.defaultDateTimeFormatter));
return -1;
} else {
log.info("Found file to restore {}", backupFile.get().getFile().getFileName().toString());
return 0;
}
Statics.restoreAwaitThread = RestoreHelper.create(
RestoreContext.Builder.newRestoreContextBuilder()
.setCommandSource(source)
.setFile(backupFile.get())
.setComment(comment)
.build()
Globals.INSTANCE.setAwaitThread(
RestoreHelper.create(
RestoreContext.Builder.newRestoreContextBuilder()
.setCommandSource(source)
.setFile(backupFile.get())
.setComment(comment)
.build()
)
);
Statics.restoreAwaitThread.start();
Globals.INSTANCE.getAwaitThread().get().start();
return 1;
} else {
log.sendInfo(source, "Someone has already started another restoration.");
return 0;
}
}
}

View File

@ -18,6 +18,7 @@
package net.szum123321.textile_backup.config;
import blue.endless.jankson.annotation.SerializedName;
import me.shedaniel.autoconfig.ConfigData;
import me.shedaniel.autoconfig.annotation.Config;
import me.shedaniel.autoconfig.annotation.ConfigEntry;
@ -63,8 +64,9 @@ public class ConfigPOJO implements ConfigData {
public boolean backupOldWorlds = true;
@Comment("\nA path to the backup folder\n")
@SerializedName("path")
@ConfigEntry.Gui.NoTooltip()
public String path = "backup/";
public String backupDirectoryPath = "backup/";
@Comment("""
\nThis setting allows you to exclude files form being backed-up.

View File

@ -18,10 +18,13 @@
package net.szum123321.textile_backup.core;
/**
* Enum representing possible sources of action
*/
public enum ActionInitiator {
Player("Player", "by"),
ServerConsole("Server Console", "from"),
Timer("Timer", "by"),
ServerConsole("Server Console", "from"), //some/ting typed a command and it was not a player (command blocks and server console count)
Timer("Timer", "by"), //a.k.a scheduler
Shutdown("Server Shutdown", "by"),
Restore("Backup Restoration", "because of"),
Null("Null (That shouldn't have happened)", "form");

View File

@ -0,0 +1,138 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
package net.szum123321.textile_backup.core;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.stream.Stream;
/**
* Utility used for removing old backups
*/
public class Cleanup implements Callable<Integer> {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
private final ServerCommandSource ctx;
private final String worldName;
public Cleanup(ServerCommandSource ctx, String worldName) {
this.ctx = ctx;
this.worldName = worldName;
}
public Integer call() {
Path root = Utilities.getBackupRootPath(worldName);
int deletedFiles = 0;
if (!Files.isDirectory(root) || !Files.exists(root) || isEmpty(root)) return 0;
if (config.get().maxAge > 0) { // delete files older that configured
final long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
deletedFiles += RestoreableFile.applyOnFiles(root, 0L,
e -> log.error("An exception occurred while trying to delete old files!", e),
stream -> stream.filter(f -> now - f.getCreationTime().toEpochSecond(ZoneOffset.UTC) > config.get().maxAge)
.filter(f -> deleteFile(f.getFile(), ctx))
.count()
);
}
final int noToKeep = config.get().backupsToKeep > 0 ? config.get().backupsToKeep : Integer.MAX_VALUE;
final long maxSize = config.get().maxSize > 0 ? config.get().maxSize * 1024: Long.MAX_VALUE; //max number of bytes to keep
long[] counts = count(root);
long n = counts[0], size = counts[1];
var it = RestoreableFile.applyOnFiles(root, null,
e -> log.error("An exception occurred while trying to delete old files!", e),
s -> s.sorted().toList().iterator());
if(Objects.isNull(it)) return deletedFiles;
while(it.hasNext() && (n > noToKeep || size > maxSize)) {
Path f = it.next().getFile();
long x;
try {
x = Files.size(f);
} catch (IOException e) { size = 0; continue; }
if(!deleteFile(f, ctx)) continue;
size -= x;
n--;
deletedFiles++;
}
return deletedFiles;
}
private long[] count(Path root) {
long n = 0, size = 0;
try(Stream<Path> stream = Files.list(root)) {
var it = stream.flatMap(f -> RestoreableFile.build(f).stream()).iterator();
while(it.hasNext()) {
var f = it.next();
try {
size += Files.size(f.getFile());
} catch (IOException e) {
log.error("Couldn't get size of " + f.getFile(), e);
continue;
}
n++;
}
} catch (IOException e) {
log.error("Error while counting files!", e);
}
return new long[]{n, size};
}
private boolean isEmpty(Path root) {
if (!Files.isDirectory(root)) return false;
return RestoreableFile.applyOnFiles(root, false, e -> {}, s -> s.findFirst().isEmpty());
}
//1 -> ok, 0 -> err
private boolean deleteFile(Path f, ServerCommandSource ctx) {
if(Globals.INSTANCE.getLockedFile().filter(p -> p == f).isPresent()) return false;
try {
Files.delete(f);
log.sendInfoAL(ctx, "Deleted: {}", f);
} catch (IOException e) {
if(Utilities.wasSentByPlayer(ctx)) log.sendError(ctx, "Something went wrong while deleting: {}.", f);
log.error("Something went wrong while deleting: {}.", f, e);
return false;
}
return true;
}
}

View File

@ -0,0 +1,122 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
package net.szum123321.textile_backup.core;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.config.ConfigPOJO;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
/**
* This class parses backup files, extracting its creation time, format and possibly comment
*/
public class RestoreableFile implements Comparable<RestoreableFile> {
private final Path file;
private final ConfigPOJO.ArchiveFormat archiveFormat;
private final LocalDateTime creationTime;
private final String comment;
private RestoreableFile(Path file, ConfigPOJO.ArchiveFormat archiveFormat, LocalDateTime creationTime, String comment) {
this.file = file;
this.archiveFormat = archiveFormat;
this.creationTime = creationTime;
this.comment = comment;
}
//removes repetition of the files stream thingy with awfully large lambdas
public static <T> T applyOnFiles(Path root, T def, Consumer<IOException> errorConsumer, Function<Stream<RestoreableFile>, T> streamConsumer) {
try (Stream<Path> stream = Files.list(root)) {
return streamConsumer.apply(stream.flatMap(f -> RestoreableFile.build(f).stream()));
} catch (IOException e) {
errorConsumer.accept(e);
}
return def;
}
public static Optional<RestoreableFile> build(Path file) throws NoSuchElementException {
if(!Files.exists(file) || !Files.isRegularFile(file)) return Optional.empty();
String filename = file.getFileName().toString();
var format = Arrays.stream(ConfigPOJO.ArchiveFormat.values())
.filter(f -> filename.endsWith(f.getCompleteString()))
.findAny()
.orElse(null);
if(Objects.isNull(format)) return Optional.empty();
int parsed_pos = filename.length() - format.getCompleteString().length();
String comment = null;
if(filename.contains("#")) {
comment = filename.substring(filename.indexOf("#") + 1, parsed_pos);
parsed_pos -= comment.length() + 1;
}
var time_string = filename.substring(0, parsed_pos);
try {
return Optional.of(new RestoreableFile(file, format, LocalDateTime.from(Utilities.getDateTimeFormatter().parse(time_string)), comment));
} catch (Exception ignored) {}
try {
return Optional.of(new RestoreableFile(file, format, LocalDateTime.from(Globals.defaultDateTimeFormatter.parse(time_string)), comment));
} catch (Exception ignored) {}
try {
FileTime fileTime = Files.readAttributes(file, BasicFileAttributes.class, NOFOLLOW_LINKS).creationTime();
return Optional.of(new RestoreableFile(file, format, LocalDateTime.ofInstant(fileTime.toInstant(), ZoneOffset.systemDefault()), comment));
} catch (IOException ignored) {}
return Optional.empty();
}
public Path getFile() { return file; }
public ConfigPOJO.ArchiveFormat getArchiveFormat() { return archiveFormat; }
public LocalDateTime getCreationTime() { return creationTime; }
public Optional<String> getComment() { return Optional.ofNullable(comment); }
@Override
public int compareTo(@NotNull RestoreableFile o) { return creationTime.compareTo(o.creationTime); }
public String toString() {
return this.getCreationTime().format(Globals.defaultDateTimeFormatter) + (comment != null ? "#" + comment : "");
}
}

View File

@ -28,34 +28,24 @@ import net.minecraft.world.World;
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.mixin.MinecraftServerSessionAccessor;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.file.SimplePathVisitor;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Optional;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
public class Utilities {
private final static ConfigHelper config = ConfigHelper.INSTANCE;
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static boolean wasSentByPlayer(ServerCommandSource source) {
return source.isExecutedByPlayer();
}
//I'm keeping this wrapper function for easier backporting
public static boolean wasSentByPlayer(ServerCommandSource source) { return source.isExecutedByPlayer(); }
public static void notifyPlayers(@NotNull MinecraftServer server, String msg) {
MutableText message = log.getPrefixText();
@ -73,20 +63,6 @@ public class Utilities {
.getSession()
.getWorldDirectory(World.OVERWORLD);
}
public static Path getBackupRootPath(String worldName) {
Path path = Path.of(config.get().path).toAbsolutePath();
if (config.get().perWorldBackup) path = path.resolve(worldName);
try {
Files.createDirectories(path);
} catch (IOException e) {
//I REALLY shouldn't be handling this here
}
return path;
}
public static void deleteDirectory(Path path) throws IOException {
Files.walkFileTree(path, new SimplePathVisitor() {
@ -104,25 +80,6 @@ public class Utilities {
});
}
public static void updateTMPFSFlag(MinecraftServer server) {
boolean flag = false;
Path tmp_dir = Path.of(System.getProperty("java.io.tmpdir"));
if(
FileUtils.sizeOfDirectory(Utilities.getWorldFolder(server).toFile()) >=
tmp_dir.toFile().getUsableSpace()
) {
log.error("Not enough space left in TMP directory! ({})", tmp_dir);
flag = true;
}
//!Files.isWritable(tmp_dir.resolve("test_txb_file_2137")) - Unsure why this was resolving to a file that isn't being created (at least not in Windows)
if(!Files.isWritable(tmp_dir)) {
log.error("TMP filesystem ({}) is read-only!", tmp_dir);
flag = true;
}
if((Statics.disableTMPFiles = flag)) log.error("Might cause: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems");
}
public static void disableWorldSaving(MinecraftServer server) {
for (ServerWorld serverWorld : server.getWorlds()) {
if (serverWorld != null && !serverWorld.savingDisabled)
@ -141,6 +98,22 @@ public class Utilities {
return System.getProperty("os.name").toLowerCase().contains("win");
}
public static Path getBackupRootPath(String worldName) {
Path path = Path.of(config.get().backupDirectoryPath).toAbsolutePath();
if (config.get().perWorldBackup) path = path.resolve(worldName);
if(Files.notExists(path)) {
try {
Files.createDirectories(path);
} catch (IOException e) {
//I REALLY shouldn't be handling this here
}
}
return path;
}
public static boolean isBlacklisted(Path path) {
if(isWindows()) { //hotfix!
if (path.getFileName().toString().equals("session.lock")) return true;
@ -149,66 +122,10 @@ public class Utilities {
return config.get().fileBlacklist.stream().anyMatch(path::startsWith);
}
public static Optional<ConfigPOJO.ArchiveFormat> getArchiveExtension(String fileName) {
String[] parts = fileName.split("\\.");
return Arrays.stream(ConfigPOJO.ArchiveFormat.values())
.filter(format -> format.getLastPiece().equals(parts[parts.length - 1]))
.findAny();
}
public static Optional<ConfigPOJO.ArchiveFormat> getArchiveExtension(Path f) {
return getArchiveExtension(f.getFileName().toString());
}
public static Optional<LocalDateTime> getFileCreationTime(Path file) {
if(getArchiveExtension(file).isEmpty()) return Optional.empty();
try {
FileTime fileTime = Files.readAttributes(file, BasicFileAttributes.class, NOFOLLOW_LINKS).creationTime();
return Optional.of(LocalDateTime.ofInstant(fileTime.toInstant(), ZoneOffset.systemDefault()));
} catch (IOException ignored) {}
String fileExtension = getArchiveExtension(file).get().getCompleteString();
try {
return Optional.of(
LocalDateTime.from(
Utilities.getDateTimeFormatter().parse(
file.getFileName().toString().split(fileExtension)[0].split("#")[0]
)));
} catch (Exception ignored) {}
try {
return Optional.of(
LocalDateTime.from(
Utilities.getBackupDateTimeFormatter().parse(
file.getFileName().toString().split(fileExtension)[0].split("#")[0]
)));
} catch (Exception ignored) {}
return Optional.empty();
}
public static boolean isValidBackup(Path f) {
return getArchiveExtension(f).isPresent() && getFileCreationTime(f).isPresent() && isFileOk(f);
}
public static boolean isFileOk(File f) {
return f.exists() && f.isFile();
}
public static boolean isFileOk(Path f) {
return Files.exists(f) && Files.isRegularFile(f);
}
public static DateTimeFormatter getDateTimeFormatter() {
return DateTimeFormatter.ofPattern(config.get().dateTimeFormat);
}
public static DateTimeFormatter getBackupDateTimeFormatter() {
return Statics.defaultDateTimeFormatter;
}
public static String formatDuration(Duration duration) {
DateTimeFormatter formatter;

View File

@ -21,12 +21,9 @@ package net.szum123321.textile_backup.core.create;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.util.Util;
import net.szum123321.textile_backup.core.ActionInitiator;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public record BackupContext(@NotNull MinecraftServer server,
ServerCommandSource commandSource,
ActionInitiator initiator,
@ -103,8 +100,7 @@ public record BackupContext(@NotNull MinecraftServer server,
if (server == null) {
if (commandSource != null) setServer(commandSource.getServer());
else
throw new RuntimeException("Neither MinecraftServer or ServerCommandSource were provided!");
else throw new RuntimeException("Neither MinecraftServer or ServerCommandSource were provided!");
}
return new BackupContext(server, commandSource, initiator, save, comment);

View File

@ -1,189 +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.core.create;
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.config.ConfigHelper;
import net.szum123321.textile_backup.core.Utilities;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Comparator;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Stream;
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) {
if(config.get().broadcastBackupStart) {
Utilities.notifyPlayers(ctx.server(),
"Warning! Server backup will begin shortly. You may experience some lag."
);
} else {
log.sendInfoAL(ctx, "Warning! Server backup will begin shortly. You may experience some lag.");
}
StringBuilder builder = new StringBuilder();
builder.append("Backup started ");
builder.append(ctx.initiator().getPrefix());
if(ctx.startedByPlayer())
builder.append(ctx.commandSource().getDisplayName().getString());
else
builder.append(ctx.initiator().getName());
builder.append(" on: ");
builder.append(Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
log.info(builder.toString());
if (ctx.shouldSave()) {
log.sendInfoAL(ctx, "Saving server...");
ctx.server().getPlayerManager().saveAllPlayerData();
try {
ctx.server().save(false, true, true);
} catch (Exception e) {
log.sendErrorAL(ctx,"An exception occurred when trying to save the world!");
}
}
return new MakeBackupRunnable(ctx);
}
public static int executeFileLimit(ServerCommandSource ctx, String worldName) {
Path root = Utilities.getBackupRootPath(worldName);
int deletedFiles = 0;
if (Files.isDirectory(root) && Files.exists(root) && !isEmpty(root)) {
if (config.get().maxAge > 0) { // delete files older that configured
final long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
try(Stream<Path> stream = Files.list(root)) {
deletedFiles += stream
.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 - Utilities.getFileCreationTime(f).get().toEpochSecond(ZoneOffset.UTC) > config.get().maxAge)
.mapToInt(f -> deleteFile(f, ctx))
.sum();
} catch (IOException e) {
log.error("An exception occurred while trying to delete old files!", e);
}
}
final int noToKeep = config.get().backupsToKeep > 0 ? config.get().backupsToKeep : Integer.MAX_VALUE;
final long maxSize = config.get().maxSize > 0 ? config.get().maxSize * 1024: Long.MAX_VALUE;
AtomicInteger currentNo = new AtomicInteger(countBackups(root));
AtomicLong currentSize = new AtomicLong(countSize(root));
try(Stream<Path> stream = Files.list(root)) {
deletedFiles += stream
.filter(Utilities::isValidBackup)
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get()))
.takeWhile(f -> (currentNo.get() > noToKeep) || (currentSize.get() > maxSize))
.peek(f -> {
currentNo.decrementAndGet();
try {
currentSize.addAndGet(-Files.size(f));
} catch (IOException e) {
currentSize.set(0);
}
})
.mapToInt(f -> deleteFile(f, ctx))
.sum();
} catch (IOException e) {
log.error("An exception occurred while trying to delete old files!", e);
}
}
return deletedFiles;
}
private static int countBackups(Path path) {
try(Stream<Path> stream = Files.list(path)) {
return (int) stream
.filter(Utilities::isValidBackup)
.count();
} catch (IOException e) {
log.error("Error while counting files!", e);
}
return 0;
}
private static long countSize(Path path) {
try(Stream<Path> stream = Files.list(path)) {
return stream
.filter(Utilities::isValidBackup)
.mapToLong(f -> {
try {
return Files.size(f);
} catch (IOException e) {
log.error("Couldn't delete a file!", e);
return 0;
}
})
.sum();
} catch (IOException e) {
log.error("Error while counting files!", e);
}
return 0;
}
private static boolean isEmpty(Path path) {
if (Files.isDirectory(path)) {
try (Stream<Path> entries = Files.list(path)) {
return entries.findFirst().isEmpty();
} catch (IOException e) {
return false;
}
}
return false;
}
//1 -> ok, 0 -> err
private static int deleteFile(Path f, ServerCommandSource ctx) {
if(Statics.untouchableFile.isEmpty()|| !Statics.untouchableFile.get().equals(f)) {
try {
Files.delete(f);
log.sendInfoAL(ctx, "Deleting: {}", f);
} catch (IOException e) {
if(Utilities.wasSentByPlayer(ctx)) log.sendError(ctx, "Something went wrong while deleting: {}.", f);
log.error("Something went wrong while deleting: {}.", f, e);
return 0;
}
return 1;
}
return 0;
}
}

View File

@ -19,32 +19,41 @@
package net.szum123321.textile_backup.core.create;
import net.minecraft.server.MinecraftServer;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.ActionInitiator;
import java.time.Instant;
/**
* Runs backup on a preset interval
* <br><br>
* The important thing to note: <br>
* The decision of whether to do a backup or not is made at the time of scheduling, that is, whenever the <code>nextBackup</code>
* flag is set. This means that even if doBackupsOnEmptyServer=false, the backup that was scheduled with players online will
* still go through. <br>
* It might appear as though there has been made a backup with no players online despite the config. This is the expected behaviour
* <br><br>
* Furthermore, it uses system time
*/
public class BackupScheduler {
private final static ConfigHelper config = ConfigHelper.INSTANCE;
private boolean scheduled;
private long nextBackup;
//Scheduled flag tells whether we have decided to run another backup
private static boolean scheduled = false;
private static long nextBackup = - 1;
public BackupScheduler() {
scheduled = false;
nextBackup = -1;
}
public void tick(MinecraftServer server) {
public static void tick(MinecraftServer server) {
if(config.get().backupInterval < 1) return;
long now = Instant.now().getEpochSecond();
if(config.get().doBackupsOnEmptyServer || server.getPlayerManager().getCurrentPlayerCount() > 0) {
//Either just run backup with no one playing or there's at least one player
if(scheduled) {
if(nextBackup <= now) {
Statics.executorService.submit(
BackupHelper.create(
//It's time to run
Globals.INSTANCE.getQueueExecutor().submit(
MakeBackupRunnableFactory.create(
BackupContext.Builder
.newBackupContextBuilder()
.setServer(server)
@ -57,13 +66,17 @@ public class BackupScheduler {
nextBackup = now + config.get().backupInterval;
}
} else {
//Either server just started or a new player joined after the last backup has finished
//So let's schedule one some time from now
nextBackup = now + config.get().backupInterval;
scheduled = true;
}
} else if(!config.get().doBackupsOnEmptyServer && server.getPlayerManager().getCurrentPlayerCount() == 0) {
//Do the final backup. No one's on-line and doBackupsOnEmptyServer == false
if(scheduled && nextBackup <= now) {
Statics.executorService.submit(
BackupHelper.create(
//Verify we hadn't done the final one, and it's time to do so
Globals.INSTANCE.getQueueExecutor().submit(
MakeBackupRunnableFactory.create(
BackupContext.Builder
.newBackupContextBuilder()
.setServer(server)
@ -77,4 +90,4 @@ public class BackupScheduler {
}
}
}
}
}

View File

@ -18,13 +18,15 @@
package net.szum123321.textile_backup.core.create;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.Globals;
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.Cleanup;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.create.compressors.ParallelZipCompressor;
import net.szum123321.textile_backup.core.create.compressors.ZipCompressor;
import net.szum123321.textile_backup.core.create.compressors.tar.AbstractTarArchiver;
import net.szum123321.textile_backup.core.create.compressors.tar.ParallelBZip2Compressor;
import net.szum123321.textile_backup.core.create.compressors.tar.ParallelGzipCompressor;
@ -36,13 +38,16 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
/**
* The actual object responsible for creating the backup
*/
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){
public MakeBackupRunnable(BackupContext context) {
this.context = context;
}
@ -50,9 +55,9 @@ public class MakeBackupRunnable implements Runnable {
public void run() {
try {
Utilities.disableWorldSaving(context.server());
Statics.disableWatchdog = true;
Globals.INSTANCE.disableWatchdog = true;
Utilities.updateTMPFSFlag(context.server());
Globals.INSTANCE.updateTMPFSFlag(context.server());
log.sendInfoAL(context, "Starting backup");
@ -66,17 +71,8 @@ public class MakeBackupRunnable implements Runnable {
log.trace("Outfile is: {}", outFile);
try {
Files.createDirectories(outFile.getParent());
Files.createFile(outFile);
} catch (IOException e) {
log.error("An exception occurred when trying to create new backup file!", e);
if(context.initiator() == ActionInitiator.Player)
log.sendError(context, "An exception occurred when trying to create new backup file!");
return;
}
Files.createDirectories(outFile.getParent());
Files.createFile(outFile);
int coreCount;
@ -90,12 +86,12 @@ public class MakeBackupRunnable implements Runnable {
switch (config.get().format) {
case ZIP -> {
if (coreCount > 1 && !Statics.disableTMPFiles) {
ParallelZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
if (coreCount > 1 && !Globals.INSTANCE.disableTMPFS()) {
log.trace("Using PARALLEL Zip Compressor. Threads: {}", coreCount);
ParallelZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
} else {
log.trace("Using REGULAR Zip Compressor.");
ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
log.trace("Using REGULAR Zip Compressor. Threads: {}");
}
}
case BZIP2 -> ParallelBZip2Compressor.getInstance().createArchive(world, outFile, context, coreCount);
@ -108,7 +104,7 @@ public class MakeBackupRunnable implements Runnable {
case TAR -> new AbstractTarArchiver().createArchive(world, outFile, context, coreCount);
}
BackupHelper.executeFileLimit(context.commandSource(), Utilities.getLevelName(context.server()));
new Cleanup(context.commandSource(), Utilities.getLevelName(context.server())).call();
if(config.get().broadcastBackupDone) {
Utilities.notifyPlayers(
@ -118,9 +114,15 @@ public class MakeBackupRunnable implements Runnable {
} else {
log.sendInfoAL(context, "Done!");
}
} catch (Throwable e) {
//ExecutorService swallows exception, so I need to catch everything
log.error("An exception occurred when trying to create new backup file!", e);
if(context.initiator() == ActionInitiator.Player)
log.sendError(context, "An exception occurred when trying to create new backup file!");
} finally {
Utilities.enableWorldSaving(context.server());
Statics.disableWatchdog = false;
Globals.INSTANCE.disableWatchdog = false;
}
}

View File

@ -0,0 +1,72 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 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.create;
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.Utilities;
import java.time.LocalDateTime;
public class MakeBackupRunnableFactory {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public static Runnable create(BackupContext ctx) {
if(config.get().broadcastBackupStart) {
Utilities.notifyPlayers(ctx.server(),
"Warning! Server backup will begin shortly. You may experience some lag."
);
} else {
log.sendInfoAL(ctx, "Warning! Server backup will begin shortly. You may experience some lag.");
}
StringBuilder builder = new StringBuilder();
builder.append("Backup started ");
builder.append(ctx.initiator().getPrefix());
if(ctx.startedByPlayer())
builder.append(ctx.commandSource().getDisplayName().getString());
else
builder.append(ctx.initiator().getName());
builder.append(" on: ");
builder.append(Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
log.info(builder.toString());
if (ctx.shouldSave()) {
log.sendInfoAL(ctx, "Saving server...");
ctx.server().getPlayerManager().saveAllPlayerData();
try {
ctx.server().save(false, true, true);
} catch (Exception e) {
log.sendErrorAL(ctx,"An exception occurred when trying to save the world!");
}
}
return new MakeBackupRunnable(ctx);
}
}

View File

@ -33,6 +33,9 @@ import java.time.Instant;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
/**
* Basic abstract class representing directory compressor
*/
public abstract class AbstractCompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
@ -73,7 +76,6 @@ public abstract class AbstractCompressor {
}
} catch (IOException | InterruptedException | ExecutionException e) {
log.error("An exception occurred!", e);
} catch (Exception e) {
if(ctx.initiator() == ActionInitiator.Player)
log.sendError(ctx, "Something went wrong while compressing files!");
} finally {
@ -89,7 +91,7 @@ public abstract class AbstractCompressor {
protected abstract void addEntry(Path file, String entryName, OutputStream arc) throws IOException;
protected void finish(OutputStream arc) throws InterruptedException, ExecutionException, IOException {
//Basically this function is only needed for the ParallelZipCompressor to write out ParallelScatterZipCreator
//This function is only needed for the ParallelZipCompressor to write out ParallelScatterZipCreator
}
protected void close() {

View File

@ -18,16 +18,16 @@
package net.szum123321.textile_backup.core.restore;
import net.szum123321.textile_backup.Globals;
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;
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.MakeBackupRunnableFactory;
import net.szum123321.textile_backup.core.restore.decompressors.GenericTarDecompressor;
import net.szum123321.textile_backup.core.restore.decompressors.ZipDecompressor;
@ -35,6 +35,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
//TODO: Verify backup's validity?
public class RestoreBackupRunnable implements Runnable {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
@ -47,7 +48,7 @@ public class RestoreBackupRunnable implements Runnable {
@Override
public void run() {
Statics.globalShutdownBackupFlag.set(false);
Globals.INSTANCE.globalShutdownBackupFlag.set(false);
log.info("Shutting down server...");
@ -55,9 +56,10 @@ public class RestoreBackupRunnable implements Runnable {
awaitServerShutdown();
if(config.get().backupOldWorlds) {
BackupHelper.create(
MakeBackupRunnableFactory.create(
BackupContext.Builder
.newBackupContextBuilder()
.saveServer()
.setServer(ctx.server())
.setInitiator(ActionInitiator.Restore)
.setComment("Old_World" + (ctx.comment() != null ? "_" + ctx.comment() : ""))
@ -82,7 +84,6 @@ public class RestoreBackupRunnable implements Runnable {
log.info("Deleting old world...");
Utilities.deleteDirectory(worldFile);
Files.move(tmp, worldFile);
if (config.get().deleteOldBackupAfterRestore) {
@ -95,7 +96,7 @@ public class RestoreBackupRunnable implements Runnable {
}
//in case we're playing on client
Statics.globalShutdownBackupFlag.set(true);
Globals.INSTANCE.globalShutdownBackupFlag.set(true);
log.info("Done!");
}

View File

@ -22,16 +22,17 @@ import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.RestoreableFile;
import javax.annotation.Nullable;
public record RestoreContext(RestoreHelper.RestoreableFile restoreableFile,
public record RestoreContext(RestoreableFile restoreableFile,
MinecraftServer server,
@Nullable String comment,
ActionInitiator initiator,
ServerCommandSource commandSource) {
public static final class Builder {
private RestoreHelper.RestoreableFile file;
private RestoreableFile file;
private MinecraftServer server;
private String comment;
private ServerCommandSource serverCommandSource;
@ -43,7 +44,7 @@ public record RestoreContext(RestoreHelper.RestoreableFile restoreableFile,
return new Builder();
}
public Builder setFile(RestoreHelper.RestoreableFile file) {
public Builder setFile(RestoreableFile file) {
this.file = file;
return this;
}

View File

@ -19,22 +19,18 @@
package net.szum123321.textile_backup.core.restore;
import net.minecraft.server.MinecraftServer;
import net.szum123321.textile_backup.Globals;
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.RestoreableFile;
import net.szum123321.textile_backup.core.Utilities;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class RestoreHelper {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
@ -43,22 +39,28 @@ public class RestoreHelper {
public static Optional<RestoreableFile> findFileAndLockIfPresent(LocalDateTime backupTime, MinecraftServer server) {
Path root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
Optional<RestoreableFile> optionalFile;
try (Stream<Path> stream = Files.list(root)) {
optionalFile = stream
.map(RestoreableFile::newInstance)
.flatMap(Optional::stream)
.filter(rf -> rf.getCreationTime().equals(backupTime))
.findFirst();
} catch (IOException e) {
throw new RuntimeException(e);
}
Optional<RestoreableFile> optionalFile =
RestoreableFile.applyOnFiles(root, Optional.empty(),
e -> log.error("An exception occurred while trying to lock the file!", e),
s -> s.filter(rf -> rf.getCreationTime().equals(backupTime))
.findFirst());
Statics.untouchableFile = optionalFile.map(RestoreableFile::getFile);
optionalFile.ifPresent(r -> Globals.INSTANCE.setLockedFile(r.getFile()));
return optionalFile;
}
public static Optional<RestoreableFile> getLatestAndLockIfPresent( MinecraftServer server) {
var available = RestoreHelper.getAvailableBackups(server);
if(available.isEmpty()) return Optional.empty();
else {
var latest = available.getLast();
Globals.INSTANCE.setLockedFile(latest.getFile());
return Optional.of(latest);
}
}
public static AwaitThread create(RestoreContext ctx) {
if(ctx.initiator() == ActionInitiator.Player)
log.info("Backup restoration was initiated by: {}", ctx.commandSource().getName());
@ -76,69 +78,11 @@ public class RestoreHelper {
);
}
public static List<RestoreableFile> getAvailableBackups(MinecraftServer server) {
public static LinkedList<RestoreableFile> getAvailableBackups(MinecraftServer server) {
Path root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
try (Stream<Path> stream = Files.list(root)) {
return stream.filter(Utilities::isValidBackup)
.map(RestoreableFile::newInstance)
.flatMap(Optional::stream)
.collect(Collectors.toList());
} catch (IOException e) {
log.error("Error while listing available backups", e);
return new LinkedList<>();
}
}
public static class RestoreableFile implements Comparable<RestoreableFile> {
private final Path file;
private final ConfigPOJO.ArchiveFormat archiveFormat;
private final LocalDateTime creationTime;
private final String comment;
private RestoreableFile(Path file) throws NoSuchElementException {
this.file = file;
archiveFormat = Utilities.getArchiveExtension(file).orElseThrow(() -> new NoSuchElementException("Couldn't get file extension!"));
String extension = archiveFormat.getCompleteString();
creationTime = Utilities.getFileCreationTime(file).orElseThrow(() -> new NoSuchElementException("Couldn't get file creation time!"));
final String filename = file.getFileName().toString();
if(filename.split("#").length > 1) this.comment = filename.split("#")[1].split(extension)[0];
else this.comment = null;
}
public static Optional<RestoreableFile> newInstance(Path file) {
try {
return Optional.of(new RestoreableFile(file));
} catch (NoSuchElementException ignored) {}
return Optional.empty();
}
public Path getFile() {
return file;
}
public ConfigPOJO.ArchiveFormat getArchiveFormat() {
return archiveFormat;
}
public LocalDateTime getCreationTime() {
return creationTime;
}
public String getComment() {
return comment;
}
@Override
public int compareTo(@NotNull RestoreHelper.RestoreableFile o) {
return creationTime.compareTo(o.creationTime);
}
public String toString() {
return this.getCreationTime().format(Statics.defaultDateTimeFormatter) + (comment != null ? "#" + comment : "");
}
return RestoreableFile.applyOnFiles(root, new LinkedList<>(),
e -> log.error("Error while listing available backups", e),
s -> s.sorted().collect(Collectors.toCollection(LinkedList::new)));
}
}

View File

@ -20,16 +20,20 @@ package net.szum123321.textile_backup.mixin;
import net.minecraft.server.dedicated.DedicatedServerWatchdog;
import net.minecraft.util.Util;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.Globals;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
/**
* This mixin should numb Watchdog while a backup runs.
* If works as intended solves issues with watchdog errors
*/
@Mixin(DedicatedServerWatchdog.class)
public class DedicatedServerWatchdogMixin {
@ModifyVariable(method = "run()V", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/util/Util;getMeasuringTimeMs()J"), ordinal = 0, name = "l")
private long redirectedCall(long original) {
return Statics.disableWatchdog ? Util.getMeasuringTimeMs() : original;
return Globals.INSTANCE.disableWatchdog ? Util.getMeasuringTimeMs() : original;
}
}

View File

@ -37,7 +37,7 @@ import org.at4j.support.io.LittleEndianBitOutputStream;
* @since 1.1
* @see BZip2OutputStreamSettings
*/
public class BZip2OutputStream extends OutputStream
public class BZip2OutputStream extends OutputStream implements AutoCloseable
{
private static final byte[] EOS_MAGIC = new byte[] { 0x17, 0x72, 0x45, 0x38, 0x50, (byte) 0x90 };
@ -263,17 +263,6 @@ public class BZip2OutputStream extends OutputStream
return this == o;
}
/**
* Close the stream if the client has been sloppy about it.
*/
@Override
protected void finalize() throws Throwable
{
close();
super.finalize();
}
/**
* Create a {@link BZip2EncoderExecutorService} that can be shared between
* several {@link BZip2OutputStream}:s to spread the bzip2 encoding work

View File

@ -24,7 +24,7 @@
"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.backupDirectoryPath": "Path to backup folder",
"text.autoconfig.textile_backup.option.fileBlacklist": "Blacklisted files",
@ -36,7 +36,7 @@
"text.autoconfig.textile_backup.option.maxAge.@Tooltip": "In seconds since creation",
"text.autoconfig.textile_backup.option.maxSize": "Max size of backup folder",
"text.autoconfig.textile_backup.option.maxSize.@Tooltip": "In KBytes",
"text.autoconfig.textile_backup.option.maxSize.@Tooltip": "In KiBytes",
"text.autoconfig.textile_backup.option.compression": "Compression level",
"text.autoconfig.textile_backup.option.compression.@Tooltip": "Only affects zip",

View File

@ -0,0 +1,10 @@
package net.szum123321.test.textile_backup;
import net.fabricmc.api.ModInitializer;
public class TextileBackupTest implements ModInitializer {
@Override
public void onInitialize() {
}
}

View File

@ -0,0 +1,35 @@
{
"schemaVersion": 1,
"id": "textile_backup",
"version": "${version}",
"name": "Textile Backup Test",
"authors": [
"Szum123321"
],
"contact": {
"homepage": "https://www.curseforge.com/minecraft/mc-mods/textile-backup",
"issues": "https://github.com/Szum123321/textile_backup/issues",
"sources": "https://github.com/Szum123321/textile_backup"
},
"license": "GPLv3",
"environment": "*",
"entrypoints": {
"main": [
"net.szum123321.test.textile_backup.TextileBackupTest"
]
},
"mixins": [
],
"depends": {
"fabricloader": ">=0.14.6",
"fabric": "*",
"minecraft": ">=1.19.1",
"cloth-config2": "*",
"java": ">=16",
"textile_backup": "*"
}
}

View File

@ -0,0 +1,12 @@
{
"required": true,
"package": "net.szum123321.test.textile_backup.mixin",
"compatibilityLevel": "JAVA_16",
"mixins": [
],
"client": [
],
"injectors": {
"defaultRequire": 1
}
}