Merge pull request #111 from Szum123321/refactor_2022

Refactor 2022
2.x
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 { plugins {
id 'fabric-loom' version '0.12-SNAPSHOT' id 'fabric-loom' version '1.0-SNAPSHOT'
id 'maven-publish' id 'maven-publish'
} }
@ -10,16 +10,22 @@ archivesBaseName = project.archives_base_name
version = "${project.mod_version}-${getMcMinor(project.minecraft_version)}" version = "${project.mod_version}-${getMcMinor(project.minecraft_version)}"
group = project.maven_group group = project.maven_group
repositories{ repositories {
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
maven { url "https://maven.shedaniel.me/" } maven { url "https://maven.shedaniel.me/" }
maven { maven { url "https://maven.terraformersmc.com/releases/" }
url "https://maven.terraformersmc.com/releases/" mavenCentral()
content { }
includeGroup "com.terraformersmc"
loom {
runs {
testServer {
server()
ideConfigGenerated project.rootProject == project
name = "Testmod Server"
source sourceSets.test
} }
} }
mavenCentral()
} }
dependencies { dependencies {
@ -40,15 +46,15 @@ dependencies {
modImplementation("com.terraformersmc:modmenu:${project.modmenu_version}") modImplementation("com.terraformersmc:modmenu:${project.modmenu_version}")
//General compression library //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" include "org.apache.commons:commons-compress:1.21"
//LZMA support //LZMA support
modImplementation 'org.tukaani:xz:1.9' implementation 'org.tukaani:xz:1.9'
include "org.tukaani:xz:1.9" include "org.tukaani:xz:1.9"
//Gzip compression, parallel, GITHUB //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}" 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. // 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 org.gradle.jvmargs=-Xmx1G
minecraft_version=1.19.2 minecraft_version=1.19.2
yarn_mappings=1.19.2+build.8 yarn_mappings=1.19.2+build.28
loader_version=0.14.9 loader_version=0.14.10
#Fabric api #Fabric api
fabric_version=0.60.0+1.19.2 fabric_version=0.64.0+1.19.2
#Cloth Config #Cloth Config
cloth_version=8.0.75 cloth_version=8.2.88
#ModMenu #ModMenu
modmenu_version=4.0.5 modmenu_version=4.1.0
#Lazy DFU for faster dev start #Lazy DFU for faster dev start
lazydfu_version=v0.1.3 lazydfu_version=v0.1.3
@ -21,6 +21,6 @@ lazydfu_version=v0.1.3
pgzip_commit_hash=af5f5c297e735f3f2df7aa4eb0e19a5810b8aff6 pgzip_commit_hash=af5f5c297e735f3f2df7aa4eb0e19a5810b8aff6
# Mod Properties # Mod Properties
mod_version = 2.4.0 mod_version = 2.5.0
maven_group = net.szum123321 maven_group = net.szum123321
archives_base_name = textile_backup archives_base_name = textile_backup

Binary file not shown.

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
PRG="$0" app_path=$0
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do # Need this for daisy-chained symlinks.
ls=`ls -ld "$PRG"` while
link=`expr "$ls" : '.*-> \(.*\)$'` APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
if expr "$link" : '/.*' > /dev/null; then [ -h "$app_path" ]
PRG="$link" do
else ls=$( ls -ld "$app_path" )
PRG=`dirname "$PRG"`"/$link" link=${ls#*' -> '}
fi case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" 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. # 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"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD=maximum
warn () { warn () {
echo "$*" echo "$*"
} } >&2
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} } >&2
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "`uname`" in case "$( uname )" in #(
CYGWIN* ) CYGWIN* ) cygwin=true ;; #(
cygwin=true Darwin* ) darwin=true ;; #(
;; MSYS* | MINGW* ) msys=true ;; #(
Darwin* ) NONSTOP* ) nonstop=true ;;
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java" JAVACMD=$JAVA_HOME/jre/sh/java
else else
JAVACMD="$JAVA_HOME/bin/java" JAVACMD=$JAVA_HOME/bin/java
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 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." location of your Java installation."
fi fi
else 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. 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 Please set the JAVA_HOME variable in your environment to match the
@ -105,84 +140,95 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
MAX_FD_LIMIT=`ulimit -H -n` case $MAX_FD in #(
if [ $? -eq 0 ] ; then max*)
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD=$( ulimit -H -n ) ||
MAX_FD="$MAX_FD_LIMIT" warn "Could not query maximum file descriptor limit"
fi esac
ulimit -n $MAX_FD case $MAX_FD in #(
if [ $? -ne 0 ] ; then '' | soft) :;; #(
warn "Could not set maximum file descriptor limit: $MAX_FD" *)
fi ulimit -n "$MAX_FD" ||
else warn "Could not set maximum file descriptor limit to $MAX_FD"
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" ;;
esac esac
fi fi
# Escape application args # Collect all arguments for the java command, stacking in reverse order:
save () { # * args from the command line
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done # * the main class name
echo " " # * -classpath
} # * -D...appname settings
APP_ARGS=$(save "$@") # * --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 # For Cygwin or MSYS, switch paths to Windows format before running java
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 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 JAVACMD=$( cygpath --unix "$JAVACMD" )
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")" # 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 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" "$@" exec "$JAVACMD" "$@"

25
gradlew.bat vendored
View File

@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% 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. @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" set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init if "%ERRORLEVEL%" == "0" goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 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_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init if exist "%JAVA_EXE%" goto execute
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@ -61,28 +64,14 @@ echo location of your Java installation.
goto fail 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 :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @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 :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

View File

@ -1,10 +1,10 @@
pluginManagement { pluginManagement {
repositories { repositories {
jcenter()
maven { maven {
name = 'Fabric' name = 'Fabric'
url = 'https://maven.fabricmc.net/' url = 'https://maven.fabricmc.net/'
} }
mavenCentral()
gradlePluginPortal() 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.ConfigHelper;
import net.szum123321.textile_backup.config.ConfigPOJO; import net.szum123321.textile_backup.config.ConfigPOJO;
import net.szum123321.textile_backup.core.ActionInitiator; import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.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.BackupScheduler; import net.szum123321.textile_backup.core.create.BackupScheduler;
import net.szum123321.textile_backup.core.create.MakeBackupRunnableFactory;
import java.util.concurrent.Executors;
public class TextileBackup implements ModInitializer { public class TextileBackup implements ModInitializer {
public static final String MOD_NAME = "Textile Backup"; public static final String MOD_NAME = "Textile Backup";
@ -58,20 +55,20 @@ public class TextileBackup implements ModInitializer {
ConfigHelper.updateInstance(AutoConfig.register(ConfigPOJO.class, JanksonConfigSerializer::new)); 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 //Restart Executor Service in single-player
ServerLifecycleEvents.SERVER_STARTING.register(server -> { ServerLifecycleEvents.SERVER_STARTING.register(server -> {
if(Statics.executorService.isShutdown()) Statics.executorService = Executors.newSingleThreadExecutor(); Globals.INSTANCE.resetQueueExecutor();
Globals.INSTANCE.updateTMPFSFlag(server);
Utilities.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 -> { ServerLifecycleEvents.SERVER_STOPPED.register(server -> {
Statics.executorService.shutdown(); Globals.INSTANCE.shutdownQueueExecutor(60000);
if (config.get().shutdownBackup && Statics.globalShutdownBackupFlag.get()) { if (config.get().shutdownBackup && Globals.INSTANCE.globalShutdownBackupFlag.get()) {
BackupHelper.create( MakeBackupRunnableFactory.create(
BackupContext.Builder BackupContext.Builder
.newBackupContextBuilder() .newBackupContextBuilder()
.setServer(server) .setServer(server)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,33 +21,36 @@ package net.szum123321.textile_backup.commands.restore;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Statics; import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.TextileBackup; import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger; import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.Utilities; import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.restore.AwaitThread;
import java.util.Optional;
public class KillRestoreCommand { public class KillRestoreCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME); private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() { public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("killR") return CommandManager.literal("killR")
.executes(ctx -> { .executes(ctx -> {
if(Statics.restoreAwaitThread != null && Statics.restoreAwaitThread.isAlive()) { if(Globals.INSTANCE.getAwaitThread().filter(Thread::isAlive).isEmpty()) {
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 {
log.sendInfo(ctx.getSource(), "Failed to stop backup restoration"); 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; 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.CommandManager;
import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.TextileBackup; import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger; import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.commands.CommandExceptions; import net.szum123321.textile_backup.commands.CommandExceptions;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.commands.FileSuggestionProvider; 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.RestoreContext;
import net.szum123321.textile_backup.core.restore.RestoreHelper; import net.szum123321.textile_backup.core.restore.RestoreHelper;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
public class RestoreBackupCommand { 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, "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, "[YEAR]-[MONTH]-[DAY]_[HOUR].[MINUTE].[SECOND]");
log.sendInfo(source, "Example: /backup restore 2020-08-05_10.58.33"); 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; return 1;
}); });
} }
private static int execute(String file, @Nullable String comment, ServerCommandSource source) throws CommandSyntaxException { private static int execute(String file, @Nullable String comment, ServerCommandSource source) throws CommandSyntaxException {
if(Statics.restoreAwaitThread == null || (Statics.restoreAwaitThread != null && !Statics.restoreAwaitThread.isAlive())) { if(Globals.INSTANCE.getAwaitThread().filter(Thread::isAlive).isPresent()) {
LocalDateTime dateTime; 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 { try {
dateTime = LocalDateTime.from(Statics.defaultDateTimeFormatter.parse(file)); dateTime = LocalDateTime.from(Globals.defaultDateTimeFormatter.parse(file));
} catch (DateTimeParseException e) { } catch (DateTimeParseException e) {
throw CommandExceptions.DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE.create(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()) { if(backupFile.isEmpty()) {
log.info("Found file to restore {}", backupFile.get().getFile().getFileName().toString()); log.sendInfo(source, "No file created on {} was found!", dateTime.format(Globals.defaultDateTimeFormatter));
} else { return -1;
log.sendInfo(source, "No file created on {} was found!", dateTime.format(Statics.defaultDateTimeFormatter)); } else {
log.info("Found file to restore {}", backupFile.get().getFile().getFileName().toString());
return 0; Globals.INSTANCE.setAwaitThread(
} RestoreHelper.create(
RestoreContext.Builder.newRestoreContextBuilder()
Statics.restoreAwaitThread = RestoreHelper.create( .setCommandSource(source)
RestoreContext.Builder.newRestoreContextBuilder() .setFile(backupFile.get())
.setCommandSource(source) .setComment(comment)
.setFile(backupFile.get()) .build()
.setComment(comment) )
.build()
); );
Statics.restoreAwaitThread.start(); Globals.INSTANCE.getAwaitThread().get().start();
return 1; 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; package net.szum123321.textile_backup.config;
import blue.endless.jankson.annotation.SerializedName;
import me.shedaniel.autoconfig.ConfigData; import me.shedaniel.autoconfig.ConfigData;
import me.shedaniel.autoconfig.annotation.Config; import me.shedaniel.autoconfig.annotation.Config;
import me.shedaniel.autoconfig.annotation.ConfigEntry; import me.shedaniel.autoconfig.annotation.ConfigEntry;
@ -63,8 +64,9 @@ public class ConfigPOJO implements ConfigData {
public boolean backupOldWorlds = true; public boolean backupOldWorlds = true;
@Comment("\nA path to the backup folder\n") @Comment("\nA path to the backup folder\n")
@SerializedName("path")
@ConfigEntry.Gui.NoTooltip() @ConfigEntry.Gui.NoTooltip()
public String path = "backup/"; public String backupDirectoryPath = "backup/";
@Comment(""" @Comment("""
\nThis setting allows you to exclude files form being backed-up. \nThis setting allows you to exclude files form being backed-up.

View File

@ -18,10 +18,13 @@
package net.szum123321.textile_backup.core; package net.szum123321.textile_backup.core;
/**
* Enum representing possible sources of action
*/
public enum ActionInitiator { public enum ActionInitiator {
Player("Player", "by"), Player("Player", "by"),
ServerConsole("Server Console", "from"), ServerConsole("Server Console", "from"), //some/ting typed a command and it was not a player (command blocks and server console count)
Timer("Timer", "by"), Timer("Timer", "by"), //a.k.a scheduler
Shutdown("Server Shutdown", "by"), Shutdown("Server Shutdown", "by"),
Restore("Backup Restoration", "because of"), Restore("Backup Restoration", "because of"),
Null("Null (That shouldn't have happened)", "form"); 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.TextileBackup;
import net.szum123321.textile_backup.TextileLogger; import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper; 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 net.szum123321.textile_backup.mixin.MinecraftServerSessionAccessor;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.file.SimplePathVisitor; import org.apache.commons.io.file.SimplePathVisitor;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.FileVisitResult; import java.nio.file.FileVisitResult;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.*; import java.time.*;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Optional;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
public class Utilities { public class Utilities {
private final static ConfigHelper config = ConfigHelper.INSTANCE; private final static ConfigHelper config = ConfigHelper.INSTANCE;
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME); private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static boolean wasSentByPlayer(ServerCommandSource source) { //I'm keeping this wrapper function for easier backporting
return source.isExecutedByPlayer(); public static boolean wasSentByPlayer(ServerCommandSource source) { return source.isExecutedByPlayer(); }
}
public static void notifyPlayers(@NotNull MinecraftServer server, String msg) { public static void notifyPlayers(@NotNull MinecraftServer server, String msg) {
MutableText message = log.getPrefixText(); MutableText message = log.getPrefixText();
@ -74,20 +64,6 @@ public class Utilities {
.getWorldDirectory(World.OVERWORLD); .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 { public static void deleteDirectory(Path path) throws IOException {
Files.walkFileTree(path, new SimplePathVisitor() { Files.walkFileTree(path, new SimplePathVisitor() {
@Override @Override
@ -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) { public static void disableWorldSaving(MinecraftServer server) {
for (ServerWorld serverWorld : server.getWorlds()) { for (ServerWorld serverWorld : server.getWorlds()) {
if (serverWorld != null && !serverWorld.savingDisabled) if (serverWorld != null && !serverWorld.savingDisabled)
@ -141,6 +98,22 @@ public class Utilities {
return System.getProperty("os.name").toLowerCase().contains("win"); 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) { public static boolean isBlacklisted(Path path) {
if(isWindows()) { //hotfix! if(isWindows()) { //hotfix!
if (path.getFileName().toString().equals("session.lock")) return true; if (path.getFileName().toString().equals("session.lock")) return true;
@ -149,66 +122,10 @@ public class Utilities {
return config.get().fileBlacklist.stream().anyMatch(path::startsWith); 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() { public static DateTimeFormatter getDateTimeFormatter() {
return DateTimeFormatter.ofPattern(config.get().dateTimeFormat); return DateTimeFormatter.ofPattern(config.get().dateTimeFormat);
} }
public static DateTimeFormatter getBackupDateTimeFormatter() {
return Statics.defaultDateTimeFormatter;
}
public static String formatDuration(Duration duration) { public static String formatDuration(Duration duration) {
DateTimeFormatter formatter; DateTimeFormatter formatter;

View File

@ -21,12 +21,9 @@ package net.szum123321.textile_backup.core.create;
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.util.Util;
import net.szum123321.textile_backup.core.ActionInitiator; import net.szum123321.textile_backup.core.ActionInitiator;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public record BackupContext(@NotNull MinecraftServer server, public record BackupContext(@NotNull MinecraftServer server,
ServerCommandSource commandSource, ServerCommandSource commandSource,
ActionInitiator initiator, ActionInitiator initiator,
@ -103,8 +100,7 @@ public record BackupContext(@NotNull MinecraftServer server,
if (server == null) { if (server == null) {
if (commandSource != null) setServer(commandSource.getServer()); if (commandSource != null) setServer(commandSource.getServer());
else else throw new RuntimeException("Neither MinecraftServer or ServerCommandSource were provided!");
throw new RuntimeException("Neither MinecraftServer or ServerCommandSource were provided!");
} }
return new BackupContext(server, commandSource, initiator, save, comment); 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; package net.szum123321.textile_backup.core.create;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.szum123321.textile_backup.Statics; import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.config.ConfigHelper; import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.ActionInitiator; import net.szum123321.textile_backup.core.ActionInitiator;
import java.time.Instant; import java.time.Instant;
/**
* 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 { public class BackupScheduler {
private final static ConfigHelper config = ConfigHelper.INSTANCE; private final static ConfigHelper config = ConfigHelper.INSTANCE;
private boolean scheduled; //Scheduled flag tells whether we have decided to run another backup
private long nextBackup; private static boolean scheduled = false;
private static long nextBackup = - 1;
public BackupScheduler() { public static void tick(MinecraftServer server) {
scheduled = false;
nextBackup = -1;
}
public void tick(MinecraftServer server) {
if(config.get().backupInterval < 1) return; if(config.get().backupInterval < 1) return;
long now = Instant.now().getEpochSecond(); long now = Instant.now().getEpochSecond();
if(config.get().doBackupsOnEmptyServer || server.getPlayerManager().getCurrentPlayerCount() > 0) { 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(scheduled) {
if(nextBackup <= now) { if(nextBackup <= now) {
Statics.executorService.submit( //It's time to run
BackupHelper.create( Globals.INSTANCE.getQueueExecutor().submit(
MakeBackupRunnableFactory.create(
BackupContext.Builder BackupContext.Builder
.newBackupContextBuilder() .newBackupContextBuilder()
.setServer(server) .setServer(server)
@ -57,13 +66,17 @@ public class BackupScheduler {
nextBackup = now + config.get().backupInterval; nextBackup = now + config.get().backupInterval;
} }
} else { } 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; nextBackup = now + config.get().backupInterval;
scheduled = true; scheduled = true;
} }
} else if(!config.get().doBackupsOnEmptyServer && server.getPlayerManager().getCurrentPlayerCount() == 0) { } 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) { if(scheduled && nextBackup <= now) {
Statics.executorService.submit( //Verify we hadn't done the final one, and it's time to do so
BackupHelper.create( Globals.INSTANCE.getQueueExecutor().submit(
MakeBackupRunnableFactory.create(
BackupContext.Builder BackupContext.Builder
.newBackupContextBuilder() .newBackupContextBuilder()
.setServer(server) .setServer(server)

View File

@ -18,13 +18,15 @@
package net.szum123321.textile_backup.core.create; package net.szum123321.textile_backup.core.create;
import net.szum123321.textile_backup.Statics; import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.TextileBackup; import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger; import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper; import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.ActionInitiator; import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.create.compressors.*; import net.szum123321.textile_backup.core.Cleanup;
import net.szum123321.textile_backup.core.Utilities; 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.AbstractTarArchiver;
import net.szum123321.textile_backup.core.create.compressors.tar.ParallelBZip2Compressor; import net.szum123321.textile_backup.core.create.compressors.tar.ParallelBZip2Compressor;
import net.szum123321.textile_backup.core.create.compressors.tar.ParallelGzipCompressor; 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.nio.file.Path;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/**
* The actual object responsible for creating the backup
*/
public class MakeBackupRunnable implements Runnable { public class MakeBackupRunnable implements Runnable {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME); private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE; private final static ConfigHelper config = ConfigHelper.INSTANCE;
private final BackupContext context; private final BackupContext context;
public MakeBackupRunnable(BackupContext context){ public MakeBackupRunnable(BackupContext context) {
this.context = context; this.context = context;
} }
@ -50,9 +55,9 @@ public class MakeBackupRunnable implements Runnable {
public void run() { public void run() {
try { try {
Utilities.disableWorldSaving(context.server()); 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"); log.sendInfoAL(context, "Starting backup");
@ -66,17 +71,8 @@ public class MakeBackupRunnable implements Runnable {
log.trace("Outfile is: {}", outFile); log.trace("Outfile is: {}", outFile);
try { Files.createDirectories(outFile.getParent());
Files.createDirectories(outFile.getParent()); Files.createFile(outFile);
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;
}
int coreCount; int coreCount;
@ -90,12 +86,12 @@ public class MakeBackupRunnable implements Runnable {
switch (config.get().format) { switch (config.get().format) {
case ZIP -> { case ZIP -> {
if (coreCount > 1 && !Statics.disableTMPFiles) { if (coreCount > 1 && !Globals.INSTANCE.disableTMPFS()) {
ParallelZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
log.trace("Using PARALLEL Zip Compressor. Threads: {}", coreCount); log.trace("Using PARALLEL Zip Compressor. Threads: {}", coreCount);
ParallelZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
} else { } else {
log.trace("Using REGULAR Zip Compressor.");
ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount); ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
log.trace("Using REGULAR Zip Compressor. Threads: {}");
} }
} }
case BZIP2 -> ParallelBZip2Compressor.getInstance().createArchive(world, outFile, context, coreCount); 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); 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) { if(config.get().broadcastBackupDone) {
Utilities.notifyPlayers( Utilities.notifyPlayers(
@ -118,9 +114,15 @@ public class MakeBackupRunnable implements Runnable {
} else { } else {
log.sendInfoAL(context, "Done!"); 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 { } finally {
Utilities.enableWorldSaving(context.server()); 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.concurrent.ExecutionException;
import java.util.stream.Stream; import java.util.stream.Stream;
/**
* Basic abstract class representing directory compressor
*/
public abstract class AbstractCompressor { public abstract class AbstractCompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME); private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
@ -73,7 +76,6 @@ public abstract class AbstractCompressor {
} }
} catch (IOException | InterruptedException | ExecutionException e) { } catch (IOException | InterruptedException | ExecutionException e) {
log.error("An exception occurred!", e); log.error("An exception occurred!", e);
} catch (Exception e) {
if(ctx.initiator() == ActionInitiator.Player) if(ctx.initiator() == ActionInitiator.Player)
log.sendError(ctx, "Something went wrong while compressing files!"); log.sendError(ctx, "Something went wrong while compressing files!");
} finally { } finally {
@ -89,7 +91,7 @@ public abstract class AbstractCompressor {
protected abstract void addEntry(Path file, String entryName, OutputStream arc) throws IOException; protected abstract void addEntry(Path file, String entryName, OutputStream arc) throws IOException;
protected void finish(OutputStream arc) throws InterruptedException, ExecutionException, 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() { protected void close() {

View File

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

View File

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

View File

@ -19,22 +19,18 @@
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.Globals;
import net.szum123321.textile_backup.TextileBackup; import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger; import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper; 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.ActionInitiator;
import net.szum123321.textile_backup.core.RestoreableFile;
import net.szum123321.textile_backup.core.Utilities; 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.nio.file.Path;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
public class RestoreHelper { public class RestoreHelper {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME); 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) { public static Optional<RestoreableFile> findFileAndLockIfPresent(LocalDateTime backupTime, MinecraftServer server) {
Path root = Utilities.getBackupRootPath(Utilities.getLevelName(server)); Path root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
Optional<RestoreableFile> optionalFile; Optional<RestoreableFile> optionalFile =
try (Stream<Path> stream = Files.list(root)) { RestoreableFile.applyOnFiles(root, Optional.empty(),
optionalFile = stream e -> log.error("An exception occurred while trying to lock the file!", e),
.map(RestoreableFile::newInstance) s -> s.filter(rf -> rf.getCreationTime().equals(backupTime))
.flatMap(Optional::stream) .findFirst());
.filter(rf -> rf.getCreationTime().equals(backupTime))
.findFirst();
} catch (IOException e) {
throw new RuntimeException(e);
}
Statics.untouchableFile = optionalFile.map(RestoreableFile::getFile); optionalFile.ifPresent(r -> Globals.INSTANCE.setLockedFile(r.getFile()));
return optionalFile; 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) { public static AwaitThread create(RestoreContext ctx) {
if(ctx.initiator() == ActionInitiator.Player) if(ctx.initiator() == ActionInitiator.Player)
log.info("Backup restoration was initiated by: {}", ctx.commandSource().getName()); 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)); Path root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
try (Stream<Path> stream = Files.list(root)) { return RestoreableFile.applyOnFiles(root, new LinkedList<>(),
return stream.filter(Utilities::isValidBackup) e -> log.error("Error while listing available backups", e),
.map(RestoreableFile::newInstance) s -> s.sorted().collect(Collectors.toCollection(LinkedList::new)));
.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 : "");
}
} }
} }

View File

@ -20,16 +20,20 @@ package net.szum123321.textile_backup.mixin;
import net.minecraft.server.dedicated.DedicatedServerWatchdog; import net.minecraft.server.dedicated.DedicatedServerWatchdog;
import net.minecraft.util.Util; 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.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable; 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) @Mixin(DedicatedServerWatchdog.class)
public class DedicatedServerWatchdogMixin { public class DedicatedServerWatchdogMixin {
@ModifyVariable(method = "run()V", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/util/Util;getMeasuringTimeMs()J"), ordinal = 0, name = "l") @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) { 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 * @since 1.1
* @see BZip2OutputStreamSettings * @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 }; 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; 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 * Create a {@link BZip2EncoderExecutorService} that can be shared between
* several {@link BZip2OutputStream}:s to spread the bzip2 encoding work * 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.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", "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.maxAge.@Tooltip": "In seconds since creation",
"text.autoconfig.textile_backup.option.maxSize": "Max size of backup folder", "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": "Compression level",
"text.autoconfig.textile_backup.option.compression.@Tooltip": "Only affects zip", "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
}
}