Compare commits

..

No commits in common. "2.x_zh_cn_translation" and "2.3.0" have entirely different histories.

68 changed files with 1537 additions and 2423 deletions

View File

@ -1,3 +0,0 @@
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,38 +1,39 @@
# Textile Backup # Textile Backup
[![Curseforge](http://cf.way2muchnoise.eu/full_359893_downloads.svg) ![Available for](http://cf.way2muchnoise.eu/versions/359893.svg)](https://www.curseforge.com/minecraft/mc-mods/textile-backup) >Finally, a backup mod for fabric!
[![Modrinth](https://modrinth-utils.vercel.app/api/badge/downloads?id=wwcspvkr&logo=true)](https://modrinth.com/mod/textile_backup)
[![Github](https://img.shields.io/github/stars/Szum123321/textile_backup?style=social) ![Github](https://img.shields.io/github/v/release/Szum123321/textile_backup?style=plastic)](https://github.com/Szum123321/textile_backup)
## What is it? [中文/Chinese](https://github.com/Szum123321/textile_backup/blob/2.x/README_zh-CN.md)
Textile Backup porvides a way to automatically make a backup of your world, zip it and even bring it back later, replacing your old world, all without leaving the game.
------------ [![Downloads](http://cf.way2muchnoise.eu/full_359893_downloads.svg)
### List of features ![Available for](http://cf.way2muchnoise.eu/versions/359893.svg)](https://www.curseforge.com/minecraft/mc-mods/textile-backup)
- Multithreaded compression: backups are made much faster than with other mods.
- Multiple compression formats: performace can be tuned to your preference
- It's super easy to bring back an old verion of your world (Requires server restart)
- Built-in management features: you can whitelist/blacklist players' access to txb commands
- Automatic deletion of old files (Age-, Number- and Size- based)
- Highly configurable: [Wiki](https://github.com/Szum123321/textile_backup/wiki/Configuration)
- Fully server-sided: all features are available even if it's only peresent on the server
### [**Installation Guide**](https://github.com/Szum123321/textile_backup/wiki/Installation) Small, configurable, fully server-side backup mod for Fabric
### [**Usage Guide**](https://github.com/Szum123321/textile_backup/wiki/Usage)
Commands look like that: `/backup <operation> [args]`
------------ Available operations are:
* start - just starts backup. You can add comment* to file by just typing it after command. For example: `/backup start FabricIsGreat`
* restore - restores backup. Note that the current world will be backuped, and you can add comment to it. `/backup restore <creation date> [comment]`
* killR - terminate current restoration.
* list - lists all avaliable backups.
* cleanup - forces cleanup procedure (deletes old backups according to config)
* delete - delets given file, usage the same as restore
* whitelist - here you can add, remove and list player that are allowed to run any operation within this mod despite not having high enough permission level*
* blacklist - here you can add, remove and list player that are not allowed to run any operation within this mod despite having high enough permission level*
This mod requires both the Fabric API and Cloth Config to run. All of the above can only be done by server admins(permission level 4 - configurable*) or player on a single player.
Feel free to use this mod in your modpack or on a server! Feel free to use this mod in your modpack or on a server!
------------ ### Important
### Contact: * Time format defaultly used by this mod is: dd.MM.yyyy_HH-mm-ss although it is configurable*.
* Since 2.2.0 TxB depends on **Cloth config**
If you need any help then you can join the [Discord](https://discord.gg/ktasEy4) server \* - feature available since 1.1.0
In case of an issue please report it [HERE](https://github.com/Szum123321/textile_backup/issues)
If you'd like to support me: If you have any suggestions or found a problem please report it on [Github](https://github.com/Szum123321/textile_backup).
[![Donate with Bitcoin](https://en.cryptobadges.io/badge/micro/bc1qwnqrdv5rs36tkfgxmnkw5f7qx4nhsncy5kj69s)](https://en.cryptobadges.io/donate/bc1qwnqrdv5rs36tkfgxmnkw5f7qx4nhsncy5kj69s)[![Donate with Ethereum](https://en.cryptobadges.io/badge/micro/0xF196c12b0A013d91015c541E63A87BA636851871)](https://en.cryptobadges.io/donate/0xF196c12b0A013d91015c541E63A87BA636851871)
And here's a link to my [discord server](https://discord.gg/ktasEy4)
For copyright info see: [Copyright Notice](/Copyright_Notice)

34
README_zh-CN.md Normal file
View File

@ -0,0 +1,34 @@
# Textile Backup
>终于有了一个Fabric的备份mod
[English/英文](https://github.com/Szum123321/textile_backup/blob/2.x/README.md)
[![下载](http://cf.way2muchnoise.eu/full_359893_downloads.svg)
![版本](http://cf.way2muchnoise.eu/versions/359893.svg)](https://www.curseforge.com/minecraft/mc-mods/textile-backup)
轻量可配置完全服务器端的备份Fabric备份mod
指令看起来应该像这样:`/backup <operation> [args]`
有效的指令包括:
* start - 创建备份。你可以为备份加入注释,只要将注释作为第二个参数传入即可,输入中文需要用引号包起来。例如: `/backup start "Fabric太棒了"`
* restore - 恢复备份。注意当前的时间会被备份,你可以添加该备份的注释。`/backup restore <版本> [注释]`
* killR - 终止当前恢复进程。
* list - 列出所有可用备份。
* cleanup - 强制启动清理进程 - 根据配置文件删除无效备份
* whitelist - 你可以在这里添加移除和列出所有无需足够权限就可以操作这个mod的玩家白名单*
* backlist - 你可以在这里添加移除和列出所有即使有足够权限也不能操作这个mod的玩家黑名单*
上面这些只能被服务器管理员(权限等级4 - 可配置*)、白名单玩家、单人模式的玩家或局域网联机的所有玩家使用。
你可以随意在你的整合包或服务器中使用这个mod。
### 重要
* 这个mod使用的时间格式是`dd.MM.yyyy_HH-mm-ss`,当然,这是可以配置的*.
* 这个mod以jars in a jar的形式包含 **Cotton Config** 和它的依赖,这是**CottonMC**的作品_.
\* - 自1.1.0版本可用的特性
如果你有任何建议或发现了问题,请在[Github](https://github.com/Szum123321/textile_backup)报告。

View File

@ -1,19 +1,28 @@
plugins { plugins {
id 'fabric-loom' version '1.2-SNAPSHOT' id 'fabric-loom' version '0.10-SNAPSHOT'
id 'maven-publish' id 'maven-publish'
} }
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_16
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_16
archivesBaseName = project.archives_base_name 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 { minecraft {
}
repositories{
maven { url 'https://server.bbkr.space/artifactory/libs-release' }
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
maven { url "https://maven.shedaniel.me/" } maven { url "https://maven.shedaniel.me/" }
maven { url "https://maven.terraformersmc.com/releases/" } maven {
url "https://maven.terraformersmc.com/releases/"
content {
includeGroup "com.terraformersmc"
}
}
mavenCentral() mavenCentral()
} }
@ -35,20 +44,21 @@ dependencies {
modImplementation("com.terraformersmc:modmenu:${project.modmenu_version}") modImplementation("com.terraformersmc:modmenu:${project.modmenu_version}")
//General compression library //General compression library
implementation "org.apache.commons:commons-compress:1.22" modImplementation "org.apache.commons:commons-compress:1.19"
include "org.apache.commons:commons-compress:1.22" include "org.apache.commons:commons-compress:1.19"
//LZMA support //LZMA support
implementation 'org.tukaani:xz:1.9' modImplementation '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
implementation "com.github.shevek:parallelgzip:${project.pgzip_commit_hash}" modImplementation "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.
// I couldn't get this working in my environment - IzzyBizzy modRuntime("com.github.astei:lazydfu:${project.lazydfu_version}") {
//modLocalRuntime("com.github.astei:lazydfu:${project.lazydfu_version}") exclude(module: "fabric-loader")
}
} }
processResources { processResources {
@ -105,9 +115,9 @@ publishing {
} }
static def getMcMinor(ver) { static def getMcMinor(ver) {
String[] arr = ((String)ver).split("[.-]") String[] arr = ((String)ver).split("\\.");
if(arr.length < 2) return ver if(arr.length < 2) return ver;
return (String)(arr[0] + "." + arr[1]) return (String)(arr[0] + "." + arr[1]);
} }

View File

@ -1,25 +1,25 @@
# Done to increase the memory available to gradle. # Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G org.gradle.jvmargs=-Xmx1G
minecraft_version=1.20 minecraft_version=1.18
yarn_mappings=1.20+build.1 yarn_mappings=1.18+build.1
loader_version=0.14.21 loader_version=0.12.6
#Fabric api #Fabric api
fabric_version=0.83.0+1.20 fabric_version=0.43.1+1.18
#Cloth Config #Cloth Config
cloth_version=11.0.98 cloth_version=6.0.42
#ModMenu #ModMenu
modmenu_version=7.0.0-beta.2 modmenu_version=3.0.0
databreaker_version=0.2.10 #Lazy DFU for faster dev start
lazydfu_version=0.1.2
#Hash of commit form which parallel gzip will be build #Hash of commit form which parallel gzip will be build
pgzip_commit_hash=af5f5c297e735f3f2df7aa4eb0e19a5810b8aff6 pgzip_commit_hash=af5f5c297e735f3f2df7aa4eb0e19a5810b8aff6
# Mod Properties # Mod Properties
mod_version = 3.1.0 mod_version = 2.3.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-8.1.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-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 @@
#!/bin/sh #!/usr/bin/env sh
# #
# Copyright © 2015-2021 the original authors. # Copyright 2015 the original author or 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,113 +17,78 @@
# #
############################################################################## ##############################################################################
# ##
# Gradle start up script for POSIX generated by Gradle. ## Gradle start up script for UN*X
# ##
# 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
app_path=$0 PRG="$0"
# Need this for relative symlinks.
# Need this for daisy-chained symlinks. while [ -h "$PRG" ] ; do
while ls=`ls -ld "$PRG"`
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path link=`expr "$ls" : '.*-> \(.*\)$'`
[ -h "$app_path" ] if expr "$link" : '/.*' > /dev/null; then
do PRG="$link"
ls=$( ls -ld "$app_path" ) else
link=${ls#*' -> '} PRG=`dirname "$PRG"`"/$link"
case $link in #( fi
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" APP_NAME="Gradle"
APP_BASE_NAME=${0##*/} APP_BASE_NAME=`basename "$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=true ;; #( CYGWIN* )
Darwin* ) darwin=true ;; #( cygwin=true
MSYS* | MINGW* ) msys=true ;; #( ;;
NONSTOP* ) nonstop=true ;; Darwin* )
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
@ -132,7 +97,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
@ -140,95 +105,84 @@ 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" && ! "$darwin" && ! "$nonstop" ; then if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
case $MAX_FD in #( MAX_FD_LIMIT=`ulimit -H -n`
max*) if [ $? -eq 0 ] ; then
MAX_FD=$( ulimit -H -n ) || if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
warn "Could not query maximum file descriptor limit" MAX_FD="$MAX_FD_LIMIT"
esac fi
case $MAX_FD in #( ulimit -n $MAX_FD
'' | soft) :;; #( if [ $? -ne 0 ] ; then
*) warn "Could not set maximum file descriptor limit: $MAX_FD"
ulimit -n "$MAX_FD" || fi
warn "Could not set maximum file descriptor limit to $MAX_FD" else
esac warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi 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; # For Darwin, add options to specify how the application appears in the dock
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of if $darwin; then
# shell script including quotes and variable substitutions, so put them in GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
# double quotes to make sure that they get re-expanded; and fi
# * put everything else in single quotes, so that it's not re-expanded.
set -- \ # For Cygwin, switch paths to Windows format before running java
"-Dorg.gradle.appname=$APP_BASE_NAME" \ if $cygwin ; then
-classpath "$CLASSPATH" \ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
org.gradle.wrapper.GradleWrapperMain \ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
"$@" JAVACMD=`cygpath --unix "$JAVACMD"`
# Use "xargs" to parse quoted args. # We build the pattern for arguments to be converted via cygpath
# ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
# With -n1 it outputs one arg per line, with the quotes and backslashes removed. SEP=""
# for dir in $ROOTDIRSRAW ; do
# In Bash we could simply go: ROOTDIRS="$ROOTDIRS$SEP$dir"
# SEP="|"
# readarray ARGS < <( xargs -n1 <<<"$var" ) && done
# set -- "${ARGS[@]}" "$@" OURCYGPATTERN="(^($ROOTDIRS))"
# # Add a user-defined pattern to the cygpath arguments
# but POSIX shell has neither arrays nor command substitution, so instead we if [ "$GRADLE_CYGPATTERN" != "" ] ; then
# post-process each arg (as a line of input to sed) to backslash-escape any OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
# character that might be a shell metacharacter, then use eval to reverse fi
# that process (while maintaining the separation between arguments), and wrap # Now convert the arguments - kludge to limit ourselves to /bin/sh
# the whole thing up as a single "set" statement. i=0
# for arg in "$@" ; do
# This will of course break if any of these variables contains a newline or CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
# an unmatched quote. CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
#
eval "set -- $( if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
xargs -n1 | else
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | eval `echo args$i`="\"$arg\""
tr '\n' ' ' 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
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

25
gradlew.bat vendored
View File

@ -29,9 +29,6 @@ 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"
@ -40,7 +37,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 execute if "%ERRORLEVEL%" == "0" goto init
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.
@ -54,7 +51,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 execute if exist "%JAVA_EXE%" goto init
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%
@ -64,14 +61,28 @@ 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 %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
: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

@ -1,146 +0,0 @@
/*
* 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.digest.BalticHash;
import net.szum123321.textile_backup.core.digest.Hash;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.restore.AwaitThread;
import org.apache.commons.io.FileUtils;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
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;
import java.util.function.Supplier;
import java.util.zip.CRC32;
public class Globals {
public static final Globals INSTANCE = new Globals();
private static final TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static final DateTimeFormatter defaultDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss");
public static final Supplier<Hash> CHECKSUM_SUPPLIER = BalticHash::new;/*() -> new Hash() {
private final CRC32 crc = new CRC32();
@Override
public void update ( int b){
crc.update(b);
}
@Override
public void update ( long b) {
ByteBuffer v = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
v.putLong(b);
crc.update(v.array());
}
@Override
public void update ( byte[] b, int off, int len){
crc.update(b, off, len);
}
@Override
public long getValue () {
return crc.getValue();
}
};*/
private ExecutorService executorService = null;//TODO: AAAAAAAAAAAAAAA MEMORY LEAK!!!!!!!!!
public final AtomicBoolean globalShutdownBackupFlag = new AtomicBoolean(true);
public boolean disableWatchdog = false;
private boolean disableTMPFiles = false;
private AwaitThread restoreAwaitThread = null;
private Path lockedPath = null;
private String combinedVersionString;
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("在等待当前运行的备份完成时发生了超时!");
executorService.shutdownNow().stream()
// .filter(r -> r instanceof ExecutableBackup)
// .map(r -> (ExecutableBackup)r)
.forEach(r -> log.error("Dropping: {}", r.toString()));
if(!executorService.awaitTermination(1000, TimeUnit.MICROSECONDS))
log.error("无法关闭执行器!");
}
} catch (InterruptedException e) {
log.error("发生了一个异常!(灾难性的错误)", 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 synchronized boolean disableTMPFS() { return disableTMPFiles; }
public synchronized 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("TMP(临时)文件目录没有可用空间 ({})", tmp_dir);
disableTMPFiles = true;
}
if(!Files.isWritable(tmp_dir)) {
log.error("TMP(临时)文件目录({})是只读的", tmp_dir);
disableTMPFiles = true;
}
if(disableTMPFiles) log.error("Might cause: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems");
}
public String getCombinedVersionString() {
return combinedVersionString;
}
public void setCombinedVersionString(String combinedVersionString) {
this.combinedVersionString = combinedVersionString;
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.io.File;
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<File> untouchableFile = Optional.empty();
public static boolean disableTMPFiles = false;
}

View File

@ -1,20 +1,20 @@
/* /*
* A simple backup mod for Fabric A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 Copyright (C) 2020 Szum123321
*
* This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package net.szum123321.textile_backup; package net.szum123321.textile_backup;
@ -23,10 +23,9 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import me.shedaniel.autoconfig.AutoConfig; import me.shedaniel.autoconfig.AutoConfig;
import me.shedaniel.autoconfig.serializer.JanksonConfigSerializer; import me.shedaniel.autoconfig.serializer.JanksonConfigSerializer;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.commands.create.CleanupCommand; import net.szum123321.textile_backup.commands.create.CleanupCommand;
import net.szum123321.textile_backup.commands.create.StartBackupCommand; import net.szum123321.textile_backup.commands.create.StartBackupCommand;
@ -39,9 +38,12 @@ 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.BackupHelper;
import net.szum123321.textile_backup.core.create.BackupScheduler; import net.szum123321.textile_backup.core.create.BackupScheduler;
import net.szum123321.textile_backup.core.create.ExecutableBackup;
import net.szum123321.textile_backup.test.BalticHashTest; 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";
@ -52,43 +54,35 @@ public class TextileBackup implements ModInitializer {
@Override @Override
public void onInitialize() { public void onInitialize() {
Globals.INSTANCE.setCombinedVersionString( log.info("Starting Textile Backup by Szum123321");
FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().getMetadata().getVersion().getFriendlyString() +
":" +
FabricLoader.getInstance().getModContainer("minecraft").orElseThrow().getMetadata().getVersion().getFriendlyString()
);
log.info("Starting Textile Backup {} by Szum123321", Globals.INSTANCE.getCombinedVersionString());
ConfigHelper.updateInstance(AutoConfig.register(ConfigPOJO.class, JanksonConfigSerializer::new)); ConfigHelper.updateInstance(AutoConfig.register(ConfigPOJO.class, JanksonConfigSerializer::new));
ServerTickEvents.END_SERVER_TICK.register(BackupScheduler::tick); ServerTickEvents.END_SERVER_TICK.register(new BackupScheduler()::tick);
//Restart Executor Service in single-player //Restart Executor Service in single-player
ServerLifecycleEvents.SERVER_STARTING.register(server -> { ServerLifecycleEvents.SERVER_STARTING.register(server -> {
Globals.INSTANCE.resetQueueExecutor(); if(Statics.executorService.isShutdown()) Statics.executorService = Executors.newSingleThreadExecutor();
Globals.INSTANCE.updateTMPFSFlag(server);
Utilities.updateTMPFSFlag(server);
}); });
//Wait 60s for already submitted 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 -> {
Globals.INSTANCE.shutdownQueueExecutor(60000); Statics.executorService.shutdown();
if (config.get().shutdownBackup && Globals.INSTANCE.globalShutdownBackupFlag.get()) { if (config.get().shutdownBackup && Statics.globalShutdownBackupFlag.get()) {
try { BackupHelper.create(
ExecutableBackup.Builder BackupContext.Builder
.newBackupContextBuilder() .newBackupContextBuilder()
.setServer(server) .setServer(server)
.setInitiator(ActionInitiator.Shutdown) .setInitiator(ActionInitiator.Shutdown)
.setComment("shutdown") .setComment("shutdown")
.announce()
.build() .build()
.call(); ).run();
} catch (Exception ignored) {}
} }
}); });
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> dispatcher.register( CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> dispatcher.register(
LiteralArgumentBuilder.<ServerCommandSource>literal("backup") LiteralArgumentBuilder.<ServerCommandSource>literal("backup")
.requires((ctx) -> { .requires((ctx) -> {
try { try {

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2021 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -18,12 +18,12 @@
package net.szum123321.textile_backup; package net.szum123321.textile_backup;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.Text; import net.minecraft.text.LiteralText;
import net.minecraft.text.MutableText; import net.minecraft.text.MutableText;
import net.minecraft.util.Formatting; import net.minecraft.util.Formatting;
import net.szum123321.textile_backup.core.Utilities; import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.core.create.ExecutableBackup;
import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@ -43,15 +43,22 @@ public class TextileLogger {
private final String prefix; private final String prefix;
private final MutableText prefixText; private final MutableText prefixText;
/* public TextileLogger(String name, String prefix) {
this.messageFactory = ParameterizedMessageFactory.INSTANCE;
this.logger = LogManager.getLogger(name, messageFactory);
this.prefix = "[" + prefix + "]" + " ";
this.prefixText = new LiteralText(this.prefix).styled(style -> style.withColor(0x5B23DA));
}
*/
public TextileLogger(String prefix) { public TextileLogger(String prefix) {
this.messageFactory = ParameterizedMessageFactory.INSTANCE; this.messageFactory = ParameterizedMessageFactory.INSTANCE;
this.logger = LogManager.getLogger(StackLocatorUtil.getCallerClass(2), messageFactory); this.logger = LogManager.getLogger(StackLocatorUtil.getCallerClass(2), messageFactory);
this.prefix = "[" + prefix + "]" + " "; this.prefix = "[" + prefix + "]" + " ";
this.prefixText = Text.literal(this.prefix).styled(style -> style.withColor(0x5B23DA)); this.prefixText = new LiteralText(this.prefix).styled(style -> style.withColor(0x5B23DA));
} }
public MutableText getPrefixText() { public MutableText getPrefixText() {
return prefixText.copy(); return prefixText.shallowCopy();
} }
public void log(Level level, String msg, Object... data) { public void log(Level level, String msg, Object... data) {
@ -78,23 +85,19 @@ public class TextileLogger {
log(Level.ERROR, msg, data); log(Level.ERROR, msg, data);
} }
void error(String message, Throwable throwable) {
logger.error(prefix + message, throwable);
}
public void fatal(String msg, Object... data) { public void fatal(String msg, Object... data) {
log(Level.FATAL, msg, data); log(Level.FATAL, msg, data);
} }
boolean sendFeedback(Level level, ServerCommandSource source, String msg, Object... args) { boolean sendFeedback(Level level, ServerCommandSource source, String msg, Object... args) {
if(source != null && Utilities.wasSentByPlayer(source)) { if(source != null && source.getEntity() instanceof PlayerEntity) {
MutableText text = Text.literal(messageFactory.newMessage(msg, args).getFormattedMessage()); LiteralText text = new LiteralText(messageFactory.newMessage(msg, args).getFormattedMessage());
if(level.intLevel() == Level.TRACE.intLevel()) text.formatted(Formatting.GREEN); if(level.intLevel() == Level.TRACE.intLevel()) text.formatted(Formatting.GREEN);
else if(level.intLevel() <= Level.WARN.intLevel()) text.formatted(Formatting.RED); else if(level.intLevel() <= Level.WARN.intLevel()) text.formatted(Formatting.RED);
else text.formatted(Formatting.WHITE); else text.formatted(Formatting.WHITE);
source.sendFeedback(() -> prefixText.copy().append(text), false); source.sendFeedback(prefixText.shallowCopy().append(text), false);
return true; return true;
} else { } else {
@ -112,17 +115,16 @@ public class TextileLogger {
sendFeedback(Level.INFO, source, msg, args); sendFeedback(Level.INFO, source, msg, args);
} }
public void sendInfo(ExecutableBackup context, String msg, Object... args) { public void sendInfo(BackupContext context, String msg, Object... args) {
sendInfo(context.commandSource(), msg, args); sendInfo(context.getCommandSource(), msg, args);
} }
public void sendError(ServerCommandSource source, String msg, Object... args) { public void sendError(ServerCommandSource source, String msg, Object... args) {
sendFeedback(Level.ERROR, source, msg, args); sendFeedback(Level.ERROR, source, msg, args);
} }
public void sendError(BackupContext context, String msg, Object... args) {
public void sendError(ExecutableBackup context, String msg, Object... args) { sendError(context.getCommandSource(), msg, args);
sendError(context.commandSource(), msg, args);
} }
public void sendToPlayerAndLog(Level level, ServerCommandSource source, String msg, Object... args) { public void sendToPlayerAndLog(Level level, ServerCommandSource source, String msg, Object... args) {
@ -135,15 +137,15 @@ public class TextileLogger {
sendToPlayerAndLog(Level.INFO, source, msg, args); sendToPlayerAndLog(Level.INFO, source, msg, args);
} }
public void sendInfoAL(ExecutableBackup context, String msg, Object... args) { public void sendInfoAL(BackupContext context, String msg, Object... args) {
sendInfoAL(context.commandSource(), msg, args); sendInfoAL(context.getCommandSource(), msg, args);
} }
public void sendErrorAL(ServerCommandSource source, String msg, Object... args) { public void sendErrorAL(ServerCommandSource source, String msg, Object... args) {
sendToPlayerAndLog(Level.ERROR, source, msg, args); sendToPlayerAndLog(Level.ERROR, source, msg, args);
} }
public void sendErrorAL(ExecutableBackup context, String msg, Object... args) { public void sendErrorAL(BackupContext context, String msg, Object... args) {
sendErrorAL(context.commandSource(), msg, args); sendErrorAL(context.getCommandSource(), msg, args);
} }
} }

View File

@ -1,21 +1,3 @@
/*
* 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.client; package net.szum123321.textile_backup.client;
import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ConfigScreenFactory;

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -19,7 +19,7 @@
package net.szum123321.textile_backup.commands; package net.szum123321.textile_backup.commands;
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
import net.minecraft.text.Text; import net.minecraft.text.LiteralText;
import net.minecraft.text.MutableText; import net.minecraft.text.MutableText;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
@ -28,7 +28,7 @@ public class CommandExceptions {
public static final DynamicCommandExceptionType DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE = new DynamicCommandExceptionType(o -> { public static final DynamicCommandExceptionType DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE = new DynamicCommandExceptionType(o -> {
DateTimeParseException e = (DateTimeParseException)o; DateTimeParseException e = (DateTimeParseException)o;
MutableText message = Text.literal("An exception occurred while trying to parse:\n") MutableText message = new LiteralText("An exception occurred while trying to parse:\n")
.append(e.getParsedString()) .append(e.getParsedString())
.append("\n"); .append("\n");

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -20,13 +20,13 @@ 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.entity.player.PlayerEntity;
import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Globals; import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.core.RestoreableFile;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.restore.RestoreHelper; import net.szum123321.textile_backup.core.restore.RestoreHelper;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -34,40 +34,33 @@ 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() { return INSTANCE; } public static FileSuggestionProvider Instance() {
return INSTANCE;
}
@Override @Override
public CompletableFuture<Suggestions> getSuggestions(CommandContext<ServerCommandSource> ctx, SuggestionsBuilder builder) { public CompletableFuture<Suggestions> getSuggestions(CommandContext<ServerCommandSource> ctx, SuggestionsBuilder builder) throws CommandSyntaxException {
String remaining = builder.getRemaining(); String remaining = builder.getRemaining();
var files = RestoreHelper.getAvailableBackups(ctx.getSource().getServer()); for (RestoreHelper.RestoreableFile file : 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 (ctx.getSource().getEntity() instanceof PlayerEntity) { //was typed by player
if (file.getComment().isPresent()) { if (file.getComment() != null) {
builder.suggest(formattedCreationTime, new LiteralMessage("Comment: " + file.getComment().get())); builder.suggest(formattedCreationTime, new LiteralMessage("Comment: " + file.getComment()));
} else { } else {
builder.suggest(formattedCreationTime); builder.suggest(formattedCreationTime);
} }
} else { //was typed from server console } else { //was typed from server console
if (file.getComment().isPresent()) { if (file.getComment() != null) {
builder.suggest(file.getCreationTime() + "#" + file.getComment().get()); builder.suggest(file.getCreationTime() + "#" + file.getComment());
} 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

@ -1,20 +1,20 @@
/* /*
* A simple backup mod for Fabric A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 Copyright (C) 2020 Szum123321
*
* This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package net.szum123321.textile_backup.commands.create; package net.szum123321.textile_backup.commands.create;
@ -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.Cleanup; import net.szum123321.textile_backup.core.create.BackupHelper;
import net.szum123321.textile_backup.core.Utilities; import net.szum123321.textile_backup.core.Utilities;
public class CleanupCommand { public class CleanupCommand {
@ -37,8 +37,8 @@ public class CleanupCommand {
private static int execute(ServerCommandSource source) { private static int execute(ServerCommandSource source) {
log.sendInfo( log.sendInfo(
source, source,
"删除了: {} 文件.", "Deleted: {} files.",
new Cleanup(source, Utilities.getLevelName(source.getServer())).call() BackupHelper.executeFileLimit(source, Utilities.getLevelName(source.getServer()))
); );
return 1; return 1;

View File

@ -1,20 +1,20 @@
/* /*
* A simple backup mod for Fabric A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 Copyright (C) 2020 Szum123321
*
* This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package net.szum123321.textile_backup.commands.create; package net.szum123321.textile_backup.commands.create;
@ -22,10 +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.Globals; import net.szum123321.textile_backup.Statics;
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.ExecutableBackup; import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.core.create.BackupHelper;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -40,15 +41,24 @@ public class StartBackupCommand {
} }
private static int execute(ServerCommandSource source, @Nullable String comment) { private static int execute(ServerCommandSource source, @Nullable String comment) {
Globals.INSTANCE.getQueueExecutor().submit( if(!Statics.executorService.isShutdown()) {
ExecutableBackup.Builder try {
Statics.executorService.submit(
BackupHelper.create(
BackupContext.Builder
.newBackupContextBuilder() .newBackupContextBuilder()
.setCommandSource(source) .setCommandSource(source)
.setComment(comment) .setComment(comment)
.guessInitiator() .guessInitiator()
.saveServer() .saveServer()
.build() .build()
)
); );
} catch (Exception e) {
log.error("Something went wrong while executing command!", e);
throw e;
}
}
return 1; return 1;
} }

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2021 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -49,7 +49,7 @@ public class BlacklistCommand {
} }
private static int help(ServerCommandSource source) { private static int help(ServerCommandSource source) {
log.sendInfo(source, "可用的命令有add [player]remove [player]list. "); log.sendInfo(source, "Available command are: add [player], remove [player], list.");
return 1; return 1;
} }
@ -57,7 +57,7 @@ public class BlacklistCommand {
private static int executeList(ServerCommandSource source) { private static int executeList(ServerCommandSource source) {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append("目前在黑名单上的有:"); builder.append("Currently on the blacklist are: ");
for(String name : config.get().playerBlacklist){ for(String name : config.get().playerBlacklist){
builder.append(name); builder.append(name);
@ -73,24 +73,24 @@ public class BlacklistCommand {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player"); ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(config.get().playerBlacklist.contains(player.getEntityName())) { if(config.get().playerBlacklist.contains(player.getEntityName())) {
log.sendInfo(ctx.getSource(), "玩家: {} 已经在黑名单中!", player.getEntityName()); log.sendInfo(ctx.getSource(), "Player: {} is already blacklisted.", player.getEntityName());
} else { } else {
config.get().playerBlacklist.add(player.getEntityName()); config.get().playerBlacklist.add(player.getEntityName());
config.save(); config.save();
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append("玩家: "); builder.append("Player: ");
builder.append(player.getEntityName()); builder.append(player.getEntityName());
builder.append(" 被添加到黑名单"); builder.append(" added to the blacklist");
if(config.get().playerWhitelist.contains(player.getEntityName())){ if(config.get().playerWhitelist.contains(player.getEntityName())){
config.get().playerWhitelist.remove(player.getEntityName()); config.get().playerWhitelist.remove(player.getEntityName());
config.save(); config.save();
builder.append(" 并且被移除白名单"); builder.append(" and removed form the whitelist");
} }
builder.append(" 成功."); builder.append(" successfully.");
ctx.getSource().getServer().getCommandManager().sendCommandTree(player); ctx.getSource().getServer().getCommandManager().sendCommandTree(player);
@ -104,14 +104,14 @@ public class BlacklistCommand {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player"); ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(!config.get().playerBlacklist.contains(player.getEntityName())) { if(!config.get().playerBlacklist.contains(player.getEntityName())) {
log.sendInfo(ctx.getSource(), "玩家: {} 还从未被列入黑名单.", player.getEntityName()); log.sendInfo(ctx.getSource(), "Player: {} newer was blacklisted.", player.getEntityName());
} else { } else {
config.get().playerBlacklist.remove(player.getEntityName()); config.get().playerBlacklist.remove(player.getEntityName());
config.save(); config.save();
ctx.getSource().getServer().getCommandManager().sendCommandTree(player); ctx.getSource().getServer().getCommandManager().sendCommandTree(player);
log.sendInfo(ctx.getSource(), "玩家: {} 被移除黑名单成功! ", player.getEntityName()); log.sendInfo(ctx.getSource(), "Player: {} removed from the blacklist successfully.", player.getEntityName());
} }
return 1; return 1;

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -21,21 +21,20 @@ package net.szum123321.textile_backup.commands.manage;
import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.entity.player.PlayerEntity;
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.File;
import java.nio.file.Files;
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.Arrays;
import java.util.Optional; import java.util.Optional;
public class DeleteCommand { public class DeleteCommand {
@ -53,36 +52,37 @@ public class DeleteCommand {
LocalDateTime dateTime; LocalDateTime dateTime;
try { try {
dateTime = LocalDateTime.from(Globals.defaultDateTimeFormatter.parse(fileName)); dateTime = LocalDateTime.from(Statics.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())); File root = Utilities.getBackupRootPath(Utilities.getLevelName(source.getServer()));
RestoreableFile.applyOnFiles(root, Optional.empty(), Optional<File> optionalFile = Arrays.stream(root.listFiles())
e -> log.sendErrorAL(source, "在尝试删除备份文件时发生了异常!", e), .filter(Utilities::isValidBackup)
stream -> stream.filter(f -> f.getCreationTime().equals(dateTime)).map(RestoreableFile::getFile).findFirst() .filter(file -> Utilities.getFileCreationTime(file).orElse(LocalDateTime.MIN).equals(dateTime))
).ifPresentOrElse(file -> { .findFirst();
if(Globals.INSTANCE.getLockedFile().filter(p -> p == file).isEmpty()) {
try {
Files.delete((Path) file);
log.sendInfo(source, "备份: {} 被成功删除!", file);
if(Utilities.wasSentByPlayer(source)) if(optionalFile.isPresent()) {
log.info("玩家 {} 删除了备份: {}.", source.getPlayer().getName(), file); if(Statics.untouchableFile.isEmpty() || !Statics.untouchableFile.get().equals(optionalFile.get())) {
} catch (IOException e) { if(optionalFile.get().delete()) {
log.sendError(source, "在尝试删除备份文件时发生了异常!"); log.sendInfo(source, "File {} successfully deleted!", optionalFile.get().getName());
if(source.getEntity() instanceof PlayerEntity)
log.info("Player {} deleted {}.", source.getPlayer().getName(), optionalFile.get().getName());
} else {
log.sendError(source, "Something went wrong while deleting file!");
} }
} else { } else {
log.sendError(source, "由于备份正在恢复中,无法删除该文件."); log.sendError(source, "Couldn't delete the file because it's being restored right now.");
log.sendHint(source, "如果您想中止恢复过程,请使用以下命令:/backup killR"); log.sendHint(source, "If you want to abort restoration then use: /backup killR");
} }
}, () -> { } else {
log.sendInfo(source, "根据您提供的文件名找不到相应的文件."); log.sendError(source, "Couldn't find file by this name.");
log.sendInfo(source, "也许您可以试试: /backup list"); log.sendHint(source, "Maybe try /backup list");
} }
);
return 0; return 0;
} }
} }

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -23,7 +23,6 @@ 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.*;
@ -34,17 +33,17 @@ 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();
var backups = RestoreHelper.getAvailableBackups(ctx.getSource().getServer()); List<RestoreHelper.RestoreableFile> backups = RestoreHelper.getAvailableBackups(ctx.getSource().getServer());
if(backups.size() == 0) { if(backups.size() == 0) {
builder.append("该世界没有可用的备份文件. "); builder.append("There a no backups available for this world.");
} else if(backups.size() == 1) { } else if(backups.size() == 1) {
builder.append("只有一个可用的备份文件: "); builder.append("There is only one backup available: ");
builder.append(backups.get(0).toString()); builder.append(backups.get(0).toString());
} else { } else {
backups.sort(null); backups.sort(null);
Iterator<RestoreableFile> iterator = backups.iterator(); Iterator<RestoreHelper.RestoreableFile> iterator = backups.iterator();
builder.append("可用的备份文件:\n"); builder.append("Available backups:\n");
builder.append(iterator.next()); builder.append(iterator.next());

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2021 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -49,7 +49,7 @@ public class WhitelistCommand {
} }
private static int help(ServerCommandSource source){ private static int help(ServerCommandSource source){
log.sendInfo(source, "可用的命令有: add [player], remove [player], list."); log.sendInfo(source, "Available command are: add [player], remove [player], list.");
return 1; return 1;
} }
@ -57,7 +57,7 @@ public class WhitelistCommand {
private static int executeList(ServerCommandSource source){ private static int executeList(ServerCommandSource source){
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append("目前在白名单的有: "); builder.append("Currently on the whitelist are: ");
for(String name : config.get().playerWhitelist){ for(String name : config.get().playerWhitelist){
builder.append(name); builder.append(name);
@ -73,24 +73,24 @@ public class WhitelistCommand {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player"); ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(config.get().playerWhitelist.contains(player.getEntityName())) { if(config.get().playerWhitelist.contains(player.getEntityName())) {
log.sendInfo(ctx.getSource(), "玩家: {} 已经在白名单列表里.", player.getEntityName()); log.sendInfo(ctx.getSource(), "Player: {} is already whitelisted.", player.getEntityName());
} else { } else {
config.get().playerWhitelist.add(player.getEntityName()); config.get().playerWhitelist.add(player.getEntityName());
config.save(); config.save();
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append("玩家: "); builder.append("Player: ");
builder.append(player.getEntityName()); builder.append(player.getEntityName());
builder.append(" 被添加的白名单"); builder.append(" added to the whitelist");
if(config.get().playerBlacklist.contains(player.getEntityName())){ if(config.get().playerBlacklist.contains(player.getEntityName())){
config.get().playerBlacklist.remove(player.getEntityName()); config.get().playerBlacklist.remove(player.getEntityName());
config.save(); config.save();
builder.append(" 并且被移除黑名单"); builder.append(" and removed form the blacklist");
} }
builder.append(" 成功."); builder.append(" successfully.");
ctx.getSource().getServer().getCommandManager().sendCommandTree(player); ctx.getSource().getServer().getCommandManager().sendCommandTree(player);
@ -104,14 +104,14 @@ public class WhitelistCommand {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player"); ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(!config.get().playerWhitelist.contains(player.getEntityName())) { if(!config.get().playerWhitelist.contains(player.getEntityName())) {
log.sendInfo(ctx.getSource(), "玩家: {} 还从未被列入白名单.", player.getEntityName()); log.sendInfo(ctx.getSource(), "Player: {} newer was whitelisted.", player.getEntityName());
} else { } else {
config.get().playerWhitelist.remove(player.getEntityName()); config.get().playerWhitelist.remove(player.getEntityName());
config.save(); config.save();
ctx.getSource().getServer().getCommandManager().sendCommandTree(player); ctx.getSource().getServer().getCommandManager().sendCommandTree(player);
log.sendInfo(ctx.getSource(), "玩家: {} 被移除白名单成功!", player.getEntityName()); log.sendInfo(ctx.getSource(), "Player: {} removed from the whitelist successfully.", player.getEntityName());
} }
return 1; return 1;

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -19,38 +19,36 @@
package net.szum123321.textile_backup.commands.restore; package net.szum123321.textile_backup.commands.restore;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.entity.player.PlayerEntity;
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.Statics;
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.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(Globals.INSTANCE.getAwaitThread().filter(Thread::isAlive).isEmpty()) { if(Statics.restoreAwaitThread != null && Statics.restoreAwaitThread.isAlive()) {
log.sendInfo(ctx.getSource(), "无法停止备份恢复过程"); Statics.restoreAwaitThread.interrupt();
return -1; Statics.globalShutdownBackupFlag.set(true);
} Statics.untouchableFile = Optional.empty();
AwaitThread thread = Globals.INSTANCE.getAwaitThread().get(); log.info("{} cancelled backup restoration.", ctx.getSource().getEntity() instanceof PlayerEntity ?
thread.interrupt();
Globals.INSTANCE.globalShutdownBackupFlag.set(true);
Globals.INSTANCE.setLockedFile(null);
log.info("{} 备份恢复操作已被取消", Utilities.wasSentByPlayer(ctx.getSource()) ?
"Player: " + ctx.getSource().getName() : "Player: " + ctx.getSource().getName() :
"SERVER" "SERVER"
); );
if(Utilities.wasSentByPlayer(ctx.getSource())) if(ctx.getSource().getEntity() instanceof PlayerEntity)
log.sendInfo(ctx.getSource(), "备份恢复已成功停止. "); log.sendInfo(ctx.getSource(), "Backup restoration successfully stopped.");
} else {
log.sendInfo(ctx.getSource(), "Failed to stop backup restoration");
}
return 1; return 1;
}); });
} }

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -24,19 +24,17 @@ 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 {
@ -63,57 +61,50 @@ public class RestoreBackupCommand {
).executes(context -> { ).executes(context -> {
ServerCommandSource source = context.getSource(); ServerCommandSource source = context.getSource();
log.sendInfo(source, "要恢复给定的备份,您必须以以下格式提供准确的创建时间:"); log.sendInfo(source, "To restore given backup you have to provide exact creation time in format:");
log.sendInfo(source, "[年]-[月]-[日]_[小时].[分钟].[秒]"); log.sendInfo(source, "[YEAR]-[MONTH]-[DAY]_[HOUR].[MINUTE].[SECOND]");
log.sendInfo(source, "示例:/backup restore 2020-08-05_10.58.33"); log.sendInfo(source, "Example: /backup restore 2020-08-05_10.58.33");
log.sendInfo(source, "您还可以输入 '/backup restore latest' 来恢复最新的备份。");
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(Globals.INSTANCE.getAwaitThread().filter(Thread::isAlive).isPresent()) { if(Statics.restoreAwaitThread == null || (Statics.restoreAwaitThread != null && !Statics.restoreAwaitThread.isAlive())) {
log.sendInfo(source, "已经有其他人开始了另一个恢复操作。");
return -1;
}
LocalDateTime dateTime; 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(Globals.defaultDateTimeFormatter.parse(file)); dateTime = LocalDateTime.from(Statics.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);
} }
backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getServer()); Optional<RestoreHelper.RestoreableFile> backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getServer());
if(backupFile.isPresent()) {
log.info("Found file to restore {}", backupFile.get().getFile().getName());
} else {
log.sendInfo(source, "No file created on {} was found!", dateTime.format(Statics.defaultDateTimeFormatter));
return 0;
} }
if(backupFile.isEmpty()) { Statics.restoreAwaitThread = RestoreHelper.create(
log.sendInfo(source, "在{}上没有找到创建的文件!", dateTime.format(Globals.defaultDateTimeFormatter));
return -1;
} else {
log.info("找到要恢复的文件:{}", backupFile.get().getFile().getFileName().toString());
Globals.INSTANCE.setAwaitThread(
RestoreHelper.create(
RestoreContext.Builder.newRestoreContextBuilder() RestoreContext.Builder.newRestoreContextBuilder()
.setCommandSource(source) .setCommandSource(source)
.setFile(backupFile.get()) .setFile(backupFile.get())
.setComment(comment) .setComment(comment)
.build() .build()
)
); );
Globals.INSTANCE.getAwaitThread().get().start(); Statics.restoreAwaitThread.start();
return 1; return 1;
} else {
log.sendInfo(source, "Someone has already started another restoration.");
return 0;
} }
} }
} }

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2021 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2021 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -18,17 +18,15 @@
package net.szum123321.textile_backup.config; package net.szum123321.textile_backup.config;
import me.shedaniel.cloth.clothconfig.shadowed.blue.endless.jankson.annotation.SerializedName;
import me.shedaniel.cloth.clothconfig.shadowed.blue.endless.jankson.Comment;
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;
import me.shedaniel.cloth.clothconfig.shadowed.blue.endless.jankson.Comment;
import net.szum123321.textile_backup.TextileBackup; import net.szum123321.textile_backup.TextileBackup;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
//TODO: Remove BZIP2 and LZMA compressors. As for the popular vote
@Config(name = TextileBackup.MOD_ID) @Config(name = TextileBackup.MOD_ID)
public class ConfigPOJO implements ConfigData { public class ConfigPOJO implements ConfigData {
@Comment("\nShould every world have its own backup folder?\n") @Comment("\nShould every world have its own backup folder?\n")
@ -65,9 +63,8 @@ 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 backupDirectoryPath = "backup/"; public String path = "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.
@ -91,11 +88,11 @@ public class ConfigPOJO implements ConfigData {
public long maxAge = 0; public long maxAge = 0;
@Comment(""" @Comment("""
\nMaximum size of backup folder in kibi bytes (1024). \nMaximum size of backup folder in kilo bytes (1024).
If set to 0 then backups will not be deleted If set to 0 then backups will not be deleted
""") """)
@ConfigEntry.Gui.Tooltip() @ConfigEntry.Gui.Tooltip()
public long maxSize = 0; public int maxSize = 0;
@Comment("\nCompression level \n0 - 9\n Only affects zip compression.\n") @Comment("\nCompression level \n0 - 9\n Only affects zip compression.\n")
@ConfigEntry.Gui.Tooltip() @ConfigEntry.Gui.Tooltip()
@ -115,6 +112,8 @@ public class ConfigPOJO implements ConfigData {
\nAvailable formats are: \nAvailable formats are:
ZIP - normal zip archive using standard deflate compression ZIP - normal zip archive using standard deflate compression
GZIP - tar.gz using gzip compression GZIP - tar.gz using gzip compression
BZIP2 - tar.bz2 archive using bzip2 compression
LZMA - tar.xz using lzma compression
TAR - .tar with no compression TAR - .tar with no compression
""") """)
@ConfigEntry.Gui.Tooltip() @ConfigEntry.Gui.Tooltip()
@ -161,14 +160,6 @@ public class ConfigPOJO implements ConfigData {
@ConfigEntry.Gui.Tooltip() @ConfigEntry.Gui.Tooltip()
public String dateTimeFormat = "yyyy.MM.dd_HH-mm-ss"; public String dateTimeFormat = "yyyy.MM.dd_HH-mm-ss";
@Comment("""
\nThe Strict mode (default) aborts backup creation in case of any problem and deletes created files
Permissible mode keeps partial/damaged backup but won't allow to restore it
Very Permissible mode will skip the verification process. THIS MOST CERTAINLY WILL LEAD TO DATA LOSS OR CORRUPTION
""")
@ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
public IntegrityVerificationMode integrityVerificationMode = IntegrityVerificationMode.STRICT;
@Override @Override
public void validatePostLoad() throws ValidationException { public void validatePostLoad() throws ValidationException {
if(compressionCoreCountLimit > Runtime.getRuntime().availableProcessors()) if(compressionCoreCountLimit > Runtime.getRuntime().availableProcessors())
@ -184,16 +175,6 @@ public class ConfigPOJO implements ConfigData {
} }
} }
public enum IntegrityVerificationMode {
STRICT,
PERMISSIBLE,
VERY_PERMISSIBLE;
public boolean isStrict() { return this == STRICT; }
public boolean verify() { return this != VERY_PERMISSIBLE; }
}
public enum ArchiveFormat { public enum ArchiveFormat {
ZIP("zip"), ZIP("zip"),
GZIP("tar", "gz"), GZIP("tar", "gz"),

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -18,15 +18,14 @@
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"), //some/ting typed a command and it was not a player (command blocks and server console count) ServerConsole("Server Console", "from"),
Timer("Timer", "by"), //a.k.a scheduler Timer("Timer", "by"),
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");
private final String name; private final String name;
private final String prefix; private final String prefix;

View File

@ -1,137 +0,0 @@
/*
* 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("尝试删除旧文件时发生异常!", 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("尝试删除旧文件时发生异常!", 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("无法获取文件的大小 " + f.getFile(), e);
continue;
}
n++;
}
} catch (IOException e) {
log.error("在计算文件数量时发生错误!", 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, "删除时发生了错误:{}.", f);
log.error("删除时发生了错误:{}.", f, e);
return false;
}
return true;
}
}

View File

@ -1,100 +0,0 @@
/*
* 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.core.restore.RestoreContext;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Optional;
public record CompressionStatus(long treeHash, Map<String, Exception> brokenFiles, LocalDateTime date, long startTimestamp, long finishTimestamp, String version) implements Serializable {
public static final String DATA_FILENAME = "textile_status.data";
public Optional<String> validate(long hash, RestoreContext ctx) throws RuntimeException {
if(hash != treeHash)
return Optional.of("Tree Hash mismatch!\n Expected: " + hex(treeHash) + ", got: " + hex(hash));
if(!brokenFiles.isEmpty()) return Optional.of("Damaged files present! ^");
if(ctx.restoreableFile().getCreationTime().equals(date))
return Optional.of(
"Creation date mismatch!\n Expected: " +
date.format(DateTimeFormatter.ISO_DATE_TIME) + ", got: " +
ctx.restoreableFile().getCreationTime().format(DateTimeFormatter.ISO_DATE_TIME)
);
return Optional.empty();
}
public static Path resolveStatusFilename(Path directory) { return directory.resolve(DATA_FILENAME); }
public static CompressionStatus readFromFile(Path directory) throws IOException, ClassNotFoundException {
try(InputStream i = Files.newInputStream(directory.resolve(DATA_FILENAME));
ObjectInputStream obj = new ObjectInputStream(i)) {
return (CompressionStatus) obj.readObject();
}
}
public byte[] serialize() throws IOException {
try (ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream o = new ObjectOutputStream(bo)) {
o.writeObject(this);
return bo.toByteArray();
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{ ");
builder.append("Hash: ")
.append(hex(treeHash))
.append(", Date: ")
.append(date.format(DateTimeFormatter.ISO_DATE_TIME))
.append(", Start timestamp: ").append(startTimestamp)
.append(", Finish timestamp: ").append(finishTimestamp)
.append(", Mod Version: ").append(version);
builder.append(", Broken files: ");
if(brokenFiles.isEmpty()) builder.append("[]");
else {
builder.append("[\n");
for(String i: brokenFiles.keySet()) {
builder.append(i)
.append(":");
ByteArrayOutputStream o = new ByteArrayOutputStream();
brokenFiles.get(i).printStackTrace(new PrintStream(o));
builder.append(o).append("\n");
}
builder.append("]");
}
builder.append(" }");
return builder.toString();
}
private static String hex(long val) { return "0x" + Long.toHexString(val).toUpperCase(); }
}

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2021 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
package net.szum123321.textile_backup.core; package net.szum123321.textile_backup.core;
import java.io.IOException; public interface LivingServer {
boolean isAlive();
public class DataLeftException extends IOException {
public DataLeftException(long n) { super("Input stream closed with " + n + " bytes left!"); }
} }

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2021 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -25,6 +25,6 @@ import java.io.IOException;
*/ */
public class NoSpaceLeftOnDeviceException extends IOException { public class NoSpaceLeftOnDeviceException extends IOException {
public NoSpaceLeftOnDeviceException(Throwable cause) { public NoSpaceLeftOnDeviceException(Throwable cause) {
super("底层文件系统的可用空间已耗尽. \nSee: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems", cause); super(cause);
} }
} }

View File

@ -1,121 +0,0 @@
/*
* 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

@ -1,83 +1,92 @@
/* /*
* A simple backup mod for Fabric A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 Copyright (C) 2020 Szum123321
*
* This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package net.szum123321.textile_backup.core; package net.szum123321.textile_backup.core;
import net.minecraft.network.MessageType;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.world.ServerWorld; import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.LiteralText;
import net.minecraft.text.MutableText; import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting; import net.minecraft.util.Formatting;
import net.minecraft.world.World; 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.file.SimplePathVisitor; import org.apache.commons.io.FileUtils;
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.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.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 java.util.UUID;
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);
//I'm keeping this wrapper function for easier backporting public static void notifyPlayers(MinecraftServer server, UUID sender, String msg) {
public static boolean wasSentByPlayer(ServerCommandSource source) { return source.isExecutedByPlayer(); }
public static void notifyPlayers(@NotNull MinecraftServer server, String msg) {
MutableText message = log.getPrefixText(); MutableText message = log.getPrefixText();
message.append(Text.literal(msg).formatted(Formatting.WHITE)); message.append(new LiteralText(msg).formatted(Formatting.WHITE));
server.getPlayerManager().broadcast(message, false); server.getPlayerManager().broadcast(
message,
MessageType.SYSTEM,
sender
);
} }
public static String getLevelName(MinecraftServer server) { public static String getLevelName(MinecraftServer server) {
return ((MinecraftServerSessionAccessor)server).getSession().getDirectoryName(); return ((MinecraftServerSessionAccessor)server).getSession().getDirectoryName();
} }
public static Path getWorldFolder(MinecraftServer server) { public static File getWorldFolder(MinecraftServer server) {
return ((MinecraftServerSessionAccessor)server) return ((MinecraftServerSessionAccessor)server)
.getSession() .getSession()
.getWorldDirectory(World.OVERWORLD); .getWorldDirectory(World.OVERWORLD)
.toFile();
} }
public static void deleteDirectory(Path path) throws IOException { public static File getBackupRootPath(String worldName) {
Files.walkFileTree(path, new SimplePathVisitor() { File path = new File(config.get().path).getAbsoluteFile();
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (config.get().perWorldBackup) path = path.toPath().resolve(worldName).toFile();
Files.delete(file);
return FileVisitResult.CONTINUE; if (!path.exists()) path.mkdirs();
return path;
} }
@Override public static boolean updateTMPFSFlag(MinecraftServer server) {
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Statics.disableTMPFiles = (FileUtils.sizeOfDirectory(Utilities.getWorldFolder(server)) >=
Files.delete(dir); (new File(System.getProperty("java.io.tmpdir"))).getFreeSpace());
return FileVisitResult.CONTINUE;
} if(Statics.disableTMPFiles) log.warn("Not enough space left in tmp directory!\n Might cause: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems");
});
return Statics.disableTMPFiles;
} }
public static void disableWorldSaving(MinecraftServer server) { public static void disableWorldSaving(MinecraftServer server) {
@ -98,34 +107,77 @@ 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 (path.getFileName().equals(Path.of("session.lock"))) return true; if(isWindows()) { //hotfix!
if (path.getFileName().toString().equals("session.lock")) return true;
if(path.getFileName().equals(Path.of(CompressionStatus.DATA_FILENAME))) return true; }
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(File f) {
return getArchiveExtension(f.getName());
}
public static Optional<LocalDateTime> getFileCreationTime(File file) {
LocalDateTime creationTime = null;
if(getArchiveExtension(file).isPresent()) {
String fileExtension = getArchiveExtension(file).get().getCompleteString();
try {
creationTime = LocalDateTime.from(
Utilities.getDateTimeFormatter().parse(
file.getName().split(fileExtension)[0].split("#")[0]
)
);
} catch (Exception ignored) {}
if(creationTime == null) {
try {
creationTime = LocalDateTime.from(
Utilities.getBackupDateTimeFormatter().parse(
file.getName().split(fileExtension)[0].split("#")[0]
)
);
} catch (Exception ignored2){}
}
if(creationTime == null) {
try {
FileTime fileTime = (FileTime) Files.getAttribute(file.toPath(), "creationTime");
creationTime = LocalDateTime.ofInstant(fileTime.toInstant(), ZoneOffset.systemDefault());
} catch (IOException ignored3) {}
}
}
return Optional.ofNullable(creationTime);
}
public static boolean isValidBackup(File f) {
return getArchiveExtension(f).isPresent() && getFileCreationTime(f).isPresent() && isFileOk(f);
}
public static boolean isFileOk(File f) {
return f.exists() && f.isFile();
}
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

@ -0,0 +1,133 @@
/*
* 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.entity.player.PlayerEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.util.Util;
import net.szum123321.textile_backup.core.ActionInitiator;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public record BackupContext(@NotNull MinecraftServer server,
ServerCommandSource commandSource,
ActionInitiator initiator,
boolean save,
String comment) {
public MinecraftServer getServer() {
return server;
}
public ServerCommandSource getCommandSource() {
return commandSource;
}
public ActionInitiator getInitiator() {
return initiator;
}
public boolean startedByPlayer() {
return initiator == ActionInitiator.Player;
}
public boolean shouldSave() {
return save;
}
public String getComment() {
return comment;
}
public UUID getInitiatorUUID() {
return initiator.equals(ActionInitiator.Player) && commandSource.getEntity() != null ? commandSource.getEntity().getUuid(): Util.NIL_UUID;
}
public static class Builder {
private MinecraftServer server;
private ServerCommandSource commandSource;
private ActionInitiator initiator;
private boolean save;
private String comment;
private boolean guessInitiator;
public Builder() {
this.server = null;
this.commandSource = null;
this.initiator = null;
this.save = false;
this.comment = null;
guessInitiator = false;
}
public static Builder newBackupContextBuilder() {
return new Builder();
}
public Builder setCommandSource(ServerCommandSource commandSource) {
this.commandSource = commandSource;
return this;
}
public Builder setServer(MinecraftServer server) {
this.server = server;
return this;
}
public Builder setInitiator(ActionInitiator initiator) {
this.initiator = initiator;
return this;
}
public Builder setComment(String comment) {
this.comment = comment;
return this;
}
public Builder guessInitiator() {
this.guessInitiator = true;
return this;
}
public Builder saveServer() {
this.save = true;
return this;
}
public BackupContext build() {
if (guessInitiator) {
initiator = commandSource.getEntity() instanceof PlayerEntity ? ActionInitiator.Player : ActionInitiator.ServerConsole;
} else if (initiator == null) {
initiator = ActionInitiator.Null;
}
if (server == null) {
if (commandSource != null) setServer(commandSource.getServer());
else
throw new RuntimeException("Neither MinecraftServer or ServerCommandSource were provided!");
}
return new BackupContext(server, commandSource, initiator, save, comment);
}
}
}

View File

@ -0,0 +1,129 @@
/*
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 org.apache.commons.io.FileUtils;
import java.io.File;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Comparator;
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.getServer(),
ctx.getInitiatorUUID(),
"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.getInitiator().getPrefix());
if(ctx.startedByPlayer())
builder.append(ctx.getCommandSource().getDisplayName().getString());
else
builder.append(ctx.getInitiator().getName());
builder.append(" on: ");
builder.append(Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
log.info(builder.toString());
if (ctx.shouldSave()) {
log.sendInfoAL(ctx, "Saving server...");
ctx.getServer().getPlayerManager().saveAllPlayerData();
try {
ctx.getServer().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) {
File root = Utilities.getBackupRootPath(worldName);
int deletedFiles = 0;
if (root.isDirectory() && root.exists() && root.listFiles() != null) {
if (config.get().maxAge > 0) { // delete files older that configured
final LocalDateTime now = LocalDateTime.now();
deletedFiles += Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)// We check if we can get file's creation date so that the next line won't throw an exception
.filter(f -> now.toEpochSecond(ZoneOffset.UTC) - Utilities.getFileCreationTime(f).get().toEpochSecond(ZoneOffset.UTC) > config.get().maxAge)
.map(f -> deleteFile(f, ctx))
.filter(b -> b).count(); //a bit awkward
}
if (config.get().backupsToKeep > 0 && root.listFiles().length > config.get().backupsToKeep) {
deletedFiles += Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime((File) f).get()).reversed())
.skip(config.get().backupsToKeep)
.map(f -> deleteFile(f, ctx))
.filter(b -> b).count();
}
if (config.get().maxSize > 0 && FileUtils.sizeOfDirectory(root) / 1024 > config.get().maxSize) {
deletedFiles += Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get()))
.takeWhile(f -> FileUtils.sizeOfDirectory(root) / 1024 > config.get().maxSize)
.map(f -> deleteFile(f, ctx))
.filter(b -> b).count();
}
}
return deletedFiles;
}
private static boolean deleteFile(File f, ServerCommandSource ctx) {
if(Statics.untouchableFile.isEmpty()|| !Statics.untouchableFile.get().equals(f)) {
if(f.delete()) {
log.sendInfoAL(ctx, "Deleting: {}", f.getName());
return true;
} else {
log.sendErrorAL(ctx, "Something went wrong while deleting: {}.", f.getName());
}
}
return false;
}
}

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -19,69 +19,58 @@
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.Globals; import net.szum123321.textile_backup.Statics;
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;
//Scheduled flag tells whether we have decided to run another backup private boolean scheduled;
private static boolean scheduled = false; private long nextBackup;
private static long nextBackup = - 1;
public static void tick(MinecraftServer server) { public BackupScheduler() {
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) {
//It's time to run Statics.executorService.submit(
Globals.INSTANCE.getQueueExecutor().submit( BackupHelper.create(
ExecutableBackup.Builder BackupContext.Builder
.newBackupContextBuilder() .newBackupContextBuilder()
.setServer(server) .setServer(server)
.setInitiator(ActionInitiator.Timer) .setInitiator(ActionInitiator.Timer)
.saveServer() .saveServer()
.announce()
.build() .build()
)
); );
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) {
//Verify we hadn't done the final one, and it's time to do so Statics.executorService.submit(
Globals.INSTANCE.getQueueExecutor().submit( BackupHelper.create(
ExecutableBackup.Builder BackupContext.Builder
.newBackupContextBuilder() .newBackupContextBuilder()
.setServer(server) .setServer(server)
.setInitiator(ActionInitiator.Timer) .setInitiator(ActionInitiator.Timer)
.saveServer() .saveServer()
.announce()
.build() .build()
)
); );
scheduled = false; scheduled = false;

View File

@ -1,34 +0,0 @@
/*
* 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 java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
public class BrokenFileHandler {
private final Map<String, Exception> store = new HashMap<>();
public void handle(Path file, Exception e) { store.put(file.toString(), e); }
public boolean valid() { return store.isEmpty(); }
public Map<String, Exception> get() {
return store;
}
}

View File

@ -1,231 +0,0 @@
package net.szum123321.textile_backup.core.create;
import net.minecraft.server.MinecraftServer;
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 net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.Cleanup;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.create.compressors.ParallelZipCompressor;
import net.szum123321.textile_backup.core.create.compressors.ZipCompressor;
import net.szum123321.textile_backup.core.create.compressors.tar.AbstractTarArchiver;
import net.szum123321.textile_backup.core.create.compressors.tar.ParallelGzipCompressor;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.NoSuchElementException;
import java.util.concurrent.Callable;
public record ExecutableBackup(@NotNull MinecraftServer server,
ServerCommandSource commandSource,
ActionInitiator initiator,
boolean save,
boolean cleanup,
String comment,
LocalDateTime startDate) implements Callable<Void> {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public boolean startedByPlayer() {
return initiator == ActionInitiator.Player;
}
public void announce() {
if(config.get().broadcastBackupStart) {
Utilities.notifyPlayers(server,
"警告!服务器备份即将开始。您可能会遇到一些延迟."
);
} else {
log.sendInfoAL(this, "Something went wrong while deleting: {}.");
}
StringBuilder builder = new StringBuilder();
builder.append("备份开始 ");
builder.append(initiator.getPrefix());
if(startedByPlayer())
builder.append(commandSource.getDisplayName().getString());
else
builder.append(initiator.getName());
builder.append(" on: ");
builder.append(Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
log.info(builder.toString());
}
@Override
public Void call() throws Exception {
if (save) { //save the world
log.sendInfoAL(this, "保存世界中...");
server.saveAll(true, true, false);
}
Path outFile = Utilities.getBackupRootPath(Utilities.getLevelName(server)).resolve(getFileName());
log.trace("输出备份文件为: {}", outFile);
try {
//I think I should synchronise these two next calls...
Utilities.disableWorldSaving(server);
Globals.INSTANCE.disableWatchdog = true;
Globals.INSTANCE.updateTMPFSFlag(server);
log.sendInfoAL(this, "开始备份");
Path world = Utilities.getWorldFolder(server);
log.trace("Minecraft 存档目录: {}", world);
Files.createDirectories(outFile.getParent());
Files.createFile(outFile);
int coreCount;
if (config.get().compressionCoreCountLimit <= 0) coreCount = Runtime.getRuntime().availableProcessors();
else
coreCount = Math.min(config.get().compressionCoreCountLimit, Runtime.getRuntime().availableProcessors());
log.trace("正在使用{}个线程对{}进行压缩。可用核心数:{}", coreCount, Runtime.getRuntime().availableProcessors());
switch (config.get().format) {
case ZIP -> {
if (coreCount > 1 && !Globals.INSTANCE.disableTMPFS()) {
log.trace("使用并行压缩器进行压缩。线程数:{}", coreCount);
ParallelZipCompressor.getInstance().createArchive(world, outFile, this, coreCount);
} else {
log.trace("使用普通的Zip压缩器进行压缩 (单线程)");
ZipCompressor.getInstance().createArchive(world, outFile, this, coreCount);
}
}
case GZIP -> ParallelGzipCompressor.getInstance().createArchive(world, outFile, this, coreCount);
case TAR -> new AbstractTarArchiver().createArchive(world, outFile, this, coreCount);
}
if(cleanup) new Cleanup(commandSource, Utilities.getLevelName(server)).call();
if (config.get().broadcastBackupDone) Utilities.notifyPlayers(server, "完成!");
else log.sendInfoAL(this, "完成!");
} catch (Throwable e) {
//ExecutorService swallows exception, so I need to catch everything
log.error("在尝试创建新的备份文件时发生了异常!", e);
if (ConfigHelper.INSTANCE.get().integrityVerificationMode.isStrict()) {
try {
Files.delete(outFile);
} catch (IOException ex) {
log.error("在尝试删除{}时发生了异常!", outFile, ex);
}
}
if (initiator == ActionInitiator.Player)
log.sendError(this, "在尝试创建新的备份文件时发生了异常!");
throw e;
} finally {
Utilities.enableWorldSaving(server);
Globals.INSTANCE.disableWatchdog = false;
}
return null;
}
private String getFileName() {
return Utilities.getDateTimeFormatter().format(startDate) +
(comment != null ? "#" + comment.replaceAll("[\\\\/:*?\"<>|#]", "") : "") +
config.get().format.getCompleteString();
}
public static class Builder {
private MinecraftServer server;
private ServerCommandSource commandSource;
private ActionInitiator initiator;
private boolean save;
private boolean cleanup;
private String comment;
private boolean announce;
private boolean guessInitiator;
public Builder() {
this.server = null;
this.commandSource = null;
this.initiator = null;
this.save = false;
cleanup = true; //defaults
this.comment = null;
this.announce = false;
guessInitiator = false;
}
public static ExecutableBackup.Builder newBackupContextBuilder() {
return new ExecutableBackup.Builder();
}
public ExecutableBackup.Builder setCommandSource(ServerCommandSource commandSource) {
this.commandSource = commandSource;
return this;
}
public ExecutableBackup.Builder setServer(MinecraftServer server) {
this.server = server;
return this;
}
public ExecutableBackup.Builder setInitiator(ActionInitiator initiator) {
this.initiator = initiator;
return this;
}
public ExecutableBackup.Builder setComment(String comment) {
this.comment = comment;
return this;
}
public ExecutableBackup.Builder guessInitiator() {
this.guessInitiator = true;
return this;
}
public ExecutableBackup.Builder saveServer() {
this.save = true;
return this;
}
public ExecutableBackup.Builder noCleanup() {
this.cleanup = false;
return this;
}
public ExecutableBackup.Builder announce() {
this.announce = true;
return this;
}
public ExecutableBackup build() {
if (guessInitiator) {
initiator = Utilities.wasSentByPlayer(commandSource) ? ActionInitiator.Player : ActionInitiator.ServerConsole;
} else if (initiator == null) throw new NoSuchElementException("未提供发起者!");
if (server == null) {
if (commandSource != null) setServer(commandSource.getServer());
else throw new RuntimeException("未提供MinecraftServer或ServerCommandSource");
}
ExecutableBackup v = new ExecutableBackup(server, commandSource, initiator, save, cleanup, comment, LocalDateTime.now());
if(announce) v.announce();
return v;
}
}
}

View File

@ -1,70 +0,0 @@
/*
* 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.core.digest.FileTreeHashBuilder;
import net.szum123321.textile_backup.core.digest.HashingInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
public record FileInputStreamSupplier(Path path, String name, FileTreeHashBuilder hashTreeBuilder, BrokenFileHandler brokenFileHandler) implements InputSupplier {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
@Override
public InputStream getInputStream() throws IOException {
try {
return new HashingInputStream(Files.newInputStream(path), path, hashTreeBuilder, brokenFileHandler);
} catch (IOException e) {
//Probably good idea to just put it here. In the case an exception is thrown here, it could be possible
//The latch would have never been lifted
hashTreeBuilder.update(path, 0, 0);
brokenFileHandler.handle(path, e);
throw e;
}
}
@Override
public Optional<Path> getPath() { return Optional.of(path); }
@Override
public long size() throws IOException { return Files.size(path); }
@Override
public String getName() {
return name;
}
@Override
public InputStream get() {
try {
return getInputStream();
} catch (IOException e) {
log.error("尝试从文件{}创建输入流时发生了异常!", path.toString(), e);
}
return null;
}
}

View File

@ -1,35 +0,0 @@
/*
* 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 org.apache.commons.compress.parallel.InputStreamSupplier;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Optional;
public interface InputSupplier extends InputStreamSupplier {
InputStream getInputStream() throws IOException;
//If an entry is virtual (a.k.a. there is no actual file to open, only input stream)
Optional<Path> getPath();
String getName();
long size() throws IOException;
}

View File

@ -0,0 +1,140 @@
/*
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.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.create.compressors.*;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.create.compressors.tar.AbstractTarArchiver;
import net.szum123321.textile_backup.core.create.compressors.tar.ParallelBZip2Compressor;
import net.szum123321.textile_backup.core.create.compressors.tar.ParallelGzipCompressor;
import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.time.LocalDateTime;
public class MakeBackupRunnable implements Runnable {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
private final BackupContext context;
public MakeBackupRunnable(BackupContext context){
this.context = context;
}
@Override
public void run() {
try {
Utilities.disableWorldSaving(context.getServer());
Statics.disableWatchdog = true;
Utilities.updateTMPFSFlag(context.getServer());
log.sendInfoAL(context, "Starting backup");
File world = Utilities.getWorldFolder(context.getServer());
log.trace("Minecraft world is: {}", world);
File outFile = Utilities
.getBackupRootPath(Utilities.getLevelName(context.getServer()))
.toPath()
.resolve(getFileName())
.toFile();
log.trace("Outfile is: {}", outFile);
outFile.getParentFile().mkdirs();
try {
outFile.createNewFile();
} catch (IOException e) {
log.error("An exception occurred when trying to create new backup file!", e);
if(context.getInitiator() == ActionInitiator.Player)
log.sendError(context, "An exception occurred when trying to create new backup file!");
return;
}
int coreCount;
if(config.get().compressionCoreCountLimit <= 0) {
coreCount = Runtime.getRuntime().availableProcessors();
} else {
coreCount = Math.min(config.get().compressionCoreCountLimit, Runtime.getRuntime().availableProcessors());
}
log.trace("Running compression on {} threads. Available cores: {}", coreCount, Runtime.getRuntime().availableProcessors());
switch (config.get().format) {
case ZIP -> {
if (coreCount > 1 && !Statics.disableTMPFiles)
ParallelZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
else
ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
}
case BZIP2 -> ParallelBZip2Compressor.getInstance().createArchive(world, outFile, context, coreCount);
case GZIP -> ParallelGzipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
case LZMA -> new AbstractTarArchiver() {
protected OutputStream getCompressorOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException {
return new LZMACompressorOutputStream(stream);
}
}.createArchive(world, outFile, context, coreCount);
case TAR -> new AbstractTarArchiver().createArchive(world, outFile, context, coreCount);
default -> {
log.warn("Specified compressor ({}) is not supported! Zip will be used instead!", config.get().format);
if (context.getInitiator() == ActionInitiator.Player)
log.sendError(context.getCommandSource(), "Error! No correct compression format specified! Using default compressor!");
ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
}
}
BackupHelper.executeFileLimit(context.getCommandSource(), Utilities.getLevelName(context.getServer()));
if(config.get().broadcastBackupDone) {
Utilities.notifyPlayers(
context.getServer(),
context.getInitiatorUUID(),
"Done!"
);
} else {
log.sendInfoAL(context, "Done!");
}
} finally {
Utilities.enableWorldSaving(context.getServer());
Statics.disableWatchdog = false;
}
}
private String getFileName(){
LocalDateTime now = LocalDateTime.now();
return Utilities.getDateTimeFormatter().format(now) +
(context.getComment() != null ? "#" + context.getComment().replace("#", "") : "") +
config.get().format.getCompleteString();
}
}

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -18,114 +18,81 @@
package net.szum123321.textile_backup.core.create.compressors; package net.szum123321.textile_backup.core.create.compressors;
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.core.ActionInitiator;
import net.szum123321.textile_backup.core.*; import net.szum123321.textile_backup.core.NoSpaceLeftOnDeviceException;
import net.szum123321.textile_backup.core.create.BrokenFileHandler; import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.create.ExecutableBackup; import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.core.create.FileInputStreamSupplier;
import net.szum123321.textile_backup.core.create.InputSupplier;
import net.szum123321.textile_backup.core.digest.FileTreeHashBuilder;
import java.io.*; import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
/**
* Basic abstract class representing directory compressor with all the bells and whistles
*/
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);
public void createArchive(Path inputFile, Path outputFile, ExecutableBackup ctx, int coreLimit) throws IOException, ExecutionException, InterruptedException { public void createArchive(File inputFile, File outputFile, BackupContext ctx, int coreLimit) {
Instant start = Instant.now(); Instant start = Instant.now();
BrokenFileHandler brokenFileHandler = new BrokenFileHandler(); //Basically a hashmap storing files and their respective exceptions try (FileOutputStream outStream = new FileOutputStream(outputFile);
try (OutputStream outStream = Files.newOutputStream(outputFile);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outStream); BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outStream);
OutputStream arc = createArchiveOutputStream(bufferedOutputStream, ctx, coreLimit); OutputStream arc = createArchiveOutputStream(bufferedOutputStream, ctx, coreLimit)) {
Stream<Path> fileStream = Files.walk(inputFile)) {
var fileList = fileStream Files.walk(inputFile.toPath())
.filter(path -> !Utilities.isBlacklisted(inputFile.relativize(path))) .filter(path -> !Utilities.isBlacklisted(inputFile.toPath().relativize(path)))
.filter(Files::isRegularFile) .map(Path::toFile)
.toList(); .filter(File::isFile)
.forEach(file -> {
FileTreeHashBuilder fileHashBuilder = new FileTreeHashBuilder(fileList.size());
for (Path file : fileList) {
try { try {
addEntry( //hopefully one broken file won't spoil the whole archive
new FileInputStreamSupplier( addEntry(file, inputFile.toPath().relativize(file.toPath()).toString(), arc);
file,
inputFile.relativize(file).toString(),
fileHashBuilder,
brokenFileHandler),
arc
);
} catch (IOException e) { } catch (IOException e) {
brokenFileHandler.handle(file, e); log.error("An exception occurred while trying to compress: {}", inputFile.toPath().relativize(file.toPath()).toString(), e);
fileHashBuilder.update(file, 0, 0);
//In Permissive mode we allow partial backups if (ctx.getInitiator() == ActionInitiator.Player)
if (ConfigHelper.INSTANCE.get().integrityVerificationMode.isStrict()) throw e; log.sendError(ctx, "Something went wrong while compressing files!");
else log.sendErrorAL(ctx, "在尝试压缩{}时发生了异常!",
inputFile.relativize(file).toString(), e
);
} }
} });
arc.flush();
//wait for all the InputStreams to close/fail with InputSupplier
Instant now = Instant.now();
long treeHash = fileHashBuilder.getValue(true);
CompressionStatus status = new CompressionStatus (
treeHash,
brokenFileHandler.get(),
ctx.startDate(), start.toEpochMilli(), now.toEpochMilli(),
Globals.INSTANCE.getCombinedVersionString()
);
addEntry(new StatusFileInputSupplier(status.serialize()), arc);
finish(arc); finish(arc);
} catch(NoSpaceLeftOnDeviceException e) {
log.error("""
CRITICAL ERROR OCCURRED!
The backup is corrupt!
Don't panic! This is a known issue!
For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems
In case this isn't it here's also the exception itself""", e);
if(ctx.getInitiator() == ActionInitiator.Player) {
log.sendError(ctx, "Backup failed. The file is corrupt.");
log.error("For help see: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems");
}
} catch (IOException | InterruptedException | ExecutionException e) {
log.error("An exception occurred!", e);
} catch (Exception e) {
if(ctx.getInitiator() == ActionInitiator.Player)
log.sendError(ctx, "Something went wrong while compressing files!");
} finally { } finally {
close(); close();
} }
log.sendInfoAL(ctx, "压缩耗时:{}秒. ", Utilities.formatDuration(Duration.between(start, Instant.now()))); // close();
log.sendInfoAL(ctx, "Compression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
} }
protected abstract OutputStream createArchiveOutputStream(OutputStream stream, ExecutableBackup ctx, int coreLimit) throws IOException; protected abstract OutputStream createArchiveOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException;
protected abstract void addEntry(InputSupplier inputSupplier, OutputStream arc) throws IOException; protected abstract void addEntry(File 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 {
//This function is only needed for the ParallelZipCompressor to write out ParallelScatterZipCreator ;//Basically this function is only needed for the ParallelZipCompressor to write out ParallelScatterZipCreator
} }
protected void close() { protected void close() {
//Same as above, just for ParallelGzipCompressor to shut down ExecutorService ;//Same as above, just for ParallelGzipCompressor to shutdown ExecutorService
}
private record StatusFileInputSupplier(byte[] data) implements InputSupplier {
public InputStream getInputStream() { return new ByteArrayInputStream(data); }
public Optional<Path> getPath() { return Optional.empty(); }
public String getName() { return CompressionStatus.DATA_FILENAME; }
public long size() { return data.length; }
public InputStream get() { return getInputStream(); }
}
} }
}

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -21,13 +21,11 @@ package net.szum123321.textile_backup.core.create.compressors;
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.NoSpaceLeftOnDeviceException; import net.szum123321.textile_backup.core.NoSpaceLeftOnDeviceException;
import net.szum123321.textile_backup.core.create.ExecutableBackup; import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.core.create.InputSupplier;
import org.apache.commons.compress.archivers.zip.*; import org.apache.commons.compress.archivers.zip.*;
import org.apache.commons.compress.parallel.InputStreamSupplier;
import java.io.*; import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
@ -42,7 +40,7 @@ public class ParallelZipCompressor extends ZipCompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME); private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
//These fields are used to discriminate against the issue #51 //These fields are used to discriminate against the issue #51
private final static SimpleStackTraceElement[] STACKTRACE_NO_SPACE_ON_LEFT_ON_DEVICE = { private final static SimpleStackTraceElement[] STACKTRACE = {
new SimpleStackTraceElement("sun.nio.ch.FileDispatcherImpl", "write0", true), new SimpleStackTraceElement("sun.nio.ch.FileDispatcherImpl", "write0", true),
new SimpleStackTraceElement("sun.nio.ch.FileDispatcherImpl", "write", false), new SimpleStackTraceElement("sun.nio.ch.FileDispatcherImpl", "write", false),
new SimpleStackTraceElement("sun.nio.ch.IOUtil", "writeFromNativeBuffer", false), new SimpleStackTraceElement("sun.nio.ch.IOUtil", "writeFromNativeBuffer", false),
@ -61,39 +59,32 @@ public class ParallelZipCompressor extends ZipCompressor {
} }
@Override @Override
protected OutputStream createArchiveOutputStream(OutputStream stream, ExecutableBackup ctx, int coreLimit) { protected OutputStream createArchiveOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) {
scatterZipCreator = new ParallelScatterZipCreator(Executors.newFixedThreadPool(coreLimit)); scatterZipCreator = new ParallelScatterZipCreator(Executors.newFixedThreadPool(coreLimit));
return super.createArchiveOutputStream(stream, ctx, coreLimit); return super.createArchiveOutputStream(stream, ctx, coreLimit);
} }
@Override @Override
protected void addEntry(InputSupplier input, OutputStream arc) throws IOException { protected void addEntry(File file, String entryName, OutputStream arc) throws IOException {
ZipArchiveEntry entry; ZipArchiveEntry entry = (ZipArchiveEntry)((ZipArchiveOutputStream)arc).createArchiveEntry(file, entryName);
if(input.getPath().isEmpty()) {
entry = new ZipArchiveEntry(input.getName()); if(ZipCompressor.isDotDat(file.getName())) {
entry.setMethod(ZipEntry.STORED); entry.setMethod(ZipArchiveOutputStream.STORED);
entry.setSize(input.size()); entry.setSize(file.length());
} else { entry.setCompressedSize(file.length());
Path file = input.getPath().get();
entry = (ZipArchiveEntry) ((ZipArchiveOutputStream) arc).createArchiveEntry(file, input.getName());
if (ZipCompressor.isDotDat(file.toString())) {
entry.setMethod(ZipEntry.STORED);
entry.setSize(Files.size(file));
entry.setCompressedSize(Files.size(file));
entry.setCrc(getCRC(file)); entry.setCrc(getCRC(file));
} else entry.setMethod(ZipEntry.DEFLATED); } else entry.setMethod(ZipEntry.DEFLATED);
}
entry.setTime(System.currentTimeMillis()); entry.setTime(System.currentTimeMillis());
scatterZipCreator.addArchiveEntry(entry, input); scatterZipCreator.addArchiveEntry(entry, new FileInputStreamSupplier(file));
} }
@Override @Override
protected void finish(OutputStream arc) throws InterruptedException, IOException, ExecutionException { protected void finish(OutputStream arc) throws InterruptedException, IOException, ExecutionException {
/* /*
This is perhaps the most dreadful line of this whole mess This is perhaps the most dreadful line of this whole mess
This line causes the infamous Out of space error (#20 and #80) This line causes the infamous Out of space error
*/ */
try { try {
scatterZipCreator.writeTo((ZipArchiveOutputStream) arc); scatterZipCreator.writeTo((ZipArchiveOutputStream) arc);
@ -101,12 +92,15 @@ public class ParallelZipCompressor extends ZipCompressor {
Throwable cause; Throwable cause;
if((cause = e.getCause()).getClass().equals(IOException.class)) { if((cause = e.getCause()).getClass().equals(IOException.class)) {
//The out of space exception is thrown at sun.nio.ch.FileDispatcherImpl.write0(Native Method) //The out of space exception is thrown at sun.nio.ch.FileDispatcherImpl.write0(Native Method)
boolean match = (cause.getStackTrace().length >= STACKTRACE_NO_SPACE_ON_LEFT_ON_DEVICE.length); boolean match = (cause.getStackTrace().length >= STACKTRACE.length);
if(match) { if(match) {
for(int i = 0; i < STACKTRACE_NO_SPACE_ON_LEFT_ON_DEVICE.length && match; i++) for(int i = 0; i < STACKTRACE.length && match; i++)
if(!STACKTRACE_NO_SPACE_ON_LEFT_ON_DEVICE[i].matches(cause.getStackTrace()[i])) match = false; if(!STACKTRACE[i].equals(cause.getStackTrace()[i])) {
//Statics.LOGGER.error("Mismatch at: {}, classname: {}, methodname: {}, {}", i, cause.getStackTrace()[i].getClassName(), cause.getStackTrace()[i].getMethodName());
match = false;
}
//For clarity's sake let's not throw the ExecutionException itself rather only the cause, as the EE is just the wrapper //For clarity sake let's not throw the ExecutionException itself rather only the cause, as the EE is just the wrapper
if(match) throw new NoSpaceLeftOnDeviceException(cause); if(match) throw new NoSpaceLeftOnDeviceException(cause);
} }
} }
@ -115,13 +109,34 @@ public class ParallelZipCompressor extends ZipCompressor {
} }
} }
private record SimpleStackTraceElement ( private static record SimpleStackTraceElement (
String className, String className,
String methodName, String methodName,
boolean isNative boolean isNative
) { ) {
public boolean matches(StackTraceElement o) { @Override
return (isNative == o.isNativeMethod()) && Objects.equals(className, o.getClassName()) && Objects.equals(methodName, o.getMethodName()); public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if(o.getClass() == StackTraceElement.class) {
StackTraceElement that = (StackTraceElement) o;
return (isNative == that.isNativeMethod()) && Objects.equals(className, that.getClassName()) && Objects.equals(methodName, that.getMethodName());
}
if(getClass() != o.getClass()) return false;
SimpleStackTraceElement that = (SimpleStackTraceElement) o;
return isNative == that.isNative && Objects.equals(className, that.className) && Objects.equals(methodName, that.methodName);
}
}
record FileInputStreamSupplier(File sourceFile) implements InputStreamSupplier {
public InputStream get() {
try {
return new FileInputStream(sourceFile);
} catch (IOException e) {
log.error("An exception occurred while trying to create an input stream from file: {}!", sourceFile.getName(), e);
}
return null;
} }
} }
} }

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -20,20 +20,16 @@ package net.szum123321.textile_backup.core.create.compressors;
import net.szum123321.textile_backup.config.ConfigHelper; import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.Utilities; import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.create.ExecutableBackup; import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.core.create.InputSupplier;
import org.apache.commons.compress.archivers.zip.Zip64Mode; import org.apache.commons.compress.archivers.zip.Zip64Mode;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.compress.utils.IOUtils;
import java.io.*; import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.zip.CRC32; import java.util.zip.CRC32;
import java.util.zip.Checksum; import java.util.zip.Checksum;
import java.util.zip.ZipEntry;
public class ZipCompressor extends AbstractCompressor { public class ZipCompressor extends AbstractCompressor {
private final static ConfigHelper config = ConfigHelper.INSTANCE; private final static ConfigHelper config = ConfigHelper.INSTANCE;
@ -43,7 +39,7 @@ public class ZipCompressor extends AbstractCompressor {
} }
@Override @Override
protected OutputStream createArchiveOutputStream(OutputStream stream, ExecutableBackup ctx, int coreLimit) { protected OutputStream createArchiveOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) {
ZipArchiveOutputStream arc = new ZipArchiveOutputStream(stream); ZipArchiveOutputStream arc = new ZipArchiveOutputStream(stream);
arc.setMethod(ZipArchiveOutputStream.DEFLATED); arc.setMethod(ZipArchiveOutputStream.DEFLATED);
@ -55,23 +51,15 @@ public class ZipCompressor extends AbstractCompressor {
} }
@Override @Override
protected void addEntry(InputSupplier input, OutputStream arc) throws IOException { protected void addEntry(File file, String entryName, OutputStream arc) throws IOException {
try (InputStream fileInputStream = input.getInputStream()) { try (FileInputStream fileInputStream = new FileInputStream(file)){
ZipArchiveEntry entry; ZipArchiveEntry entry = (ZipArchiveEntry)((ZipArchiveOutputStream)arc).createArchiveEntry(file, entryName);
if(input.getPath().isEmpty()) { if(isDotDat(file.getName())) {
entry = new ZipArchiveEntry(input.getName()); entry.setMethod(ZipArchiveOutputStream.STORED);
entry.setMethod(ZipEntry.STORED); entry.setSize(file.length());
entry.setSize(input.size()); entry.setCompressedSize(file.length());
} else {
Path file = input.getPath().get();
entry = (ZipArchiveEntry) ((ZipArchiveOutputStream) arc).createArchiveEntry(file, input.getName());
if (isDotDat(file.toString())) {
entry.setMethod(ZipEntry.STORED);
entry.setSize(Files.size(file));
entry.setCompressedSize(Files.size(file));
entry.setCrc(getCRC(file)); entry.setCrc(getCRC(file));
} else entry.setMethod(ZipEntry.DEFLATED);
} }
((ZipArchiveOutputStream)arc).putArchiveEntry(entry); ((ZipArchiveOutputStream)arc).putArchiveEntry(entry);
@ -88,15 +76,15 @@ public class ZipCompressor extends AbstractCompressor {
return arr[arr.length - 1].contains("dat"); //includes dat_old return arr[arr.length - 1].contains("dat"); //includes dat_old
} }
protected static long getCRC(Path file) throws IOException { protected static long getCRC(File file) throws IOException {
Checksum sum = new CRC32(); Checksum sum = new CRC32();
byte[] buffer = new byte[8192]; byte[] buffer = new byte[8192];
int len; int len;
try (InputStream stream = Files.newInputStream(file)) { try (InputStream stream = new FileInputStream(file)) {
while ((len = stream.read(buffer)) != -1) sum.update(buffer, 0, len); while ((len = stream.read(buffer)) != -1) sum.update(buffer, 0, len);
} catch (IOException e) { } catch (IOException e) {
throw new IOException("Error while calculating CRC of: " + file.toAbsolutePath(), e); throw new IOException("Error while calculating CRC of: " + file.getAbsolutePath(), e);
} }
return sum.getValue(); return sum.getValue();

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -18,22 +18,24 @@
package net.szum123321.textile_backup.core.create.compressors.tar; package net.szum123321.textile_backup.core.create.compressors.tar;
import net.szum123321.textile_backup.core.create.ExecutableBackup; import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.core.create.compressors.AbstractCompressor; import net.szum123321.textile_backup.core.create.compressors.AbstractCompressor;
import net.szum123321.textile_backup.core.create.InputSupplier;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.compress.utils.IOUtils;
import java.io.*; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
public class AbstractTarArchiver extends AbstractCompressor { public class AbstractTarArchiver extends AbstractCompressor {
protected OutputStream getCompressorOutputStream(OutputStream stream, ExecutableBackup ctx, int coreLimit) throws IOException { protected OutputStream getCompressorOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException {
return stream; return stream;
} }
@Override @Override
protected OutputStream createArchiveOutputStream(OutputStream stream, ExecutableBackup ctx, int coreLimit) throws IOException { protected OutputStream createArchiveOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException {
TarArchiveOutputStream tar = new TarArchiveOutputStream(getCompressorOutputStream(stream, ctx, coreLimit)); TarArchiveOutputStream tar = new TarArchiveOutputStream(getCompressorOutputStream(stream, ctx, coreLimit));
tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
tar.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); tar.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
@ -42,15 +44,9 @@ public class AbstractTarArchiver extends AbstractCompressor {
} }
@Override @Override
protected void addEntry(InputSupplier input, OutputStream arc) throws IOException { protected void addEntry(File file, String entryName, OutputStream arc) throws IOException {
try (InputStream fileInputStream = input.getInputStream()) { try (FileInputStream fileInputStream = new FileInputStream(file)){
TarArchiveEntry entry; TarArchiveEntry entry = (TarArchiveEntry)((TarArchiveOutputStream) arc).createArchiveEntry(file, entryName);
if(input.getPath().isEmpty()) { //Virtual entry
entry = new TarArchiveEntry(input.getName());
entry.setSize(input.size());
} else
entry = (TarArchiveEntry)((TarArchiveOutputStream) arc).createArchiveEntry(input.getPath().get(), input.getName());
((TarArchiveOutputStream)arc).putArchiveEntry(entry); ((TarArchiveOutputStream)arc).putArchiveEntry(entry);
IOUtils.copy(fileInputStream, arc); IOUtils.copy(fileInputStream, arc);

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -18,7 +18,7 @@
package net.szum123321.textile_backup.core.create.compressors.tar; package net.szum123321.textile_backup.core.create.compressors.tar;
import net.szum123321.textile_backup.core.create.ExecutableBackup; import net.szum123321.textile_backup.core.create.BackupContext;
import org.at4j.comp.bzip2.BZip2OutputStream; import org.at4j.comp.bzip2.BZip2OutputStream;
import org.at4j.comp.bzip2.BZip2OutputStreamSettings; import org.at4j.comp.bzip2.BZip2OutputStreamSettings;
@ -30,7 +30,7 @@ public class ParallelBZip2Compressor extends AbstractTarArchiver {
} }
@Override @Override
protected OutputStream getCompressorOutputStream(OutputStream stream, ExecutableBackup ctx, int coreLimit) throws IOException { protected OutputStream getCompressorOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException {
return new BZip2OutputStream(stream, new BZip2OutputStreamSettings().setNumberOfEncoderThreads(coreLimit)); return new BZip2OutputStream(stream, new BZip2OutputStreamSettings().setNumberOfEncoderThreads(coreLimit));
} }
} }

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -18,7 +18,7 @@
package net.szum123321.textile_backup.core.create.compressors.tar; package net.szum123321.textile_backup.core.create.compressors.tar;
import net.szum123321.textile_backup.core.create.ExecutableBackup; import net.szum123321.textile_backup.core.create.BackupContext;
import org.anarres.parallelgzip.ParallelGZIPOutputStream; import org.anarres.parallelgzip.ParallelGZIPOutputStream;
import java.io.*; import java.io.*;
@ -33,7 +33,7 @@ public class ParallelGzipCompressor extends AbstractTarArchiver {
} }
@Override @Override
protected OutputStream getCompressorOutputStream(OutputStream stream, ExecutableBackup ctx, int coreLimit) throws IOException { protected OutputStream getCompressorOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException {
executorService = Executors.newFixedThreadPool(coreLimit); executorService = Executors.newFixedThreadPool(coreLimit);
return new ParallelGZIPOutputStream(stream, executorService); return new ParallelGZIPOutputStream(stream, executorService);

View File

@ -1,100 +0,0 @@
/*
* 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.digest;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
/**
* This algorithm copies the construction of <a href="https://ticki.github.io/blog/seahash-explained/">SeaHash</a> including its IV.
* What it differs in is that it uses Xoroshift64* instead of PCG as its pseudo-random function. Although it might lower
* the output quality, I don't think it matters that much, honestly. One advantage of xoroshift is that it should be
* easier to implement with AVX. Java should soon ship its vector api by default.
*/
public class BalticHash implements Hash {
//SeaHash IV
protected final static long[] IV = { 0x16f11fe89b0d677cL, 0xb480a793d8e6c86cL, 0x6fe2e5aaf078ebc9L, 0x14f994a4c5259381L };
private final long[] state = Arrays.copyOf(IV, IV.length);
protected final int buffer_limit = state.length * Long.BYTES;
protected final byte[] _byte_buffer = new byte[(state.length + 1) * Long.BYTES];
//Enforce endianness
protected final ByteBuffer buffer = ByteBuffer.wrap(_byte_buffer).order(ByteOrder.LITTLE_ENDIAN);
protected long hashed_data_length = 0;
public void update(int b) {
buffer.put((byte)b);
hashed_data_length += 1;
if (buffer.position() >= buffer_limit) round();
}
public void update(long b) {
buffer.putLong(b);
hashed_data_length += Long.BYTES;
if(buffer.position() >= buffer_limit) round();
}
public void update(byte[] data, int off, int len) {
int pos = 0;
while(pos < len) {
int n = Math.min(len - pos, buffer_limit - buffer.position());
System.arraycopy(data, off + pos, _byte_buffer, buffer.position(), n);
pos += n;
buffer.position(buffer.position() + n);
if(buffer.position() >= buffer_limit) round();
}
hashed_data_length += len;
}
public long getValue() {
if(buffer.position() != 0) {
while(buffer.position() < buffer_limit) buffer.put((byte)0);
round();
}
long result = state[0];
result ^= state[1];
result ^= state[2];
result ^= state[3];
result ^= hashed_data_length;
return xorshift64star(result);
}
protected void round() {
int p = buffer.position();
buffer.rewind();
for(int i = 0; i < 4; i++) state[i] ^= buffer.getLong();
for(int i = 0; i < 4; i++) state[i] = xorshift64star(state[i]);
if(p > buffer_limit) {
System.arraycopy(_byte_buffer, buffer_limit, _byte_buffer, 0, buffer.limit() - p);
buffer.position(buffer.limit() - p);
} else buffer.rewind();
}
long xorshift64star(long s) {
s ^= (s >> 12);
s ^= (s << 25);
s ^= (s >> 27);
return s * 0x2545F4914F6CDD1DL;
}
}

View File

@ -1,73 +0,0 @@
/*
* 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.digest;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.CompressionStatus;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.CountDownLatch;
/**
* What this class does is it collects the hashed files and combines them into a single number,
* thus we can verify file tree integrity
*/
public class FileTreeHashBuilder {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final Object lock = new Object();
private long hash = 0, filesProcessed = 0, filesTotalSize = 0;
private final CountDownLatch latch;
public FileTreeHashBuilder(int filesToProcess) {
latch = new CountDownLatch(filesToProcess);
}
public void update(Path path, long newHash, long bytes) throws IOException {
if(path.getFileName().toString().equals(CompressionStatus.DATA_FILENAME)) return;
latch.countDown();
synchronized (lock) {
this.hash ^= newHash;
filesTotalSize += bytes;
filesProcessed++;
}
}
public int getRemaining() { return (int) latch.getCount(); }
public long getValue(boolean lock) throws InterruptedException {
long leftover = latch.getCount();
if(lock) latch.await();
else if(leftover != 0) log.warn("处理中,剩余{}个文件未处理!", leftover);
var hasher = Globals.CHECKSUM_SUPPLIER.get();
log.debug("文件数:{},字节数:{},原始哈希值:{}", filesProcessed, filesTotalSize, hash);
hasher.update(hash);
hasher.update(filesProcessed);
hasher.update(filesTotalSize);
return hasher.getValue();
}
}

View File

@ -1,32 +0,0 @@
/*
* 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.digest;
public interface Hash {
void update(int b);
void update(long b);
default void update(byte[] b) { update(b, 0, b.length); }
void update(byte[] b, int off, int len);
long getValue();
}

View File

@ -1,98 +0,0 @@
/*
* 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.digest;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.core.DataLeftException;
import net.szum123321.textile_backup.core.create.BrokenFileHandler;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
/**
* This class calculates a hash of the file on the input stream, submits it to FileTreeHashBuilder.
* In case the underlying stream hasn't been read completely in, puts it into BrokeFileHandler
* Furthermore, ParallelZip works by putting all the file requests into a queue and then compressing them
* with multiple threads. Thus, we have to make sure that all the files have been read before requesting the final value
* That is what CountDownLatch does
*/
public class HashingInputStream extends FilterInputStream {
private final Path path;
private final Hash hash = Globals.CHECKSUM_SUPPLIER.get();
private final FileTreeHashBuilder hashBuilder;
private final BrokenFileHandler brokenFileHandler;
private long bytesWritten = 0;
public HashingInputStream(InputStream in, Path path, FileTreeHashBuilder hashBuilder, BrokenFileHandler brokenFileHandler) {
super(in);
this.path = path;
this.hashBuilder = hashBuilder;
this.brokenFileHandler = brokenFileHandler;
}
@Override
public int read(byte @NotNull [] b, int off, int len) throws IOException {
int i;
try {
i = in.read(b, off, len);
} catch(IOException e) {
throw new IOException("An exception occurred while trying to access: [" + path.toString() + "]", e);
}
if(i != -1) {
hash.update(b, off, i);
bytesWritten += i;
}
return i;
}
@Override
public int read() throws IOException {
int i;
try {
i = in.read();
} catch(IOException e) {
throw new IOException("An exception occurred while trying to access: [" + path.toString() + "]", e);
}
if(i != -1) {
hash.update(i);
bytesWritten++;
}
return i;
}
@Override
public boolean markSupported() {
return false;
}
@Override
public void close() throws IOException {
hash.update(path.getFileName().toString().getBytes(StandardCharsets.UTF_8));
hashBuilder.update(path, hash.getValue(), bytesWritten);
if(in.available() != 0) brokenFileHandler.handle(path, new DataLeftException(in.available()));
super.close();
}
}

View File

@ -1,63 +0,0 @@
/*
* 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.digest;
import net.szum123321.textile_backup.Globals;
import org.jetbrains.annotations.NotNull;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
public class HashingOutputStream extends FilterOutputStream {
private final Path path;
private final Hash hash = Globals.CHECKSUM_SUPPLIER.get();
private final FileTreeHashBuilder hashBuilder;
private long bytesWritten = 0;
public HashingOutputStream(OutputStream out, Path path, FileTreeHashBuilder hashBuilder) {
super(out);
this.path = path;
this.hashBuilder = hashBuilder;
}
@Override
public void write(int b) throws IOException {
out.write(b);
hash.update(b);
bytesWritten++;
}
@Override
public void write(byte @NotNull [] b, int off, int len) throws IOException {
out.write(b, off, len);
hash.update(b, off, len);
bytesWritten += len;
}
@Override
public void close() throws IOException {
hash.update(path.getFileName().toString().getBytes(StandardCharsets.UTF_8));
hashBuilder.update(path, hash.getValue(), bytesWritten);
super.close();
}
}

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -42,13 +42,13 @@ public class AwaitThread extends Thread {
@Override @Override
public void run() { public void run() {
log.info("开始倒计时...等待{}秒.", delay); log.info("Countdown begins... Waiting {} second.", delay);
// 𝄞 This is final count down! Tu ruru Tu, Tu Ru Tu Tu ♪ // 𝄞 This is final count down! Tu ruru Tu, Tu Ru Tu Tu ♪
try { try {
Thread.sleep(delay * 1000L); Thread.sleep(delay * 1000L);
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.info("备份恢复已取消."); log.info("Backup restoration cancelled.");
return; return;
} }

View File

@ -1,45 +1,38 @@
/* /*
* A simple backup mod for Fabric A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 Copyright (C) 2020 Szum123321
*
* This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package net.szum123321.textile_backup.core.restore; package net.szum123321.textile_backup.core.restore;
import net.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.CompressionStatus; 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.ExecutableBackup; import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.core.create.BackupHelper;
import net.szum123321.textile_backup.core.restore.decompressors.GenericTarDecompressor; import net.szum123321.textile_backup.core.restore.decompressors.GenericTarDecompressor;
import net.szum123321.textile_backup.core.restore.decompressors.ZipDecompressor; import net.szum123321.textile_backup.core.restore.decompressors.ZipDecompressor;
import net.szum123321.textile_backup.mixin.MinecraftServerSessionAccessor;
import java.io.IOException; import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.FutureTask;
/**
* This class restores a file provided by RestoreContext.
*/
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;
@ -52,107 +45,74 @@ public class RestoreBackupRunnable implements Runnable {
@Override @Override
public void run() { public void run() {
Globals.INSTANCE.globalShutdownBackupFlag.set(false); Statics.globalShutdownBackupFlag.set(false);
log.info("关闭服务器..."); log.info("Shutting down server...");
ctx.server().stop(false); ctx.getServer().stop(false);
awaitServerShutdown();
Path worldFile = Utilities.getWorldFolder(ctx.server()),
tmp;
try {
tmp = Files.createTempDirectory(
ctx.server().getRunDirectory().toPath(),
ctx.restoreableFile().getFile().getFileName().toString()
);
} catch (IOException e) {
log.error("在解压备份时发生了异常.", e);
return;
}
//By making a separate thread we can start unpacking an old backup instantly
//Let the server shut down gracefully, and wait for the old world backup to complete
FutureTask<Void> waitForShutdown = new FutureTask<>(() -> {
ctx.server().getThread().join(); //wait for server thread to die and save all its state
if(config.get().backupOldWorlds) { if(config.get().backupOldWorlds) {
return ExecutableBackup.Builder BackupHelper.create(
BackupContext.Builder
.newBackupContextBuilder() .newBackupContextBuilder()
.setServer(ctx.server()) .setServer(ctx.getServer())
.setInitiator(ActionInitiator.Restore) .setInitiator(ActionInitiator.Restore)
.noCleanup() .setComment("Old_World" + (ctx.getComment() != null ? "_" + ctx.getComment() : ""))
.setComment("Old_World" + (ctx.comment() != null ? "_" + ctx.comment() : "")) .build()
.announce() ).run();
.build().call();
} }
return null;
});
//run the thread. File worldFile = Utilities.getWorldFolder(ctx.getServer());
new Thread(waitForShutdown, "Server shutdown wait thread").start();
try { log.info("Deleting old world...");
log.info("开始解压...");
long hash; if(!deleteDirectory(worldFile))
log.error("Something went wrong while deleting old world!");
if (ctx.restoreableFile().getArchiveFormat() == ConfigPOJO.ArchiveFormat.ZIP) worldFile.mkdirs();
hash = ZipDecompressor.decompress(ctx.restoreableFile().getFile(), tmp);
log.info("Starting decompression...");
if(ctx.getFile().getArchiveFormat() == ConfigPOJO.ArchiveFormat.ZIP)
ZipDecompressor.decompress(ctx.getFile().getFile(), worldFile);
else else
hash = GenericTarDecompressor.decompress(ctx.restoreableFile().getFile(), tmp); GenericTarDecompressor.decompress(ctx.getFile().getFile(), worldFile);
log.info("等待服务器完全终止..."); if(config.get().deleteOldBackupAfterRestore) {
log.info("Deleting old backup");
//locks until the backup is finished and the server is dead if(!ctx.getFile().getFile().delete()) log.info("Something went wrong while deleting old backup");
waitForShutdown.get();
Optional<String> errorMsg;
if(Files.notExists(CompressionStatus.resolveStatusFilename(tmp))) {
errorMsg = Optional.of("未找到状态文件!");
} else {
CompressionStatus status = CompressionStatus.readFromFile(tmp);
log.info("状态: {}", status);
Files.delete(tmp.resolve(CompressionStatus.DATA_FILENAME));
errorMsg = status.validate(hash, ctx);
}
if(errorMsg.isEmpty() || !config.get().integrityVerificationMode.verify()) {
if (errorMsg.isEmpty()) log.info("备份验证有效, 正在恢复.");
else log.info("备份已损坏,但验证已禁用[{}]。正在恢复. ", errorMsg.get());
//Disables write lock to override world file
((MinecraftServerSessionAccessor) ctx.server()).getSession().close();
Utilities.deleteDirectory(worldFile);
Files.move(tmp, worldFile);
if (config.get().deleteOldBackupAfterRestore) {
log.info("正在删除恢复的备份文件.");
Files.delete(ctx.restoreableFile().getFile());
}
} else {
log.error(errorMsg.get());
}
} catch (Exception e) {
log.error("在尝试恢复备份时发生了异常!", e);
} finally {
//Regardless of what happened, we should still clean up
if(Files.exists(tmp)) {
try {
Utilities.deleteDirectory(tmp);
} catch (IOException ignored) {}
}
} }
//in case we're playing on client //in case we're playing on client
Globals.INSTANCE.globalShutdownBackupFlag.set(true); Statics.globalShutdownBackupFlag.set(true);
log.info("完成!"); log.info("Done!");
//Might solve #37
//Idk if it's a good idea...
//Runtime.getRuntime().exit(0);
}
private void awaitServerShutdown() {
while(((LivingServer)ctx.getServer()).isAlive()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
log.error("Exception occurred!", e);
}
}
}
private static boolean deleteDirectory(File f) {
boolean state = true;
if(f.isDirectory()) {
for(File f2 : f.listFiles())
state &= deleteDirectory(f2);
}
return f.delete() && state;
} }
} }

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2020 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -21,18 +21,45 @@ package net.szum123321.textile_backup.core.restore;
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 net.szum123321.textile_backup.core.RestoreableFile;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.UUID;
public record RestoreContext(RestoreableFile restoreableFile, public record RestoreContext(RestoreHelper.RestoreableFile file,
MinecraftServer server, MinecraftServer server,
@Nullable String comment, @Nullable String comment,
ActionInitiator initiator, ActionInitiator initiator,
ServerCommandSource commandSource) { ServerCommandSource commandSource) {
public RestoreHelper.RestoreableFile getFile() {
return file;
}
public MinecraftServer getServer() {
return server;
}
@Nullable
public String getComment() {
return comment;
}
public ActionInitiator getInitiator() {
return initiator;
}
public UUID getInitiatorUUID() {
return initiator.equals(ActionInitiator.Player) && commandSource.getEntity() != null ? commandSource.getEntity().getUuid(): Util.NIL_UUID;
}
public ServerCommandSource getCommandSource() {
return commandSource;
}
public static final class Builder { public static final class Builder {
private RestoreableFile file; private RestoreHelper.RestoreableFile file;
private MinecraftServer server; private MinecraftServer server;
private String comment; private String comment;
private ServerCommandSource serverCommandSource; private ServerCommandSource serverCommandSource;
@ -44,7 +71,7 @@ public record RestoreContext(RestoreableFile restoreableFile,
return new Builder(); return new Builder();
} }
public Builder setFile(RestoreableFile file) { public Builder setFile(RestoreHelper.RestoreableFile file) {
this.file = file; this.file = file;
return this; return this;
} }

View File

@ -1,33 +1,34 @@
/* /*
* A simple backup mod for Fabric A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 Copyright (C) 2020 Szum123321
*
* This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package net.szum123321.textile_backup.core.restore; package net.szum123321.textile_backup.core.restore;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.szum123321.textile_backup.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.nio.file.Path; import java.io.File;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -37,38 +38,29 @@ public class RestoreHelper {
private final static ConfigHelper config = ConfigHelper.INSTANCE; private final static ConfigHelper config = ConfigHelper.INSTANCE;
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)); File root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
Optional<RestoreableFile> optionalFile = Optional<RestoreableFile> optionalFile = Arrays.stream(root.listFiles())
RestoreableFile.applyOnFiles(root, Optional.empty(), .map(RestoreableFile::newInstance)
e -> log.error("在尝试锁定文件时发生了异常!", e), .flatMap(Optional::stream)
s -> s.filter(rf -> rf.getCreationTime().equals(backupTime)) .filter(rf -> rf.getCreationTime().equals(backupTime))
.findFirst()); .findFirst();
optionalFile.ifPresent(r -> Globals.INSTANCE.setLockedFile(r.getFile())); Statics.untouchableFile = optionalFile.map(RestoreableFile::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.getInitiator() == ActionInitiator.Player)
log.info("备份恢复由以下玩家发起:{}", ctx.commandSource().getName()); log.info("Backup restoration was initiated by: {}", ctx.getCommandSource().getName());
else else
log.info("备份恢复由服务器控制台发起"); log.info("Backup restoration was initiated form Server Console");
Utilities.notifyPlayers( Utilities.notifyPlayers(
ctx.server(), ctx.server(),
"警告!服务器将在" + config.get().restoreDelay + "秒后关闭!" ctx.getInitiatorUUID(),
"Warning! The server is going to shut down in " + config.get().restoreDelay + " seconds!"
); );
return new AwaitThread( return new AwaitThread(
@ -77,11 +69,68 @@ public class RestoreHelper {
); );
} }
public static LinkedList<RestoreableFile> getAvailableBackups(MinecraftServer server) { public static List<RestoreableFile> getAvailableBackups(MinecraftServer server) {
Path root = Utilities.getBackupRootPath(Utilities.getLevelName(server)); File root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
return RestoreableFile.applyOnFiles(root, new LinkedList<>(), return Arrays.stream(root.listFiles())
e -> log.error("列出可用备份时发生错误.", e), .filter(Utilities::isValidBackup)
s -> s.sorted().collect(Collectors.toCollection(LinkedList::new))); .map(RestoreableFile::newInstance)
.flatMap(Optional::stream)
.collect(Collectors.toList());
}
public static class RestoreableFile implements Comparable<RestoreableFile> {
private final File file;
private final ConfigPOJO.ArchiveFormat archiveFormat;
private final LocalDateTime creationTime;
private final String comment;
private RestoreableFile(File 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.getName();
if(filename.split("#").length > 1) {
this.comment = filename.split("#")[1].split(extension)[0];
} else {
this.comment = null;
}
}
public static Optional<RestoreableFile> newInstance(File file) {
try {
return Optional.of(new RestoreableFile(file));
} catch (NoSuchElementException ignored) {}
return Optional.empty();
}
public File 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

@ -1,28 +1,26 @@
/* /*
* A simple backup mod for Fabric A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 Copyright (C) 2020 Szum123321
*
* This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package net.szum123321.textile_backup.core.restore.decompressors; package net.szum123321.textile_backup.core.restore.decompressors;
import net.szum123321.textile_backup.TextileBackup; import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger; import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.digest.FileTreeHashBuilder;
import net.szum123321.textile_backup.core.Utilities; import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.digest.HashingOutputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.CompressorException; import org.apache.commons.compress.compressors.CompressorException;
@ -31,50 +29,51 @@ import org.apache.commons.compress.utils.IOUtils;
import java.io.*; import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
public class GenericTarDecompressor { public class GenericTarDecompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME); private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static long decompress(Path input, Path target) throws IOException { public static void decompress(File input, File target) {
Instant start = Instant.now(); Instant start = Instant.now();
FileTreeHashBuilder treeBuilder = new FileTreeHashBuilder(0);
try (InputStream fileInputStream = Files.newInputStream(input); try (InputStream fileInputStream = new FileInputStream(input);
InputStream bufferedInputStream = new BufferedInputStream(fileInputStream); InputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
InputStream compressorInputStream = getCompressorInputStream(bufferedInputStream); InputStream compressorInputStream = getCompressorInputStream(bufferedInputStream);
TarArchiveInputStream archiveInputStream = new TarArchiveInputStream(compressorInputStream)) { TarArchiveInputStream archiveInputStream = new TarArchiveInputStream(compressorInputStream)) {
TarArchiveEntry entry; TarArchiveEntry entry;
while ((entry = archiveInputStream.getNextTarEntry()) != null) { while ((entry = archiveInputStream.getNextTarEntry()) != null) {
if(!archiveInputStream.canReadEntryData(entry)) if(!archiveInputStream.canReadEntryData(entry)) {
throw new IOException("Couldn't read archive entry! " + entry.getName()); log.error("Something when wrong while trying to decompress {}", entry.getName());
continue;
}
Path file = target.resolve(entry.getName()); File file = target.toPath().resolve(entry.getName()).toFile();
if(entry.isDirectory()) { if(entry.isDirectory()) {
Files.createDirectories(file); file.mkdirs();
} else { } else {
Files.createDirectories(file.getParent()); File parent = file.getParentFile();
try (OutputStream outputStream = Files.newOutputStream(file);
HashingOutputStream out = new HashingOutputStream(outputStream, file, treeBuilder)) { if (!parent.isDirectory() && !parent.mkdirs()) {
IOUtils.copy(archiveInputStream, out); log.error("Failed to create {}", parent);
} else {
try (OutputStream outputStream = Files.newOutputStream(file.toPath());
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
IOUtils.copy(archiveInputStream, bufferedOutputStream);
} catch (IOException e) {
log.error("An exception occurred while trying to decompress file: {}", file.getName(), e);
} }
} }
} }
} catch (CompressorException e) { }
throw new IOException(e); } catch (IOException | CompressorException e) {
log.error("An exception occurred! ", e);
} }
log.info("解压缩耗时{}秒. ", Utilities.formatDuration(Duration.between(start, Instant.now()))); log.info("Decompression took {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
try {
return treeBuilder.getValue(false);
} catch (InterruptedException ignored) {
return 0;
}
} }
private static InputStream getCompressorInputStream(InputStream inputStream) throws CompressorException { private static InputStream getCompressorInputStream(InputStream inputStream) throws CompressorException {

View File

@ -1,72 +1,65 @@
/* /*
* A simple backup mod for Fabric A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 Copyright (C) 2020 Szum123321
*
* This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package net.szum123321.textile_backup.core.restore.decompressors; package net.szum123321.textile_backup.core.restore.decompressors;
import net.szum123321.textile_backup.TextileBackup; import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger; import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.digest.FileTreeHashBuilder;
import net.szum123321.textile_backup.core.Utilities; import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.digest.HashingOutputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.compress.utils.IOUtils;
import java.io.*; import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Iterator;
public class ZipDecompressor { public class ZipDecompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME); private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static long decompress(Path inputFile, Path target) throws IOException { public static void decompress(File inputFile, File target) {
Instant start = Instant.now(); Instant start = Instant.now();
FileTreeHashBuilder hashBuilder = new FileTreeHashBuilder(0); try(ZipFile zipFile = new ZipFile(inputFile)) {
zipFile.getEntries().asIterator().forEachRemaining(entry -> {
try(ZipFile zipFile = new ZipFile(inputFile.toFile())) { File file = target.toPath().resolve(entry.getName()).toFile();
for (Iterator<ZipArchiveEntry> it = zipFile.getEntries().asIterator(); it.hasNext(); ) {
ZipArchiveEntry entry = it.next();
Path file = target.resolve(entry.getName());
if(entry.isDirectory()) { if(entry.isDirectory()) {
Files.createDirectories(file); file.mkdirs();
} else { } else {
Files.createDirectories(file.getParent()); File parent = file.getParentFile();
try (OutputStream outputStream = Files.newOutputStream(file);
HashingOutputStream out = new HashingOutputStream(outputStream, file, hashBuilder);
InputStream in = zipFile.getInputStream(entry)) {
IOUtils.copy(in, out); if (!parent.isDirectory() && !parent.mkdirs()) {
log.error("Failed to create {}", parent);
} else {
try (OutputStream outputStream = Files.newOutputStream(file.toPath());
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
IOUtils.copy(zipFile.getInputStream(entry), bufferedOutputStream);
} catch (IOException e) {
log.error("An exception occurred while trying to decompress file: {}", entry.getName(), e);
} }
} }
} }
});
} catch (IOException e) {
log.error("An exception occurred! ", e);
} }
log.info("解压缩耗时{}秒。", Utilities.formatDuration(Duration.between(start, Instant.now()))); log.info("Decompression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
try {
return hashBuilder.getValue(false);
} catch (InterruptedException ignored) {
return 0;
}
} }
} }

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2021 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -20,20 +20,16 @@ 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.Globals; import net.szum123321.textile_backup.Statics;
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 Globals.INSTANCE.disableWatchdog ? Util.getMeasuringTimeMs() : original; return Statics.disableWatchdog ? Util.getMeasuringTimeMs() : original;
} }
} }

View File

@ -0,0 +1,44 @@
/*
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.mixin;
import net.minecraft.server.MinecraftServer;
import net.szum123321.textile_backup.core.LivingServer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(MinecraftServer.class)
public class MinecraftServerMixin implements LivingServer {
@Unique
private boolean isAlive = true;
@Inject(method = "shutdown", at = @At("TAIL"))
public void onFinalWorldSave(CallbackInfo ci) {
isAlive = false;
}
@Unique
@Override
public boolean isAlive() {
return isAlive;
}
}

View File

@ -1,6 +1,6 @@
/* /*
* A simple backup mod for Fabric * A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321 * Copyright (C) 2021 Szum123321
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by

View File

@ -1,63 +0,0 @@
/*
* 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.test;
import net.minecraft.util.math.random.Random;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.digest.BalticHash;
public class BalticHashTest {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
final static int TEST_LEN = 21377; //simple prime
public static void run() throws RuntimeException {
log.info("Running hash test");
Random r = Random.create(2137);
long x = 0;
byte[] data = new byte[TEST_LEN];
for(int i = 0; i < TEST_LEN; i++) data[i] = (byte)r.nextInt();
//Test block mode
for(int i = 0; i < 5*2; i++) x ^= randomHash(data, r);
if(x != 0) throw new RuntimeException("Hash mismatch!");
log.info("Test passed");
}
static long randomHash(byte[] data, Random r) {
int n = data.length;
BalticHash h = new BalticHash();
int m = r.nextBetween(1, n);
int nn = n, p = 0;
for(int i = 0; i < m; i++) {
int k = r.nextBetween(1, nn - (m - i - 1));
h.update(data, p, k);
p += k;
nn -= k;
}
return h.getValue();
}
}

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 implements AutoCloseable public class BZip2OutputStream extends OutputStream
{ {
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,6 +263,17 @@ public class BZip2OutputStream extends OutputStream implements AutoCloseable
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.backupDirectoryPath": "Path to backup folder", "text.autoconfig.textile_backup.option.path": "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 KiBytes", "text.autoconfig.textile_backup.option.maxSize.@Tooltip": "In KBytes",
"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",
@ -47,9 +47,6 @@
"text.autoconfig.textile_backup.option.format": "Archive and compression format", "text.autoconfig.textile_backup.option.format": "Archive and compression format",
"text.autoconfig.textile_backup.option.format.@Tooltip": "See: https://github.com/Szum123321/textile_backup/wiki/Configuration#format", "text.autoconfig.textile_backup.option.format.@Tooltip": "See: https://github.com/Szum123321/textile_backup/wiki/Configuration#format",
"text.autoconfig.textile_backup.option.integrityVerificationMode": "Verify backup integrity",
"text.autoconfig.textile_backup.option.integrityVerificationMode.@Tooltip": "DO NOT ALTER unless fully aware of consequences",
"text.autoconfig.textile_backup.option.permissionLevel": "Min permission level", "text.autoconfig.textile_backup.option.permissionLevel": "Min permission level",
"text.autoconfig.textile_backup.option.alwaysSingleplayerAllowed": "Always allow on single-player", "text.autoconfig.textile_backup.option.alwaysSingleplayerAllowed": "Always allow on single-player",

View File

@ -1,63 +0,0 @@
{
"text.autoconfig.textile_backup.title": "Textile备份配置",
"text.autoconfig.textile_backup.category.default": "常规",
"text.autoconfig.textile_backup.category.Create": "备份设置",
"text.autoconfig.textile_backup.category.Restore": "还原",
"text.autoconfig.textile_backup.category.Manage": "管理",
"text.autoconfig.textile_backup.option.backupInterval": "备份间隔",
"text.autoconfig.textile_backup.option.backupInterval.@Tooltip": "每次自动备份之间的时间间隔(以秒为单位)",
"text.autoconfig.textile_backup.option.restoreDelay": "还原延迟",
"text.autoconfig.textile_backup.option.restoreDelay.@Tooltip": "以秒为单位",
"text.autoconfig.textile_backup.option.broadcastBackupStart": "通告备份开始",
"text.autoconfig.textile_backup.option.broadcastBackupDone": "通告备份完成",
"text.autoconfig.textile_backup.option.doBackupsOnEmptyServer": "在服务器为空时进行自动备份",
"text.autoconfig.textile_backup.option.shutdownBackup": "关机时进行备份",
"text.autoconfig.textile_backup.option.backupOldWorlds": "备份旧世界",
"text.autoconfig.textile_backup.option.perWorldBackup": "使用不同的文件夹来存储不同的世界备份",
"text.autoconfig.textile_backup.option.backupDirectoryPath": "备份文件夹路径",
"text.autoconfig.textile_backup.option.fileBlacklist": "黑名单文件",
"text.autoconfig.textile_backup.option.deleteOldBackupAfterRestore": "还原后删除备份",
"text.autoconfig.textile_backup.option.backupsToKeep": "要保留的备份数量",
"text.autoconfig.textile_backup.option.maxAge": "备份的最大存储期",
"text.autoconfig.textile_backup.option.maxAge.@Tooltip": "以创建时间为准,以秒为单位",
"text.autoconfig.textile_backup.option.maxSize": "备份文件夹的最大大小",
"text.autoconfig.textile_backup.option.maxSize.@Tooltip": "以KBytes为单位",
"text.autoconfig.textile_backup.option.compression": "压缩级别",
"text.autoconfig.textile_backup.option.compression.@Tooltip": "仅适用于zip格式",
"text.autoconfig.textile_backup.option.compressionCoreCountLimit": "用于压缩的最大核心数",
"text.autoconfig.textile_backup.option.compressionCoreCountLimit.@Tooltip": "设置为0可使用所有可用核心",
"text.autoconfig.textile_backup.option.format": "存档和压缩格式",
"text.autoconfig.textile_backup.option.format.@Tooltip": "参见https://github.com/Szum123321/textile_backup/wiki/Configuration#format",
"text.autoconfig.textile_backup.option.integrityVerificationMode": "验证备份完整性",
"text.autoconfig.textile_backup.option.integrityVerificationMode.@Tooltip": "除非完全了解后果,否则不要修改",
"text.autoconfig.textile_backup.option.permissionLevel": "最低权限等级",
"text.autoconfig.textile_backup.option.alwaysSingleplayerAllowed": "始终允许单人游戏",
"text.autoconfig.textile_backup.option.playerWhitelist": "管理员白名单",
"text.autoconfig.textile_backup.option.playerBlacklist": "管理员黑名单",
"text.autoconfig.textile_backup.option.dateTimeFormat": "日期和时间格式",
"text.autoconfig.textile_backup.option.dateTimeFormat.@Tooltip": "参见https://github.com/Szum123321/textile_backup/wiki/Date-time-format"
}

View File

@ -12,8 +12,7 @@
"1a2s3d4f1", "1a2s3d4f1",
"pm709", "pm709",
"Harveykang", "Harveykang",
"66Leo66", "66Leo66"
"IzzyBizzy45"
], ],
"contact": { "contact": {
"homepage": "https://www.curseforge.com/minecraft/mc-mods/textile-backup", "homepage": "https://www.curseforge.com/minecraft/mc-mods/textile-backup",
@ -38,13 +37,14 @@
], ],
"depends": { "depends": {
"fabricloader": ">=0.14.0", "fabricloader": ">=0.11",
"fabric": "*", "fabric": "*",
"minecraft": "^1.20-", "minecraft": "1.18.*",
"cloth-config2": "*", "cloth-config2": "*",
"java": ">=16" "java": ">=16"
}, },
"recommends": { "recommends": {
"modmenu": "*" "modmenu": "*"
}, },

View File

@ -4,6 +4,7 @@
"compatibilityLevel": "JAVA_16", "compatibilityLevel": "JAVA_16",
"mixins": [ "mixins": [
"DedicatedServerWatchdogMixin", "DedicatedServerWatchdogMixin",
"MinecraftServerMixin",
"MinecraftServerSessionAccessor" "MinecraftServerSessionAccessor"
], ],
"client": [ "client": [