diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 0c6aa437..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: LatvianModder diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ab381a6..a3fef849 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,44 +1,24 @@ -name: Java CI +name: Java CI - Build on Push on: push: - branches: [ main, dev ] + branches: [ main, dev, "1.**" ] workflow_dispatch: inputs: - norelease: - description: 'Do not publish' + skip_maven_publish: + description: 'Skip Maven publishing' required: true default: 'false' jobs: build: - runs-on: ubuntu-latest if: | !contains(github.event.head_commit.message, '[ciskip]') - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 30 # Gets the last 30 commits so the changelog might work - - name: Set up JDK 8 - uses: actions/setup-java@v2 - with: - distribution: 'adopt' - java-version: '8' - cache: 'gradle' - - name: Build with Gradle - env: - FTB_MAVEN_TOKEN: ${{ secrets.FTB_MAVEN_TOKEN}} - SAPS_TOKEN: ${{ secrets.SAPS_TOKEN }} - run: | - chmod +x ./gradlew - ./gradlew build --stacktrace --no-daemon - - name: Release to CurseForge - if: | - github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[norelease]') && github.event.inputs.norelease != 'true' - env: - GIT_COMMIT: ${{ github.event.after }} - GIT_PREVIOUS_COMMIT: ${{ github.event.before }} - CURSEFORGE_KEY: ${{ secrets.CURSEFORGE_KEY }} - run: | - chmod +x ./gradlew - ./gradlew build curseforge --stacktrace --no-daemon + uses: FTBTeam/mods-meta/.github/workflows/standard-release.yml@main + with: + curse-publish-task: "" + maven-snapshots: true + java-version: 21 + secrets: + ftb-maven-token: ${{ secrets.FTB_MAVEN_TOKEN }} + saps-token: ${{ secrets.SAPS_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..51eb6d75 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,20 @@ +name: Java CI - Build Release + +on: + push: + tags: + - 'v[0-9]+\.[0-9]+\.[0-9]+' + - 'v[0-9]+\.[0-9]+\.[0-9]+-[a-z]+' + - 'v[0-9]+\.[0-9]+\.[0-9]+-[a-z]+\.[0-9]+' + +jobs: + build: + if: | + !contains(github.event.head_commit.message, '[ciskip]') + uses: FTBTeam/mods-meta/.github/workflows/standard-release.yml@main + with: + curse-publish-task: publishMods + java-version: 21 + secrets: + ftb-maven-token: ${{ secrets.FTB_MAVEN_TOKEN }} + curse-token: ${{ secrets.CURSEFORGE_KEY }} diff --git a/.gitignore b/.gitignore index ca672db3..d1ab1f60 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ logs # other eclipse run -.vscode \ No newline at end of file +.vscode +runs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..eaf0dd44 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [21.0.1] + +### Added +* Added "Notify Admins Only" server config setting + * When true, only players with permission level >= 2, and SSP server owners, will be notified about in-progress backups + +### Fixed +* Fixed issue where players on SMP could get a stuck backup progress notification on the client +* Fixed backup interval being ignored on server startup and always treated as 120 minutes, even if changed in config +* Better command output for `/ftbbackups3 time` is a backup is overdue (due to no players being online) + +## [21.0.0] + +### Added +* Initial release of FTB Backups 3, a major rewrite and update for Minecraft 1.21.1 of the original FTB Backups mod + * Note that this mod is not related to FTB Backups 2 in any way diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..69ae4a50 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,5 @@ +All Rights Reserved + +Copyright (c) 2025 Feed The Beast Ltd + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 4ef6396e..a35b8bba 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ -# FTB Backups [![](http://cf.way2muchnoise.eu/ftb-backups-forge.svg) ![](https://cf.way2muchnoise.eu/packs/ftb-backups-forge.svg) ![](http://cf.way2muchnoise.eu/versions/ftb-backups-forge.svg)](https://www.curseforge.com/minecraft/mc-mods/ftb-backups-forge) +# FTB Backups [![](http://cf.way2muchnoise.eu/314904.svg) ![](http://cf.way2muchnoise.eu/versions/314904.svg)](https://www.curseforge.com/minecraft/mc-mods/ftb-backups-forge) -Use https://github.com/FTBTeam/FTB-Mods-Issues for any mod issues \ No newline at end of file +Use https://github.com/FTBTeam/FTB-Mods-Issues for any mod issues + +## Support + +- For **Modpack** issues, please go here: https://go.ftb.team/support-modpack +- For **Mod** issues, please go here: https://go.ftb.team/support-mod-issues +- Just got a question? Check out our Discord: https://go.ftb.team/discord + +## Licence + +All Rights Reserved to Feed The Beast Ltd. Source code is `visible source`, please see our [LICENSE.md](/LICENSE.md) for more information. Any Pull Requests made to this mod must have the CLA (Contributor Licence Agreement) signed and agreed to before the request will be considered. + +## Keep up to date + +[![](https://cdn.feed-the-beast.com/assets/socials/icons/social-discord.webp)](https://go.ftb.team/discord) [![](https://cdn.feed-the-beast.com/assets/socials/icons/social-github.webp)](https://go.ftb.team/github) [![](https://cdn.feed-the-beast.com/assets/socials/icons/social-twitter-x.webp)](https://go.ftb.team/twitter) [![](https://cdn.feed-the-beast.com/assets/socials/icons/social-youtube.webp)](https://go.ftb.team/youtube) [![](https://cdn.feed-the-beast.com/assets/socials/icons/social-twitch.webp)](https://go.ftb.team/twitch) [![](https://cdn.feed-the-beast.com/assets/socials/icons/social-instagram.webp)](https://go.ftb.team/instagram) [![](https://cdn.feed-the-beast.com/assets/socials/icons/social-facebook.webp)](https://go.ftb.team/facebook) [![](https://cdn.feed-the-beast.com/assets/socials/icons/social-tiktok.webp)](https://go.ftb.team/tiktok) diff --git a/build.gradle b/build.gradle index 43792afd..03e1c562 100644 --- a/build.gradle +++ b/build.gradle @@ -1,113 +1,191 @@ -buildscript { - repositories { - jcenter() - maven { url = 'https://files.minecraftforge.net/maven' } - maven { url "https://plugins.gradle.org/m2/" } - mavenCentral() - } - dependencies { - classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '3.+', changing: true - classpath 'gradle.plugin.com.matthewprenger:CurseGradle:1.1.0' - } +plugins { + id 'java-library' + id 'idea' + id 'maven-publish' + id 'net.neoforged.moddev' version '2.0.78' + id "me.modmuss50.mod-publish-plugin" version "0.8.3" +} + +tasks.named('wrapper', Wrapper).configure { + distributionType = Wrapper.DistributionType.BIN +} + +def mcMinorAndPatch = getMinorAndPatchFromMc() + +version = "${mcMinorAndPatch}.${mod_version}" +group = project.maven_group + +base { + archivesName = project.archives_base_name } -apply plugin: 'net.minecraftforge.gradle' -apply plugin: 'eclipse' -apply plugin: 'com.matthewprenger.cursegradle' -apply plugin: 'maven' -apply from: 'https://files.latmod.com/public/markdown-git-changelog.gradle' +apply from: 'https://raw.githubusercontent.com/FTBTeam/mods-meta/main/gradle/changelog.gradle' +apply from: "https://raw.githubusercontent.com/FTBTeam/mods-meta/main/gradle/publishing.gradle" + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) -def ENV = System.getenv() -version = ENV.GITHUB_RUN_NUMBER ? "${mod_version}.${ENV.GITHUB_RUN_NUMBER}" : "${mod_version}.9999" -group = project.mod_package -archivesBaseName = project.mod_name +neoForge { + version = "${mcMinorAndPatch}.${neo_version}" -sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.8' + parchment { + mappingsVersion = project.parchment_mappings_version + minecraftVersion = project.parchment_minecraft_version.replace("\${mc_version}", minecraft_version) + } -minecraft { - mappings channel: 'snapshot', version: project.mapping_version - // accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg') runs { + configureEach { + systemProperty 'forge.logging.markers', 'REGISTRIES' + logLevel = org.slf4j.event.Level.DEBUG + } + client { - workingDirectory project.file('run') - //property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP' - property 'forge.logging.console.level', 'debug' - - if (project.hasProperty('mc_dev_uuid')) args '--uuid', project.mc_dev_uuid - if (project.hasProperty('mc_dev_username')) args '--username', project.mc_dev_username - if (project.hasProperty('mc_dev_width')) args '--width', project.mc_dev_width - if (project.hasProperty('mc_dev_height')) args '--height', project.mc_dev_height - - mods { - ftbbackups { - source sourceSets.main - } - } + client() + + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id } server { - workingDirectory project.file('run') - //property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP' - property 'forge.logging.console.level', 'debug' - - args 'nogui' + server() + programArgument '--nogui' + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } - mods { - ftbbackups { - source sourceSets.main - } - } + gameTestServer { + type = "gameTestServer" + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id } data { - workingDirectory project.file('run') - //property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP' - property 'forge.logging.console.level', 'debug' - - args '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/') + data() + programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } + } - mods { - ftbbackups { - source sourceSets.main - } - } + mods { + "${mod_id}" { + sourceSet(sourceSets.main) } } } +sourceSets.main.resources { srcDir 'src/generated/resources' } + +// Sets up a dependency configuration called 'localRuntime'. +// This configuration should be used instead of 'runtimeOnly' to declare +// a dependency that will be present for runtime testing but that is +// "optional", meaning it will not be pulled by dependents of this mod. +configurations { + runtimeClasspath.extendsFrom localRuntime +} + +repositories { + mavenLocal() + maven { url "https://maven.ftb.dev/releases" } // FTB Mods + maven { url "https://maven.ftb.dev/snapshots" } // FTB Mods + maven { url "https://maven.architectury.dev" } +} + dependencies { - minecraft "net.minecraftforge:forge:${forge_version}" -} - -jar { - manifest { - attributes([ - "Specification-Title" : project.mod_id, - "Specification-Vendor" : project.mod_author, - "Specification-Version" : "1", - "Implementation-Title" : project.name, - "Implementation-Version" : version, - "Implementation-Vendor" : project.mod_author, - "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ") - ]) + implementation "dev.ftb.mods:ftb-library-neoforge:${ftb_library_version}" +} + +tasks.register("sourcesJar", Jar) { + archiveClassifier.set("sources") + from sourceSets.main.allJava +} + +tasks.register("apiJar", Jar) { + archiveClassifier.set("api") + + from sourceSets.main.output + + // Only include the dev.ftb.mods.ftbbackups.api package + include 'dev/ftb/mods/ftbbackups/api/**' +} + +tasks.named("jar") { + dependsOn("sourcesJar") + dependsOn("apiJar") +} + +tasks.withType(ProcessResources).configureEach { + var replaceProperties = [ + version: version, + loader_version_range: loader_version_range, + // 99.9% of the time we're only going to target a very vague version of neoforge so this is fine. + mod_loader_range: mod_loader_range.replace("\${mc_base}", mcMinorAndPatch), // Replace the version in the mod loader range + ] + + inputs.properties replaceProperties + + filesMatching(['META-INF/neoforge.mods.toml']) { + expand replaceProperties + } + + // Copy in the repos license file + from(project.rootDir) { + include 'LICENSE.md' } } -def reobfFile = file("$buildDir/reobfJar/output.jar") -def reobfArtifact = artifacts.add('default', reobfFile) { - type 'jar' - builtBy 'reobfJar' +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation } -if (ENV.CURSEFORGE_KEY) { +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} + +def getMinorAndPatchFromMc() { + String mcVersion = project.minecraft_version + String[] split = mcVersion.split("\\.") + return split[1] + "." + (split[2] ?: "0") +} + +publishMods { + def CFToken = providers.environmentVariable("CURSEFORGE_KEY"); + + dryRun = !CFToken.isPresent() + displayName = "[NEOFORGE] ${project.mod_name} ${project.version}" + modLoaders.add("neoforge") + + changelog = createChangelog(project) + file = jar.archiveFile + version = project.version // Pretty sure it defaults to this + + def tag = providers.environmentVariable("TAG").getOrElse("release") + type = tag.endsWith("-beta") ? BETA : (tag.endsWith("-alpha") ? ALPHA : STABLE) + curseforge { - apiKey = ENV.CURSEFORGE_KEY - project { - id = project.curseforge_id - releaseType = project.curseforge_type - addGameVersion '1.16.5' - changelog = getGitChangelog - changelogType = 'markdown' + accessToken = CFToken.orElse("missing") + projectId = project.curseforge_id + minecraftVersions.add(project.minecraft_version) + javaVersions.add(JavaVersion.VERSION_21) + } +} + +publishing { + publications { + register('mavenJava', MavenPublication) { + artifactId = archives_base_name + version ftbPublishing.mavenVersion + from components.java } } -} \ No newline at end of file + + repositories { + if (ftbPublishing.ftbToken) { + maven { + url ftbPublishing.ftbURL + credentials { + username = ftbPublishing.ftbUser + password = ftbPublishing.ftbToken + } + } + } + } +} diff --git a/gradle.properties b/gradle.properties index 0cee06b8..c1d196c0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,29 @@ +# Sets default memory used for gradle commands. Can be overridden by user or command line properties. +# This is required to provide enough memory for the Minecraft decompilation process. org.gradle.jvmargs=-Xmx3G -org.gradle.daemon=false -mod_id=ftbackups -mod_name=ftb-backups -mod_package=dev.ftb.mods -mod_version=2.1.2 -mod_author=LatvianModder -forge_version=1.16.5-36.2.2 -mapping_version=20200723-1.16.1 +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true + +parchment_minecraft_version=${mc_version} +parchment_mappings_version=2024.11.17 + +mod_id=ftbbackups3 +mod_name=FTB Backups 3 +mod_version=1 +minecraft_version=1.21.1 + +loader_version_range=[4,) +mod_loader_range=[${mc_base},) +neo_version=136 + +#Mod Stuff +archives_base_name=ftb-backups-3 +maven_group=dev.ftb.mods + +#CurseForge curseforge_id=314904 -curseforge_type=release \ No newline at end of file + +#Deps +ftb_library_version=2101.1.13-SNAPSHOT \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 30d399d8..7454180f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index af38095b..9355b415 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Mon Sep 14 12:28:28 PDT 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 index 91a7e269..1b6c7873 --- a/gradlew +++ b/gradlew @@ -1,79 +1,129 @@ -#!/usr/bin/env bash +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -82,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -90,75 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# 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" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + 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 - i=$((i+1)) + # 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 - 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 -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 86209688..ac1b06f9 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +1,89 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@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=-Xmx3g - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_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=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..90ae98fa --- /dev/null +++ b/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + maven { url = 'https://maven.neoforged.net/releases' } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0' +} diff --git a/src/main/java/com/feed_the_beast/mods/ftbbackups/Backup.java b/src/main/java/com/feed_the_beast/mods/ftbbackups/Backup.java deleted file mode 100644 index 90d09736..00000000 --- a/src/main/java/com/feed_the_beast/mods/ftbbackups/Backup.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.feed_the_beast.mods.ftbbackups; - -import com.google.gson.JsonObject; - -import java.io.File; - -/** - * @author LatvianModder - */ -public class Backup implements Comparable -{ - public final long time; - public final String fileId; - public final int index; - public final boolean success; - public final long size; - - public Backup(long t, String f, int i, boolean s, long fs) - { - time = t; - fileId = f; - index = i; - success = s; - size = fs; - } - - public Backup(JsonObject o) - { - this(o.get("time").getAsLong(), o.get("file").getAsString(), o.get("index").getAsInt(), o.get("success").getAsBoolean(), o.get("size").getAsLong()); - } - - public JsonObject toJsonObject() - { - JsonObject o = new JsonObject(); - o.addProperty("time", time); - o.addProperty("file", fileId); - o.addProperty("index", index); - o.addProperty("success", success); - o.addProperty("size", size); - return o; - } - - public int hashCode() - { - return Long.hashCode(time); - } - - public String toString() - { - return fileId; - } - - public boolean equals(Object o) - { - return o == this || (o instanceof Backup && ((Backup) o).time == time); - } - - public File getFile() - { - return new File(Backups.INSTANCE.backupsFolder, fileId); - } - - @Override - public int compareTo(Backup o) - { - return Long.compare(time, o.time); - } -} \ No newline at end of file diff --git a/src/main/java/com/feed_the_beast/mods/ftbbackups/BackupCommands.java b/src/main/java/com/feed_the_beast/mods/ftbbackups/BackupCommands.java deleted file mode 100644 index b3e67f1d..00000000 --- a/src/main/java/com/feed_the_beast/mods/ftbbackups/BackupCommands.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.feed_the_beast.mods.ftbbackups; - -import com.mojang.brigadier.CommandDispatcher; -import com.mojang.brigadier.arguments.StringArgumentType; -import net.minecraft.command.CommandSource; -import net.minecraft.command.Commands; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.util.Util; -import net.minecraft.util.text.TranslationTextComponent; -import net.minecraft.world.storage.FolderName; - -/** - * @author LatvianModder - */ -public class BackupCommands -{ - public static void register(CommandDispatcher dispatcher) - { - dispatcher.register(Commands.literal("ftbbackups") - .then(Commands.literal("time") - .executes(ctx -> time(ctx.getSource())) - ) - .then(Commands.literal("start") - .requires(cs -> cs.getServer().isSinglePlayer() || cs.hasPermissionLevel(3)) - .then(Commands.argument("name", StringArgumentType.word()) - .executes(ctx -> start(ctx.getSource(), StringArgumentType.getString(ctx, "name"))) - ) - .executes(ctx -> start(ctx.getSource(), "")) - ) - .then(Commands.literal("size") - .requires(cs -> cs.getServer().isSinglePlayer() || cs.hasPermissionLevel(3)) - .executes(ctx -> size(ctx.getSource())) - ) - ); - } - - private static int time(CommandSource source) - { - source.sendFeedback(new TranslationTextComponent("ftbbackups.lang.timer", BackupUtils.getTimeString(Backups.INSTANCE.nextBackup - System.currentTimeMillis())), true); - return 1; - } - - private static int start(CommandSource source, String customName) - { - if (Backups.INSTANCE.run(source.getServer(), false, source.getDisplayName(), customName)) - { - for (ServerPlayerEntity player : source.getServer().getPlayerList().getPlayers()) - { - player.sendMessage(new TranslationTextComponent("ftbbackups.lang.manual_launch", source.getDisplayName()), Util.DUMMY_UUID); - } - } - else - { - source.sendFeedback(new TranslationTextComponent("ftbbackups.lang.already_running"), true); - } - - return 1; - } - - private static int size(CommandSource source) - { - long totalSize = 0L; - - for (Backup backup : Backups.INSTANCE.backups) - { - totalSize += backup.size; - } - - source.sendFeedback(new TranslationTextComponent("ftbbackups.lang.size.current", BackupUtils.getSizeString(source.getServer().func_240776_a_(FolderName.field_237253_i_).toFile())), true); - source.sendFeedback(new TranslationTextComponent("ftbbackups.lang.size.total", BackupUtils.getSizeString(totalSize)), true); - source.sendFeedback(new TranslationTextComponent("ftbbackups.lang.size.available", BackupUtils.getSizeString(Math.min(FTBBackupsConfig.maxTotalSize, Backups.INSTANCE.backupsFolder.getFreeSpace()))), true); - - return 1; - } -} \ No newline at end of file diff --git a/src/main/java/com/feed_the_beast/mods/ftbbackups/BackupEvent.java b/src/main/java/com/feed_the_beast/mods/ftbbackups/BackupEvent.java deleted file mode 100644 index 969cd5ab..00000000 --- a/src/main/java/com/feed_the_beast/mods/ftbbackups/BackupEvent.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.feed_the_beast.mods.ftbbackups; - -import net.minecraftforge.eventbus.api.Event; - -import javax.annotation.Nullable; -import java.io.File; -import java.util.function.Consumer; - -/** - * @author LatvianModder - */ -public class BackupEvent extends Event -{ - public static class Pre extends BackupEvent - { - private final Consumer callback; - - public Pre(Consumer c) - { - callback = c; - } - - public void add(File file) - { - callback.accept(file); - } - } - - public static class Post extends BackupEvent - { - private final Backup backup; - private final Exception error; - - public Post(Backup b, @Nullable Exception ex) - { - backup = b; - error = ex; - } - - public Backup getBackup() - { - return backup; - } - - @Nullable - public Exception getError() - { - return error; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/feed_the_beast/mods/ftbbackups/BackupStatus.java b/src/main/java/com/feed_the_beast/mods/ftbbackups/BackupStatus.java deleted file mode 100644 index fef432bf..00000000 --- a/src/main/java/com/feed_the_beast/mods/ftbbackups/BackupStatus.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.feed_the_beast.mods.ftbbackups; - -/** - * @author LatvianModder - */ -public enum BackupStatus -{ - NONE, - RUNNING, - DONE; - - public boolean isRunning() - { - return this == RUNNING; - } - - public boolean isDone() - { - return this == DONE; - } - - public boolean isRunningOrDone() - { - return isRunning() || isDone(); - } -} \ No newline at end of file diff --git a/src/main/java/com/feed_the_beast/mods/ftbbackups/BackupUtils.java b/src/main/java/com/feed_the_beast/mods/ftbbackups/BackupUtils.java deleted file mode 100644 index 525afa5b..00000000 --- a/src/main/java/com/feed_the_beast/mods/ftbbackups/BackupUtils.java +++ /dev/null @@ -1,357 +0,0 @@ -package com.feed_the_beast.mods.ftbbackups; - -import com.google.gson.JsonElement; -import com.google.gson.JsonIOException; -import com.google.gson.JsonNull; -import com.google.gson.JsonSyntaxException; -import com.google.gson.internal.Streams; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; - -import javax.annotation.Nullable; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.nio.channels.FileChannel; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -/** - * @author LatvianModder - */ -public class BackupUtils -{ - public static final long KB = 1024L; - public static final long MB = KB * 1024L; - public static final long GB = MB * 1024L; - public static final long TB = GB * 1024L; - - public static final double KB_D = 1024D; - public static final double MB_D = KB_D * 1024D; - public static final double GB_D = MB_D * 1024D; - public static final double TB_D = GB_D * 1024D; - - public static String getTimeString(long millis) - { - boolean neg = false; - if (millis < 0L) - { - neg = true; - millis = -millis; - } - - StringBuilder sb = new StringBuilder(); - - if (millis < 1000L) - { - if (neg) - { - sb.append('-'); - } - - sb.append(millis); - sb.append('m'); - sb.append('s'); - return sb.toString(); - } - - long secs = millis / 1000L; - - if (neg) - { - sb.append('-'); - } - - long h = (secs / 3600L) % 24; - long m = (secs / 60L) % 60L; - long s = secs % 60L; - - if (secs >= 86400L) - { - sb.append(secs / 86400L); - sb.append('d'); - sb.append(' '); - } - - if (h > 0 || secs >= 86400L) - { - if (h < 10) - { - sb.append('0'); - } - sb.append(h); - //sb.append("h "); - sb.append(':'); - } - - if (m < 10) - { - sb.append('0'); - } - sb.append(m); - //sb.append("m "); - sb.append(':'); - if (s < 10) - { - sb.append('0'); - } - sb.append(s); - //sb.append('s'); - - return sb.toString(); - } - - public static long getSize(File file) - { - if (!file.exists()) - { - return 0L; - } - else if (file.isFile()) - { - return file.length(); - } - else if (file.isDirectory()) - { - long length = 0L; - File[] f1 = file.listFiles(); - if (f1 != null && f1.length > 0) - { - for (File aF1 : f1) - { - length += getSize(aF1); - } - } - return length; - } - return 0L; - } - - public static String getSizeString(double b) - { - if (b >= TB_D) - { - return String.format("%.1fTB", b / TB_D); - } - else if (b >= GB_D) - { - return String.format("%.1fGB", b / GB_D); - } - else if (b >= MB_D) - { - return String.format("%.1fMB", b / MB_D); - } - else if (b >= KB_D) - { - return String.format("%.1fKB", b / KB_D); - } - - return ((long) b) + "B"; - } - - public static String getSizeString(File file) - { - return getSizeString(getSize(file)); - } - - public static File newFile(File file) - { - if (!file.exists()) - { - try - { - File parent = file.getParentFile(); - if (!parent.exists()) - { - parent.mkdirs(); - } - file.createNewFile(); - } - catch (Exception e) - { - e.printStackTrace(); - } - } - - return file; - } - - public static List listTree(File file) - { - List l = new ArrayList<>(); - listTree0(l, file); - return l; - } - - public static void listTree0(List list, File file) - { - if (file.isDirectory()) - { - File[] fl = file.listFiles(); - - if (fl != null && fl.length > 0) - { - for (File aFl : fl) - { - listTree0(list, aFl); - } - } - } - else if (file.isFile()) - { - list.add(file); - } - } - - public static void copyFile(File src, File dst) throws Exception - { - if (src.exists() && !src.equals(dst)) - { - if (src.isDirectory() && dst.isDirectory()) - { - for (File f : listTree(src)) - { - File dst1 = new File(dst.getAbsolutePath() + File.separatorChar + (f.getAbsolutePath().replace(src.getAbsolutePath(), ""))); - copyFile(f, dst1); - } - } - else - { - dst = newFile(dst); - - try (FileInputStream fis = new FileInputStream(src); - FileOutputStream fos = new FileOutputStream(dst); - FileChannel srcC = fis.getChannel(); - FileChannel dstC = fos.getChannel()) - { - dstC.transferFrom(srcC, 0L, srcC.size()); - } - } - } - } - - public static boolean delete(File file) - { - if (!file.exists()) - { - return false; - } - else if (file.isFile()) - { - return file.delete(); - } - - String[] files = file.list(); - - if (files != null) - { - for (String s : files) - { - delete(new File(file, s)); - } - } - - return file.delete(); - } - - public static void toJson(Writer writer, @Nullable JsonElement element, boolean prettyPrinting) - { - if (element == null || element.isJsonNull()) - { - try - { - writer.write("null"); - } - catch (Exception ex) - { - ex.printStackTrace(); - } - - return; - } - - JsonWriter jsonWriter = new JsonWriter(writer); - jsonWriter.setLenient(true); - jsonWriter.setHtmlSafe(false); - jsonWriter.setSerializeNulls(true); - - if (prettyPrinting) - { - jsonWriter.setIndent("\t"); - } - - try - { - Streams.write(element, jsonWriter); - } - catch (Exception ex) - { - throw new JsonIOException(ex); - } - } - - public static void toJson(File file, @Nullable JsonElement element, boolean prettyPrinting) - { - try (OutputStreamWriter output = new OutputStreamWriter(new FileOutputStream(newFile(file)), StandardCharsets.UTF_8); - BufferedWriter writer = new BufferedWriter(output)) - { - toJson(writer, element, prettyPrinting); - } - catch (Exception ex) - { - ex.printStackTrace(); - } - } - - public static JsonElement readJson(File file) - { - if (!file.exists()) - { - return JsonNull.INSTANCE; - } - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) - { - JsonReader jsonReader = new JsonReader(reader); - jsonReader.setLenient(true); - JsonElement element = Streams.parse(jsonReader); - - if (!element.isJsonNull() && jsonReader.peek() != JsonToken.END_DOCUMENT) - { - throw new JsonSyntaxException("Did not consume the entire document."); - } - - return element; - } - catch (Exception ex) - { - return null; - } - } - - public static String removeAllWhitespace(String s) - { - char[] chars = new char[s.length()]; - int j = 0; - - for (int i = 0; i < chars.length; i++) - { - char c = s.charAt(i); - - if (c > ' ') - { - chars[j] = c; - j++; - } - } - - return new String(chars, 0, j); - } -} \ No newline at end of file diff --git a/src/main/java/com/feed_the_beast/mods/ftbbackups/Backups.java b/src/main/java/com/feed_the_beast/mods/ftbbackups/Backups.java deleted file mode 100644 index db37e1a9..00000000 --- a/src/main/java/com/feed_the_beast/mods/ftbbackups/Backups.java +++ /dev/null @@ -1,475 +0,0 @@ -package com.feed_the_beast.mods.ftbbackups; - -import com.feed_the_beast.mods.ftbbackups.net.BackupProgressPacket; -import com.feed_the_beast.mods.ftbbackups.net.FTBBackupsNetHandler; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import net.minecraft.server.MinecraftServer; -import net.minecraft.util.Util; -import net.minecraft.util.text.ChatType; -import net.minecraft.util.text.Color; -import net.minecraft.util.text.ITextComponent; -import net.minecraft.util.text.StringTextComponent; -import net.minecraft.util.text.TextFormatting; -import net.minecraft.util.text.TranslationTextComponent; -import net.minecraft.world.server.ServerWorld; -import net.minecraft.world.storage.FolderName; -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.fml.loading.FMLPaths; -import net.minecraftforge.fml.network.PacketDistributor; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -public enum Backups -{ - INSTANCE; - - public final List backups = new ArrayList<>(); - public File backupsFolder; - public long nextBackup = -1L; - public BackupStatus doingBackup = BackupStatus.NONE; - public boolean printFiles = false; - - public int currentFile = 0; - public int totalFiles = 0; - private String currentFileName = ""; - public boolean hadPlayersOnline = false; - - public void init(MinecraftServer server) - { - File dataDir = server.getDataDirectory(); - backupsFolder = FTBBackupsConfig.folder.trim().isEmpty() ? FMLPaths.GAMEDIR.get().resolve("backups").toFile() : new File(FTBBackupsConfig.folder); - - try - { - backupsFolder = backupsFolder.getCanonicalFile(); - } - catch (Exception ex) - { - } - - doingBackup = BackupStatus.NONE; - backups.clear(); - - File file = new File(dataDir, "local/ftbutilities/backups.json"); - - if (!file.exists()) - { - File oldFile = new File(backupsFolder, "backups.json"); - - if (oldFile.exists()) - { - oldFile.renameTo(file); - } - } - - JsonElement element = BackupUtils.readJson(file); - - if (element != null && element.isJsonArray()) - { - try - { - for (JsonElement e : element.getAsJsonArray()) - { - JsonObject json = e.getAsJsonObject(); - - if (!json.has("size")) - { - json.addProperty("size", BackupUtils.getSize(new File(backupsFolder, json.get("file").getAsString()))); - } - - backups.add(new Backup(json)); - } - } - catch (Throwable ex) - { - ex.printStackTrace(); - } - } - - FTBBackups.LOGGER.info("Backups folder - " + backupsFolder.getAbsolutePath()); - nextBackup = System.currentTimeMillis() + FTBBackupsConfig.backupTimer; - } - - public void tick(MinecraftServer server, long now) - { - if (nextBackup > 0L && nextBackup <= now) - { - if (!FTBBackupsConfig.onlyIfPlayersOnline || hadPlayersOnline || !server.getPlayerList().getPlayers().isEmpty()) - { - hadPlayersOnline = false; - run(server, true, new StringTextComponent("Server"), ""); - } - } - - if (doingBackup.isDone()) - { - doingBackup = BackupStatus.NONE; - - for (ServerWorld world : server.getWorlds()) - { - if (world != null) - { - world.disableLevelSaving = false; - } - } - - if (!FTBBackupsConfig.silent) - { - FTBBackupsNetHandler.MAIN.send(PacketDistributor.ALL.noArg(), new BackupProgressPacket(0, 0)); - } - } - else if (doingBackup.isRunning() && printFiles) - { - if (currentFile == 0 || currentFile == totalFiles - 1) - { - FTBBackups.LOGGER.info("[" + currentFile + " | " + (int) ((currentFile / (double) totalFiles) * 100D) + "%]: " + currentFileName); - } - - if (!FTBBackupsConfig.silent) - { - FTBBackupsNetHandler.MAIN.send(PacketDistributor.ALL.noArg(), new BackupProgressPacket(currentFile, totalFiles)); - } - } - } - - public void notifyAll(MinecraftServer server, ITextComponent component, boolean error) - { - component = component.deepCopy(); - component.getStyle().setColor(Color.func_240744_a_(error ? TextFormatting.DARK_RED : TextFormatting.LIGHT_PURPLE)); - FTBBackups.LOGGER.info(component.getString()); - server.getPlayerList().func_232641_a_(component, ChatType.GAME_INFO, Util.DUMMY_UUID); - } - - public boolean run(MinecraftServer server, boolean auto, ITextComponent name, String customName) - { - if (doingBackup.isRunningOrDone()) - { - return false; - } - - if (auto && !FTBBackupsConfig.auto) - { - return false; - } - - notifyAll(server, new TranslationTextComponent("ftbbackups.lang.start", name), false); - nextBackup = System.currentTimeMillis() + FTBBackupsConfig.backupTimer; - - for (ServerWorld world : server.getWorlds()) - { - if (world != null) - { - world.disableLevelSaving = true; - } - } - - doingBackup = BackupStatus.RUNNING; - server.getPlayerList().saveAllPlayerData(); - - new Thread(() -> { - try - { - try - { - createBackup(server, customName); - } - catch (Exception ex) - { - ex.printStackTrace(); - notifyAll(server, new TranslationTextComponent("ftbbackups.lang.saving_failed"), true); - } - } - catch (Exception ex) - { - notifyAll(server, new TranslationTextComponent("ftbbackups.lang.saving_failed"), true); - ex.printStackTrace(); - } - - doingBackup = BackupStatus.DONE; - }).start(); - - return true; - } - - private void createBackup(MinecraftServer server, String customName) - { - File src = server.func_240776_a_(FolderName.field_237253_i_).toFile(); - - try - { - src = src.getCanonicalFile(); - } - catch (Exception ex) - { - } - - Calendar time = Calendar.getInstance(); - File dstFile; - boolean success = false; - StringBuilder out = new StringBuilder(); - - if (customName.isEmpty()) - { - appendNum(out, time.get(Calendar.YEAR), '-'); - appendNum(out, time.get(Calendar.MONTH) + 1, '-'); - appendNum(out, time.get(Calendar.DAY_OF_MONTH), '-'); - appendNum(out, time.get(Calendar.HOUR_OF_DAY), '-'); - appendNum(out, time.get(Calendar.MINUTE), '-'); - appendNum(out, time.get(Calendar.SECOND), '\0'); - } - else - { - out.append(customName); - } - - Exception error = null; - long fileSize = 0L; - - try - { - LinkedHashMap fileMap = new LinkedHashMap<>(); - String mcdir = server.getDataDirectory().getCanonicalFile().getAbsolutePath(); - - Consumer consumer = file0 -> { - for (File file : BackupUtils.listTree(file0)) - { - String s1 = file.getAbsolutePath().replace(mcdir, ""); - - if (s1.startsWith(File.separator)) - { - s1 = s1.substring(File.separator.length()); - } - - fileMap.put(file, "_extra_" + File.separator + s1); - } - }; - - for (String s : FTBBackupsConfig.extraFiles) - { - consumer.accept(new File(s)); - } - - MinecraftForge.EVENT_BUS.post(new BackupEvent.Pre(consumer)); - - for (File file : BackupUtils.listTree(src)) - { - if (file.getName().equals("session.lock") || !file.canRead()) - { - continue; - } - - String filePath = file.getAbsolutePath(); - fileMap.put(file, src.getName() + File.separator + filePath.substring(src.getAbsolutePath().length() + 1)); - } - - for (Map.Entry entry : fileMap.entrySet()) - { - fileSize += BackupUtils.getSize(entry.getKey()); - } - - long totalSize = 0L; - - if (!backups.isEmpty()) - { - backups.sort(null); - - int backupsToKeep = FTBBackupsConfig.backupsToKeep - 1; - - if (backupsToKeep > 0 && backups.size() > backupsToKeep) - { - while (backups.size() > backupsToKeep) - { - Backup backup = backups.remove(0); - FTBBackups.LOGGER.info("Deleting old backup: " + backup.fileId); - BackupUtils.delete(backup.getFile()); - } - } - - for (Backup backup : backups) - { - totalSize += backup.size; - } - - if (fileSize > 0L) - { - long freeSpace = Math.min(FTBBackupsConfig.maxTotalSize, backupsFolder.getFreeSpace()); - - while (totalSize + fileSize > freeSpace && !backups.isEmpty()) - { - Backup backup = backups.remove(0); - totalSize -= backup.size; - FTBBackups.LOGGER.info("Deleting backup to free space: " + backup.fileId); - BackupUtils.delete(backup.getFile()); - } - } - } - - totalFiles = fileMap.size(); - FTBBackups.LOGGER.info("Backing up " + totalFiles + " files..."); - printFiles = true; - - if (FTBBackupsConfig.compressionLevel > 0) - { - out.append(".zip"); - dstFile = BackupUtils.newFile(new File(backupsFolder, out.toString())); - - long start = System.currentTimeMillis(); - - ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dstFile)); - zos.setLevel(FTBBackupsConfig.compressionLevel); - - byte[] buffer = new byte[FTBBackupsConfig.bufferSize]; - - FTBBackups.LOGGER.info("Compressing " + totalFiles + " files..."); - - currentFile = 0; - for (Map.Entry entry : fileMap.entrySet()) - { - try - { - ZipEntry ze = new ZipEntry(entry.getValue()); - currentFileName = entry.getValue(); - - zos.putNextEntry(ze); - FileInputStream fis = new FileInputStream(entry.getKey()); - - int len; - while ((len = fis.read(buffer)) > 0) - { - zos.write(buffer, 0, len); - } - zos.closeEntry(); - fis.close(); - } - catch (Exception ex) - { - FTBBackups.LOGGER.error("Failed to read file " + entry.getValue() + ": " + ex); - } - - currentFile++; - } - - zos.close(); - fileSize = BackupUtils.getSize(dstFile); - FTBBackups.LOGGER.info("Done compressing in " + BackupUtils.getTimeString(System.currentTimeMillis() - start) + " seconds (" + BackupUtils.getSizeString(fileSize) + ")!"); - } - else - { - dstFile = new File(backupsFolder, out.toString()); - dstFile.mkdirs(); - - currentFile = 0; - for (Map.Entry entry : fileMap.entrySet()) - { - try - { - File file = entry.getKey(); - currentFileName = entry.getValue(); - File dst1 = new File(dstFile, entry.getValue()); - BackupUtils.copyFile(file, dst1); - } - catch (Exception ex) - { - FTBBackups.LOGGER.error("Failed to copy file " + entry.getValue() + ": " + ex); - } - - currentFile++; - } - } - - FTBBackups.LOGGER.info("Created " + dstFile.getAbsolutePath() + " from " + src.getAbsolutePath()); - success = true; - } - catch (Exception ex) - { - if (!FTBBackupsConfig.silent) - { - String errorName = ex.getClass().getName(); - notifyAll(server, new TranslationTextComponent("ftbbackups.lang.fail", errorName), true); - } - - ex.printStackTrace(); - error = ex; - } - - printFiles = false; - - Backup backup = new Backup(time.getTimeInMillis(), out.toString().replace('\\', '/'), getLastIndex() + 1, success, fileSize); - backups.add(backup); - MinecraftForge.EVENT_BUS.post(new BackupEvent.Post(backup, error)); - - JsonArray array = new JsonArray(); - - for (Backup backup1 : backups) - { - array.add(backup1.toJsonObject()); - } - - BackupUtils.toJson(new File(server.getDataDirectory(), "local/ftbutilities/backups.json"), array, true); - - if (error == null && !FTBBackupsConfig.silent) - { - String timeString = BackupUtils.getTimeString(System.currentTimeMillis() - time.getTimeInMillis()); - ITextComponent component; - - if (FTBBackupsConfig.displayFileSize) - { - long totalSize = 0L; - - for (Backup backup1 : backups) - { - totalSize += backup1.size; - } - - String sizeB = BackupUtils.getSizeString(fileSize); - String sizeT = BackupUtils.getSizeString(totalSize); - String sizeString = sizeB.equals(sizeT) ? sizeB : (sizeB + " | " + sizeT); - component = new TranslationTextComponent("ftbbackups.lang.end_2", timeString, sizeString); - } - else - { - component = new TranslationTextComponent("ftbbackups.lang.end_1", timeString); - } - - component.getStyle().setColor(Color.func_240744_a_(TextFormatting.LIGHT_PURPLE)); - server.getPlayerList().func_232641_a_(component, ChatType.CHAT, Util.DUMMY_UUID); - } - } - - private void appendNum(StringBuilder sb, int num, char c) - { - if (num < 10) - { - sb.append('0'); - } - sb.append(num); - if (c != '\0') - { - sb.append(c); - } - } - - private int getLastIndex() - { - int i = 0; - - for (Backup b : backups) - { - i = Math.max(i, b.index); - } - - return i; - } -} \ No newline at end of file diff --git a/src/main/java/com/feed_the_beast/mods/ftbbackups/FTBBackups.java b/src/main/java/com/feed_the_beast/mods/ftbbackups/FTBBackups.java deleted file mode 100644 index 953b1d2a..00000000 --- a/src/main/java/com/feed_the_beast/mods/ftbbackups/FTBBackups.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.feed_the_beast.mods.ftbbackups; - -import com.feed_the_beast.mods.ftbbackups.net.BackupProgressPacket; -import com.feed_the_beast.mods.ftbbackups.net.FTBBackupsNetHandler; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.server.MinecraftServer; -import net.minecraft.util.text.StringTextComponent; -import net.minecraftforge.event.RegisterCommandsEvent; -import net.minecraftforge.event.TickEvent; -import net.minecraftforge.event.entity.player.PlayerEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.ExtensionPoint; -import net.minecraftforge.fml.LogicalSide; -import net.minecraftforge.fml.LogicalSidedProvider; -import net.minecraftforge.fml.ModLoadingContext; -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; -import net.minecraftforge.fml.event.server.FMLServerStartedEvent; -import net.minecraftforge.fml.event.server.FMLServerStoppingEvent; -import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; -import net.minecraftforge.fml.network.FMLNetworkConstants; -import net.minecraftforge.fml.network.PacketDistributor; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -@Mod(FTBBackups.MOD_ID) -@Mod.EventBusSubscriber(modid = FTBBackups.MOD_ID) -public class FTBBackups -{ - public static final String MOD_ID = "ftbbackups"; - public static final Logger LOGGER = LogManager.getLogger("FTB Utilities Backups"); - - public FTBBackups() - { - FMLJavaModLoadingContext.get().getModEventBus().addListener(this::clientSetup); - FTBBackupsNetHandler.init(); - FTBBackupsConfig.register(); - ModLoadingContext.get().registerExtensionPoint(ExtensionPoint.DISPLAYTEST, () -> Pair.of(() -> FMLNetworkConstants.IGNORESERVERONLY, (a, b) -> true)); - } - - private void clientSetup(FMLClientSetupEvent event) - { - FTBBackupsClient.init(); - } - - @SubscribeEvent - public static void serverAboutToStart(FMLServerStartedEvent event) - { - Backups.INSTANCE.init(event.getServer()); - } - - @SubscribeEvent - public static void registerCommands(RegisterCommandsEvent event) - { - BackupCommands.register(event.getDispatcher()); - } - - @SubscribeEvent - public static void serverStopping(FMLServerStoppingEvent event) - { - if (FTBBackupsConfig.forceOnShutdown) - { - Backups.INSTANCE.run(event.getServer(), true, new StringTextComponent("Server"), ""); - } - } - - @SubscribeEvent - public static void playerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) - { - if (event.getPlayer() instanceof ServerPlayerEntity) - { - FTBBackupsNetHandler.MAIN.send(PacketDistributor.PLAYER.with(() -> (ServerPlayerEntity) event.getPlayer()), new BackupProgressPacket(Backups.INSTANCE.currentFile, Backups.INSTANCE.totalFiles)); - } - } - - @SubscribeEvent - public static void playerLoggedOut(PlayerEvent.PlayerLoggedOutEvent event) - { - if (event.getPlayer() instanceof ServerPlayerEntity) - { - Backups.INSTANCE.hadPlayersOnline = true; - } - } - - @SubscribeEvent - public static void serverTick(TickEvent.ServerTickEvent event) - { - if (event.phase != TickEvent.Phase.START) - { - MinecraftServer server = LogicalSidedProvider.INSTANCE.get(LogicalSide.SERVER); - - if (server != null) - { - Backups.INSTANCE.tick(server, System.currentTimeMillis()); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/feed_the_beast/mods/ftbbackups/FTBBackupsClient.java b/src/main/java/com/feed_the_beast/mods/ftbbackups/FTBBackupsClient.java deleted file mode 100644 index d7106a19..00000000 --- a/src/main/java/com/feed_the_beast/mods/ftbbackups/FTBBackupsClient.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.feed_the_beast.mods.ftbbackups; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.resources.I18n; -import net.minecraft.util.text.TextFormatting; -import net.minecraftforge.client.event.RenderGameOverlayEvent; -import net.minecraftforge.common.MinecraftForge; - -public class FTBBackupsClient -{ - private static int currentBackupFile = 0; - private static int totalBackupFiles = 0; - - public static void init() - { - //MinecraftForge.EVENT_BUS.addListener(FTBBackupsClient::onClientDisconnected); - MinecraftForge.EVENT_BUS.addListener(FTBBackupsClient::onDebugInfoEvent); - } - - public static void setFiles(int current, int total) - { - currentBackupFile = current; - totalBackupFiles = total; - } - - /*private static void onClientDisconnected(FMLNetworkEvent.ClientDisconnectionFromServerEvent event) - { - currentBackupFile = 0; - totalBackupFiles = 0; - }*/ - - private static void onDebugInfoEvent(RenderGameOverlayEvent.Text event) - { - if (Minecraft.getInstance().gameSettings.showDebugInfo) - { - return; - } - - if (totalBackupFiles > 0 && totalBackupFiles > currentBackupFile) - { - event.getLeft().add(TextFormatting.LIGHT_PURPLE + I18n.format("ftbbackups.lang.timer_progress", currentBackupFile * 100 / totalBackupFiles, currentBackupFile, totalBackupFiles)); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/feed_the_beast/mods/ftbbackups/FTBBackupsConfig.java b/src/main/java/com/feed_the_beast/mods/ftbbackups/FTBBackupsConfig.java deleted file mode 100644 index 63aa7de5..00000000 --- a/src/main/java/com/feed_the_beast/mods/ftbbackups/FTBBackupsConfig.java +++ /dev/null @@ -1,188 +0,0 @@ -package com.feed_the_beast.mods.ftbbackups; - -import net.minecraftforge.common.ForgeConfigSpec; -import net.minecraftforge.fml.ModLoadingContext; -import net.minecraftforge.fml.config.ModConfig; -import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; -import org.apache.commons.lang3.tuple.Pair; - -import java.util.ArrayList; -import java.util.List; - -/** - * @author LatvianModder - */ -public class FTBBackupsConfig -{ - public static boolean auto; - public static boolean silent; - public static int backupsToKeep; - public static long backupTimer; - public static int compressionLevel; - public static String folder; - public static boolean displayFileSize; - public static List extraFiles; - public static long maxTotalSize; - public static boolean onlyIfPlayersOnline; - public static boolean forceOnShutdown; - public static int bufferSize; - - private static Pair common; - - public static void register() - { - FMLJavaModLoadingContext.get().getModEventBus().addListener(FTBBackupsConfig::reload); - - common = new ForgeConfigSpec.Builder().configure(CommonConfig::new); - - ModLoadingContext modLoadingContext = ModLoadingContext.get(); - modLoadingContext.registerConfig(ModConfig.Type.COMMON, common.getRight()); - } - - public static void reload(ModConfig.ModConfigEvent event) - { - ModConfig config = event.getConfig(); - - if (config.getSpec() == common.getRight()) - { - reloadCommon(); - } - } - - public static void reloadCommon() - { - CommonConfig cfg = common.getLeft(); - auto = cfg.auto.get(); - silent = cfg.silent.get(); - backupsToKeep = cfg.backupsToKeep.get(); - backupTimer = cfg.backupTimer.get() * 60000L; - compressionLevel = cfg.compressionLevel.get(); - folder = cfg.folder.get(); - displayFileSize = cfg.displayFileSize.get(); - extraFiles = new ArrayList<>(cfg.extraFiles.get()); - - String mts = cfg.maxTotalSize.get(); - - if (mts.endsWith("TB")) - { - maxTotalSize = Long.parseLong(mts.substring(0, mts.length() - 2).trim()) * BackupUtils.TB; - } - else if (mts.endsWith("GB")) - { - maxTotalSize = Long.parseLong(mts.substring(0, mts.length() - 2).trim()) * BackupUtils.GB; - } - else if (mts.endsWith("MB")) - { - maxTotalSize = Long.parseLong(mts.substring(0, mts.length() - 2).trim()) * BackupUtils.MB; - } - else if (mts.endsWith("KB")) - { - maxTotalSize = Long.parseLong(mts.substring(0, mts.length() - 2).trim()) * BackupUtils.KB; - } - else - { - maxTotalSize = Long.parseLong(mts.trim()); - } - - onlyIfPlayersOnline = cfg.onlyIfPlayersOnline.get(); - forceOnShutdown = cfg.forceOnShutdown.get(); - bufferSize = cfg.bufferSize.get(); - } - - private static class CommonConfig - { - private final ForgeConfigSpec.BooleanValue auto; - private final ForgeConfigSpec.BooleanValue silent; - private final ForgeConfigSpec.IntValue backupsToKeep; - private final ForgeConfigSpec.IntValue backupTimer; - private final ForgeConfigSpec.IntValue compressionLevel; - private final ForgeConfigSpec.ConfigValue folder; - private final ForgeConfigSpec.BooleanValue displayFileSize; - private final ForgeConfigSpec.ConfigValue> extraFiles; - private final ForgeConfigSpec.ConfigValue maxTotalSize; - private final ForgeConfigSpec.BooleanValue onlyIfPlayersOnline; - private final ForgeConfigSpec.BooleanValue forceOnShutdown; - private final ForgeConfigSpec.IntValue bufferSize; - - private CommonConfig(ForgeConfigSpec.Builder builder) - { - auto = builder - .comment("Enables backups to run automatically.") - .translation("ftbbackups.general.auto") - .define("auto", true); - - silent = builder - .comment("If set to true, no messages will be displayed in chat/status bar.") - .translation("ftbbackups.general.silent") - .define("silent", false); - - backupsToKeep = builder - .comment( - "The number of backup files to keep.", - "More backups = more space used", - "0 - Infinite" - ) - .translation("ftbbackups.general.backups_to_keep") - .defineInRange("backups_to_keep", 12, 0, 32000); - - backupTimer = builder - .comment( - "Timer in minutes.", - "5 - backups every 5 minutes", - "60 - backups every hour", - "360 - backups every 6 hours", - "1440 - backups once every day" - ) - .translation("ftbbackups.general.backup_timer") - .defineInRange("backup_timer", 120, 1, 43800); - - compressionLevel = builder - .comment( - "0 - Disabled (output = folders)", - "1 - Best speed", - "9 - Smallest file size" - ) - .translation("ftbbackups.general.compression_level") - .defineInRange("compression_level", 1, 0, 9); - - folder = builder - .comment("Absolute path to backups folder.") - .translation("ftbbackups.general.folder") - .define("folder", ""); - - displayFileSize = builder - .comment("Prints (current size | total size) when backup is done.") - .translation("ftbbackups.general.display_file_size") - .define("display_file_size", true); - - extraFiles = builder - .comment("Add extra files that will be placed in backup _extra_/ folder.") - .translation("ftbbackups.general.extra_files") - .defineList("extra_files", new ArrayList<>(), o -> true); - - maxTotalSize = builder - .comment("Maximum total size that is allowed in backups folder. Older backups will be deleted to free space for newer ones.") - .translation("ftbbackups.general.max_total_size") - .define("max_total_size", "50 GB"); - - onlyIfPlayersOnline = builder - .comment("Only create backups when players have been online.") - .translation("ftbbackups.general.only_if_players_online") - .define("only_if_players_online", true); - - forceOnShutdown = builder - .comment("Create a backup when server is stopped.") - .translation("ftbbackups.general.force_on_shutdown") - .define("force_on_shutdown", false); - - builder.comment("Advanced features that shouldn't be changed unless you know what you are doing.").push("advanced"); - - bufferSize = builder - .comment("Buffer size for writing files.") - .translation("ftbbackups.general.buffer_size") - .defineInRange("buffer_size", 4096, 256, 65536); - - builder.pop(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/feed_the_beast/mods/ftbbackups/net/BackupProgressPacket.java b/src/main/java/com/feed_the_beast/mods/ftbbackups/net/BackupProgressPacket.java deleted file mode 100644 index 4c4de59b..00000000 --- a/src/main/java/com/feed_the_beast/mods/ftbbackups/net/BackupProgressPacket.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.feed_the_beast.mods.ftbbackups.net; - -import com.feed_the_beast.mods.ftbbackups.FTBBackupsClient; -import net.minecraft.network.PacketBuffer; -import net.minecraftforge.fml.network.NetworkEvent; - -import java.util.function.Supplier; - -/** - * @author LatvianModder - */ -public class BackupProgressPacket -{ - private int current, total; - - public BackupProgressPacket(int c, int t) - { - current = c; - total = t; - } - - public BackupProgressPacket(PacketBuffer buf) - { - total = buf.readVarInt(); - current = buf.readVarInt(); - } - - public void write(PacketBuffer buf) - { - buf.writeVarInt(total); - buf.writeVarInt(current); - } - - public void handle(Supplier context) - { - context.get().enqueueWork(() -> FTBBackupsClient.setFiles(current, total)); - context.get().setPacketHandled(true); - } -} \ No newline at end of file diff --git a/src/main/java/com/feed_the_beast/mods/ftbbackups/net/FTBBackupsNetHandler.java b/src/main/java/com/feed_the_beast/mods/ftbbackups/net/FTBBackupsNetHandler.java deleted file mode 100644 index 3380acd9..00000000 --- a/src/main/java/com/feed_the_beast/mods/ftbbackups/net/FTBBackupsNetHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.feed_the_beast.mods.ftbbackups.net; - -import com.feed_the_beast.mods.ftbbackups.FTBBackups; -import net.minecraft.util.ResourceLocation; -import net.minecraftforge.fml.network.NetworkRegistry; -import net.minecraftforge.fml.network.simple.SimpleChannel; - -import java.util.function.Predicate; - -public class FTBBackupsNetHandler -{ - public static SimpleChannel MAIN; - private static final String MAIN_VERSION = "1"; - - public static void init() - { - Predicate validator = v -> MAIN_VERSION.equals(v) || NetworkRegistry.ABSENT.equals(v) || NetworkRegistry.ACCEPTVANILLA.equals(v); - - MAIN = NetworkRegistry.ChannelBuilder - .named(new ResourceLocation(FTBBackups.MOD_ID, "main")) - .clientAcceptedVersions(validator) - .serverAcceptedVersions(validator) - .networkProtocolVersion(() -> MAIN_VERSION) - .simpleChannel(); - - MAIN.registerMessage(1, BackupProgressPacket.class, BackupProgressPacket::write, BackupProgressPacket::new, BackupProgressPacket::handle); - } -} \ No newline at end of file diff --git a/src/main/java/com/feed_the_beast/mods/ftbbackups/net/package-info.java b/src/main/java/com/feed_the_beast/mods/ftbbackups/net/package-info.java deleted file mode 100644 index 3f5e74f1..00000000 --- a/src/main/java/com/feed_the_beast/mods/ftbbackups/net/package-info.java +++ /dev/null @@ -1,3 +0,0 @@ -@javax.annotation.ParametersAreNonnullByDefault -@mcp.MethodsReturnNonnullByDefault -package com.feed_the_beast.mods.ftbbackups.net; \ No newline at end of file diff --git a/src/main/java/dev/ftb/mods/ftbbackups/BackupCommands.java b/src/main/java/dev/ftb/mods/ftbbackups/BackupCommands.java new file mode 100644 index 00000000..ed5bf128 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/BackupCommands.java @@ -0,0 +1,158 @@ +package dev.ftb.mods.ftbbackups; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import dev.architectury.networking.NetworkManager; +import dev.ftb.mods.ftbbackups.config.FTBBackupsClientConfig; +import dev.ftb.mods.ftbbackups.config.FTBBackupsServerConfig; +import dev.ftb.mods.ftblibrary.net.EditConfigChoicePacket; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.storage.LevelResource; + +import java.time.Duration; +import java.time.Instant; + +public class BackupCommands { + public static void register(CommandDispatcher dispatcher) { + LiteralArgumentBuilder backupCommands = Commands.literal(FTBBackups.MOD_ID) + .then(Commands.literal("time") + .executes(ctx -> time(ctx.getSource())) + ) + .then(Commands.literal("start") + .requires(BackupCommands::isAdmin) + .then(Commands.argument("name", StringArgumentType.word()) + .executes(ctx -> start(ctx.getSource(), StringArgumentType.getString(ctx, "name"))) + ) + .executes(ctx -> start(ctx.getSource(), "")) + ) + .then(Commands.literal("size") + .requires(BackupCommands::isAdmin) + .executes(ctx -> size(ctx.getSource())) + ) + .then(Commands.literal("status") + .requires(BackupCommands::isAdmin) + .executes(ctx -> status(ctx.getSource())) + ) + .then(Commands.literal("reset") + .requires(BackupCommands::isAdmin) + .executes(ctx -> resetState(ctx.getSource())) + ) + .then(Commands.literal("serverconfig") + .requires(BackupCommands::isAdmin) + .executes(ctx -> editServerConfig(ctx.getSource())) + ) + .then(Commands.literal("clientconfig") + .requires(CommandSourceStack::isPlayer) + .executes(ctx -> editClientConfig(ctx.getSource())) + ) + .then(Commands.literal("config") + .requires(CommandSourceStack::isPlayer) + .executes(ctx -> editConfig(ctx.getSource())) + ); + + dispatcher.register(backupCommands); + + if (FTBBackupsServerConfig.ADD_BACKUP_COMMAND_ALIAS.get()) { + dispatcher.register(Commands.literal("backup") + .redirect(backupCommands.build())); + + } + } + + private static boolean isAdmin(CommandSourceStack cs) { + return cs.getServer().isSingleplayer() || cs.hasPermission(3); + } + + private static int editServerConfig(CommandSourceStack source) throws CommandSyntaxException { + NetworkManager.sendToPlayer(source.getPlayerOrException(), EditConfigChoicePacket.server(FTBBackupsServerConfig.KEY)); + return Command.SINGLE_SUCCESS; + } + + private static int editClientConfig(CommandSourceStack source) throws CommandSyntaxException { + NetworkManager.sendToPlayer(source.getPlayerOrException(), EditConfigChoicePacket.client(FTBBackupsClientConfig.KEY)); + return Command.SINGLE_SUCCESS; + } + + private static int editConfig(CommandSourceStack source) throws CommandSyntaxException { + NetworkManager.sendToPlayer(source.getPlayerOrException(), EditConfigChoicePacket.choose( + FTBBackupsClientConfig.KEY, FTBBackupsServerConfig.KEY, Component.literal("FTB Backups 3")) + ); + return Command.SINGLE_SUCCESS; + } + + private static int time(CommandSourceStack source) { + Duration d = Duration.between(Instant.now(), Instant.ofEpochMilli(Backups.getServerInstance().nextBackupTime)); + String key = d.isNegative() ? "ftbbackups3.lang.timer.in_past" : "ftbbackups3.lang.timer"; + Duration a = d.abs(); + Component msg = Component.translatable(key, String.format("%02d:%02d:%02d", a.toHoursPart(), a.toMinutesPart(), a.toSecondsPart())); + source.sendSuccess(() -> msg, true); + if (d.isNegative() && FTBBackupsServerConfig.ONLY_IF_PLAYERS_ONLINE.get()) { + source.sendSuccess(() -> Component.translatable("ftbbackups3.lang.timer.no_players"), true); + } + + return Command.SINGLE_SUCCESS; + } + + private static int start(CommandSourceStack source, String customName) { + if (Backups.getServerInstance().run(source.getServer(), false, source.getDisplayName(), customName)) { + for (ServerPlayer player : source.getServer().getPlayerList().getPlayers()) { + player.sendSystemMessage(Component.translatable("ftbbackups3.lang.manual_launch", source.getDisplayName())); + } + } else { + source.sendSuccess(() -> Component.translatable("ftbbackups3.lang.already_running"), true); + } + + return Command.SINGLE_SUCCESS; + } + + private static int size(CommandSourceStack source) { + long totalSize = Backups.getServerInstance().totalBackupSize(); + + source.sendSuccess(() -> Component.translatable("ftbbackups3.lang.size.current", + BackupUtils.formatSizeString(source.getServer().getWorldPath(LevelResource.ROOT))), true); + source.sendSuccess(() -> Component.translatable("ftbbackups3.lang.size.total", + BackupUtils.formatSizeString(totalSize)), true); + source.sendSuccess(() -> Component.translatable("ftbbackups3.lang.size.available", + BackupUtils.formatSizeString(Math.min(FTBBackupsServerConfig.MAX_TOTAL_SIZE.get(), Backups.getServerInstance().backupsFolder.toFile().getFreeSpace()))), true); + + return Command.SINGLE_SUCCESS; + } + + private static int status(CommandSourceStack source) { + source.getServer().getAllLevels().forEach(level -> { + if (level != null) { + Component enabled = Component.translatable("ftbbackups3.lang.autosave." + (level.noSave() ? "disabled" : "enabled")).withStyle(ChatFormatting.AQUA); + Component dim = Component.literal(level.dimension().location().toString()).withStyle(ChatFormatting.YELLOW); + source.sendSuccess(() -> Component.translatable("ftbbackups3.lang.autosave_status", dim, enabled), true); + } + }); + + return Command.SINGLE_SUCCESS; + } + + /** + * @deprecated This seems like a hack around bigger issues + */ + @Deprecated(forRemoval = true, since = "21.1.0") + private static int resetState(CommandSourceStack source) { + source.getServer().getAllLevels().forEach(level -> { + if (level != null) { + source.sendSuccess(() -> Component.literal("Reseting state + saving for " + level.dimension().location()), true); + level.noSave = false; + level.save(null, true, true); + } + }); + + source.sendSuccess(() -> Component.literal("Reseting state + saving for players"), true); + source.getServer().getPlayerList().saveAll(); + + return 1; + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/BackupStatus.java b/src/main/java/dev/ftb/mods/ftbbackups/BackupStatus.java new file mode 100644 index 00000000..40e78e35 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/BackupStatus.java @@ -0,0 +1,19 @@ +package dev.ftb.mods.ftbbackups; + +public enum BackupStatus { + NONE, + RUNNING, + DONE; + + public boolean isRunning() { + return this == RUNNING; + } + + public boolean isDone() { + return this == DONE; + } + + public boolean isRunningOrDone() { + return isRunning() || isDone(); + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/BackupUtils.java b/src/main/java/dev/ftb/mods/ftbbackups/BackupUtils.java new file mode 100644 index 00000000..b13b18e5 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/BackupUtils.java @@ -0,0 +1,141 @@ +package dev.ftb.mods.ftbbackups; + +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonNull; +import com.google.gson.JsonSyntaxException; +import com.google.gson.internal.Streams; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import javax.annotation.Nullable; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.concurrent.atomic.AtomicLong; + +public class BackupUtils { + public static final long KB = 1024L; + public static final long MB = KB * 1024L; + public static final long GB = MB * 1024L; + public static final long TB = GB * 1024L; + + public static final double KB_D = 1024D; + public static final double MB_D = KB_D * 1024D; + public static final double GB_D = MB_D * 1024D; + public static final double TB_D = GB_D * 1024D; + + public static long getSize(Path path) { + final AtomicLong size = new AtomicLong(0); + + try { + Files.walkFileTree(path, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + size.addAndGet(attrs.size()); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + Backups.LOGGER.error("getSize: skipped: {} ({})", file, exc); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) { + if (exc != null) { + Backups.LOGGER.error("getSize: had trouble traversing dir: {} ({})", dir, exc); + } + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw new AssertionError("walkFileTree will not throw IOException if the FileVisitor does not"); + } + + return size.get(); + } + + public static String formatSizeString(double b) { + if (b >= TB_D) { + return String.format("%.1fTB", b / TB_D); + } else if (b >= GB_D) { + return String.format("%.1fGB", b / GB_D); + } else if (b >= MB_D) { + return String.format("%.1fMB", b / MB_D); + } else if (b >= KB_D) { + return String.format("%.1fKB", b / KB_D); + } + + return ((long) b) + "B"; + } + + public static String formatSizeString(Path file) { + return formatSizeString(getSize(file)); + } + + public static void writeJson(Writer writer, @Nullable JsonElement element, boolean prettyPrinting) { + if (element == null || element.isJsonNull()) { + try { + writer.write("null"); + } catch (IOException ex) { + throw new JsonIOException(ex); + } + + return; + } + + JsonWriter jsonWriter = new JsonWriter(writer); + jsonWriter.setLenient(true); + jsonWriter.setHtmlSafe(false); + jsonWriter.setSerializeNulls(true); + + if (prettyPrinting) { + jsonWriter.setIndent("\t"); + } + + try { + Streams.write(element, jsonWriter); + } catch (IOException ex) { + throw new JsonIOException(ex); + } + } + + public static void writeJson(Path jsonFile, @Nullable JsonElement element, boolean prettyPrinting) { + try { + Files.createDirectories(jsonFile.getParent()); + try (OutputStreamWriter output = new OutputStreamWriter(new FileOutputStream(jsonFile.toFile()), StandardCharsets.UTF_8); + BufferedWriter writer = new BufferedWriter(output)) { + writeJson(writer, element, prettyPrinting); + } + } catch (IOException e) { + Backups.LOGGER.error("could not write JSON file {}: {} / {}", jsonFile, e.getClass(), e.getMessage()); + } + } + + public static JsonElement readJson(Path jsonFile) { + if (!Files.exists(jsonFile)) { + return JsonNull.INSTANCE; + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(jsonFile.toFile()), StandardCharsets.UTF_8))) { + JsonReader jsonReader = new JsonReader(reader); + jsonReader.setLenient(true); + JsonElement element = Streams.parse(jsonReader); + + if (!element.isJsonNull() && jsonReader.peek() != JsonToken.END_DOCUMENT) { + throw new JsonSyntaxException("Did not consume the entire document."); + } + + return element; + } catch (Exception ex) { + return JsonNull.INSTANCE; + } + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/Backups.java b/src/main/java/dev/ftb/mods/ftbbackups/Backups.java new file mode 100644 index 00000000..47410048 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/Backups.java @@ -0,0 +1,401 @@ +package dev.ftb.mods.ftbbackups; + +import com.google.gson.JsonElement; +import com.mojang.serialization.JsonOps; +import dev.ftb.mods.ftbbackups.api.Backup; +import dev.ftb.mods.ftbbackups.api.IArchivalPlugin; +import dev.ftb.mods.ftbbackups.api.event.BackupEvent; +import dev.ftb.mods.ftbbackups.archival.ArchivePluginManager; +import dev.ftb.mods.ftbbackups.config.FTBBackupsServerConfig; +import dev.ftb.mods.ftbbackups.net.BackupProgressPacket; +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextColor; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.storage.LevelResource; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.common.NeoForge; +import org.apache.commons.lang3.mutable.MutableInt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +public class Backups { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); + + public static final Logger LOGGER = LoggerFactory.getLogger(Backups.class); + public static final String BACKUPS_JSON_FILE = "backups.json"; + + private static Backups clientInstance = null; + private static Backups serverInstance = null; + + private final List backups = new ArrayList<>(); + public Path backupsFolder; + public long nextBackupTime = -1L; + public BackupStatus backupStatus; + public boolean printFiles = false; + + private final AtomicInteger currentFileIndex = new AtomicInteger(0); + private final AtomicInteger totalFileCount = new AtomicInteger(0); + private int prevFileIndex = 0; + private String currentFileName = ""; + public boolean playersOnlineSinceLastBackup = false; + + public static Backups getClientInstance() { + if (clientInstance == null) { + clientInstance = new Backups(); + } + clientInstance.loadIndexFile(); + return clientInstance; + } + + public static void initServerInstance() { + serverInstance = new Backups(); + } + + public static Backups getServerInstance() { + if (serverInstance == null) { + initServerInstance(); + } + return serverInstance; + } + + private Backups() { + backupsFolder = FTBBackupsServerConfig.getBackupFolder(); + + try { + Files.createDirectories(backupsFolder); + backupsFolder = backupsFolder.toRealPath(); + } catch (Exception ignored) { + LOGGER.error("Failed to create backups folder: {}", backupsFolder); + } + + backupStatus = BackupStatus.NONE; + + loadIndexFile(); + + LOGGER.info("Backups will be written to: {}", backupsFolder); + } + + public void loadIndexFile() { + backups.clear(); + + Path backupsIndex = backupsIndexPath(); + JsonElement element = BackupUtils.readJson(backupsIndex); + Backup.LIST_CODEC.parse(JsonOps.INSTANCE, element) + .resultOrPartial(err -> LOGGER.warn("can't parse backups index {}", backupsIndex)) + .ifPresent(backups::addAll); + } + + private static Path backupsIndexPath() { + return FMLPaths.GAMEDIR.get().resolve(FTBBackupsServerConfig.DEFAULT_BACKUP_FOLDER).resolve(BACKUPS_JSON_FILE); + } + + public void tick(MinecraftServer server, long now) { + if (nextBackupTime < 0) { + // will be the case on the first tick + nextBackupTime = System.currentTimeMillis() + FTBBackupsServerConfig.getBackupTimerMillis(); + } + + if (nextBackupTime > 0L && nextBackupTime <= now) { + if (!FTBBackupsServerConfig.ONLY_IF_PLAYERS_ONLINE.get() || playersOnlineSinceLastBackup || !server.getPlayerList().getPlayers().isEmpty()) { + playersOnlineSinceLastBackup = false; + run(server, true, Component.translatable("ftbbackups3.lang.server"), ""); + } + } + + if (backupStatus.isDone()) { + backupStatus = BackupStatus.NONE; + + for (ServerLevel level : server.getAllLevels()) { + if (level != null) { + level.noSave = false; + } + } + + if (!FTBBackupsServerConfig.SILENT.get()) { + BackupProgressPacket.sendProgress(server, BackupProgressPacket.complete()); + } + + currentFileIndex.set(0); + totalFileCount.set(0); + } else if (backupStatus.isRunning() && printFiles) { + int curIdx = currentFileIndex.get(); + if (curIdx == 0 || curIdx == totalFileCount.get() - 1) { + LOGGER.info("[{} | {}%]: {}", currentFileIndex, (int) ((curIdx / (double) totalFileCount.get()) * 100D), currentFileName); + } + + if (!FTBBackupsServerConfig.SILENT.get() && prevFileIndex != currentFileIndex.get()) { + BackupProgressPacket.sendProgress(server, BackupProgressPacket.update()); + prevFileIndex = currentFileIndex.get(); + } + } + } + + public void notifyAll(MinecraftServer server, Component component, boolean error) { + component = component.plainCopy(); + component.getStyle().withColor(TextColor.fromLegacyFormat(error ? ChatFormatting.DARK_RED : ChatFormatting.LIGHT_PURPLE)); + server.getPlayerList().broadcastSystemMessage(component, true); + } + + public boolean run(MinecraftServer server, boolean auto, Component name, String customName) { + if (backupStatus.isRunningOrDone()) { + return false; + } + + if (auto && !FTBBackupsServerConfig.AUTO.get()) { + return false; + } + + notifyAll(server, Component.translatable("ftbbackups3.lang.start", name), false); + LOGGER.info("backup starting: {}", name.getString()); + nextBackupTime = System.currentTimeMillis() + FTBBackupsServerConfig.getBackupTimerMillis(); + + for (ServerLevel level : server.getAllLevels()) { + if (level != null) { + level.noSave = true; + } + } + + backupStatus = BackupStatus.RUNNING; + server.getPlayerList().saveAll(); + + CompletableFuture.runAsync(() -> { + try { + createBackup(server, customName); + } catch (Exception ex) { + LOGGER.error("backup creation failed: {} -> {}", ex.getClass(), ex.getMessage()); + server.execute(() -> notifyAll(server, Component.translatable("ftbbackups3.lang.saving_failed"), true)); + } + }).thenRun(() -> backupStatus = BackupStatus.DONE); + + return true; + } + + private void createBackup(MinecraftServer server, String customName) { + Path absoluteWorldPath = server.getWorldPath(LevelResource.ROOT).toAbsolutePath(); + + IArchivalPlugin archivalPlugin = ArchivePluginManager.serverInstance().getPlugin(FTBBackupsServerConfig.archivalPlugin()); + if (archivalPlugin == null) { + Backups.LOGGER.error("bad archive plugin setting in server config: {}! backup skipped", FTBBackupsServerConfig.archivalPlugin()); + return; + } + + String backupFileName = archivalPlugin.addFileExtension(customName.isEmpty() ? DATE_TIME_FORMATTER.format(LocalDateTime.now()) : customName); + + long now = Util.getEpochMillis(); + boolean success = false; + Exception error = null; + long archiveSize = 0L; + + MutableInt fileCount = new MutableInt(0); + try { + var manifest = gatherFiles(server); + fileCount.setValue(manifest.size()); + + for (Map.Entry entry : manifest.entrySet()) { + archiveSize += Files.size(entry.getKey()); + } + + doArchiveCleanup(archiveSize); + + totalFileCount.set(manifest.size()); + LOGGER.info("Backing up {} files...", totalFileCount); + printFiles = true; + + BackupProgressPacket.sendProgress(server, BackupProgressPacket.start()); + + currentFileIndex.set(0); + Path archiveDest = backupsFolder.resolve(backupFileName); + long start = Util.getEpochMillis(); + archiveSize = archivalPlugin.createArchive(new BackupContext(manifest, LOGGER, archiveDest, FTBBackupsServerConfig.COMPRESSION_LEVEL.get(), this)); + LOGGER.info("Created archive {} from {} in {} ms ({})!", + archiveDest, absoluteWorldPath, + Util.getEpochMillis() - start, BackupUtils.formatSizeString(archiveSize) + ); + + success = true; + } catch (Exception ex) { + if (!FTBBackupsServerConfig.SILENT.get()) { + String errorName = ex.getClass().getName(); + notifyAll(server, Component.translatable("ftbbackups3.lang.fail", errorName), true); + } + LOGGER.error("Backup to {} failed: {} -> {}", backupFileName, ex.getClass(), ex.getMessage()); + error = ex; + } + + printFiles = false; + + Backup backup = new Backup(now, archivalPlugin.getId(), backupFileName, server.getWorldData().getLevelName(), getLastIndex() + 1, success, archiveSize, fileCount.getValue()); + backups.add(backup); + + Backup.LIST_CODEC.encodeStart(JsonOps.INSTANCE, backups) + .ifSuccess(json -> BackupUtils.writeJson(backupsIndexPath(), json, true)); + + NeoForge.EVENT_BUS.post(new BackupEvent.Post(backup, error)); + + if (error == null && !FTBBackupsServerConfig.SILENT.get()) { + processBackupResults(server, now, archiveSize); + } + } + + /** + * Scan the instance, gathering files which will be added to the backup archive + *
    + *
  • All files in the instance's world directory (world/ for dedicated server, saves/{worldname} for SSP
  • + *
  • All extra files found the "extra_files" config setting (must be relative to instance dir)
  • + *
  • Any extra files added by listeners of {@code BackupEvent.Pre}
  • + *
+ * @param server the server + * @return a map of absolute filepath -> string path of location within the archive + * @throws IOException if any file-related problems occur + */ + private Map gatherFiles(MinecraftServer server) throws IOException { + Map res = new LinkedHashMap<>(); + + Path instanceDir = server.getServerDirectory().toRealPath(); + + Consumer extras = path -> { + try { + Files.walkFileTree(path, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (file.isAbsolute()) { + LOGGER.error("ignoring absolute file {} in extras!", file); + } else { + res.put(file.toRealPath(), file.toString()); + } + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + + for (String s : FTBBackupsServerConfig.EXTRA_FILES.get()) { + extras.accept(Path.of(s)); + } + + NeoForge.EVENT_BUS.post(new BackupEvent.Pre(extras)); + + Files.walkFileTree(server.getWorldPath(LevelResource.ROOT).toAbsolutePath(), new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (!file.endsWith("session.lock") && Files.isReadable(file)) { + res.put(file.toRealPath(), instanceDir.relativize(file).toString()); + } + return FileVisitResult.CONTINUE; + } + }); + + return res; + } + + /** + * Check if any existing backup archives need to be purged, based on the "backups_to_keep" and "max_total_size" + * config settings. + * + * @param fileSize the file size of the archive about to be created + */ + private void doArchiveCleanup(long fileSize) { + if (!backups.isEmpty()) { + backups.sort(null); + + int backupsToKeep = FTBBackupsServerConfig.BACKUPS_TO_KEEP.get() - 1; + if (backupsToKeep > 0 && backups.size() > backupsToKeep) { + while (backups.size() > backupsToKeep) { + Backup backup = backups.removeFirst(); + backup.deleteFiles(backupsFolder); + } + } + + long totalSize = backups.stream().mapToLong(Backup::size).sum(); + + if (fileSize > 0L) { + long freeSpace = Math.min(FTBBackupsServerConfig.MAX_TOTAL_SIZE.get(), backupsFolder.toFile().getFreeSpace()); + while (totalSize + fileSize > freeSpace && !backups.isEmpty()) { + Backup backup = backups.removeFirst(); + if (backup.deleteFiles(backupsFolder)) { + totalSize -= backup.size(); + } + } + } + } + } + + private void processBackupResults(MinecraftServer server, long backupTime, long archiveSize) { + Duration d = Duration.ZERO.plusMillis(Util.getEpochMillis() - backupTime); + String timeString = String.format("%d.%03ds", d.toSeconds(), d.toMillisPart()); + Component component; + + if (FTBBackupsServerConfig.DISPLAY_FILE_SIZE.get()) { + long totalSize = backups.stream().mapToLong(Backup::size).sum(); + + String sizeB = BackupUtils.formatSizeString(archiveSize); + String sizeT = BackupUtils.formatSizeString(totalSize); + String sizeString = sizeB.equals(sizeT) ? sizeB : (sizeB + " | " + sizeT); + component = Component.translatable("ftbbackups3.lang.end_2", timeString, sizeString); + } else { + component = Component.translatable("ftbbackups3.lang.end_1", timeString); + } + + component.getStyle().withColor(TextColor.fromLegacyFormat(ChatFormatting.LIGHT_PURPLE)); + server.getPlayerList().broadcastSystemMessage(component, true); + } + + public long totalBackupSize() { + return backups.stream().mapToLong(Backup::size).sum(); + } + + private int getLastIndex() { + int i = 0; + + for (Backup b : backups) { + i = Math.max(i, b.index()); + } + + return i; + } + + public int getCurrentFileIndex() { + return currentFileIndex.get(); + } + + public int getTotalFileCount() { + return totalFileCount.get(); + } + + public Collection backups() { + return Collections.unmodifiableCollection(backups); + } + + private record BackupContext( + Map manifest, + Logger logger, + Path archivePath, + int compressionLevel, + Backups backups + ) implements IArchivalPlugin.ArchivalContext { + @Override + public synchronized void notifyProcessingFile(String filename) { + backups.currentFileName = filename; + backups.currentFileIndex.incrementAndGet(); + } + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/FTBBackups.java b/src/main/java/dev/ftb/mods/ftbbackups/FTBBackups.java new file mode 100644 index 00000000..94faac73 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/FTBBackups.java @@ -0,0 +1,109 @@ +package dev.ftb.mods.ftbbackups; + +import dev.ftb.mods.ftbbackups.api.event.RegisterArchivalPluginEvent; +import dev.ftb.mods.ftbbackups.archival.ArchivePluginManager; +import dev.ftb.mods.ftbbackups.archival.FileCopyArchiver; +import dev.ftb.mods.ftbbackups.archival.ZipArchiver; +import dev.ftb.mods.ftbbackups.client.BackupsClient; +import dev.ftb.mods.ftbbackups.config.FTBBackupsClientConfig; +import dev.ftb.mods.ftbbackups.config.FTBBackupsServerConfig; +import dev.ftb.mods.ftbbackups.net.FTBBackupsNetHandler; +import dev.ftb.mods.ftblibrary.config.manager.ConfigManager; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.EventPriority; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.server.ServerAboutToStartEvent; +import net.neoforged.neoforge.event.server.ServerStoppedEvent; +import net.neoforged.neoforge.event.server.ServerStoppingEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import org.slf4j.LoggerFactory; + +@Mod(FTBBackups.MOD_ID) +public class FTBBackups { + public static final String MOD_ID = "ftbbackups3"; + + public FTBBackups(IEventBus eventBus, ModContainer container) { + if (isDisabledByEnvironmentVar()) { + LoggerFactory.getLogger(MOD_ID).info("FTB Backups 3 is disabled by environment variable! (FTB_BACKUPS_DISABLED is set)"); + return; + } + + if (FMLEnvironment.dist == Dist.CLIENT) { + eventBus.addListener(event -> BackupsClient.onModConstruction()); + } + eventBus.addListener(this::registerNetwork); + + ConfigManager.getInstance().registerServerConfig(FTBBackupsServerConfig.CONFIG, MOD_ID + ".general", + true, FTBBackupsServerConfig::onConfigChanged); + ConfigManager.getInstance().registerClientConfig(FTBBackupsClientConfig.CONFIG, MOD_ID + ".general"); + + NeoForge.EVENT_BUS.addListener(EventPriority.HIGH, this::serverAboutToStart); + NeoForge.EVENT_BUS.addListener(EventPriority.HIGH, this::serverStopping); + NeoForge.EVENT_BUS.addListener(EventPriority.HIGH, this::serverStopped); + NeoForge.EVENT_BUS.addListener(EventPriority.HIGH, this::playerLoggedOut); + NeoForge.EVENT_BUS.addListener(EventPriority.HIGH, this::levelTick); + NeoForge.EVENT_BUS.addListener(EventPriority.HIGH, this::registerArchivalPlugins); + + NeoForge.EVENT_BUS.addListener(this::registerCommands); + } + + public void serverAboutToStart(ServerAboutToStartEvent event) { + Backups.initServerInstance(); + + NeoForge.EVENT_BUS.post(new RegisterArchivalPluginEvent(ArchivePluginManager.serverInstance()::register)); + } + + public void registerCommands(RegisterCommandsEvent event) { + BackupCommands.register(event.getDispatcher()); + } + + public void registerNetwork(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar("3"); + FTBBackupsNetHandler.init(registrar); + } + + public void serverStopping(ServerStoppingEvent event) { + if (FTBBackupsServerConfig.FORCE_ON_SHUTDOWN.get()) { + Backups.getServerInstance().run(event.getServer(), true, Component.literal("Server"), ""); + } + } + + public void serverStopped(ServerStoppedEvent event) { + ArchivePluginManager.serverInstance().clear(); + } + + private void registerArchivalPlugins(RegisterArchivalPluginEvent event) { + event.register(FileCopyArchiver.INSTANCE); + event.register(ZipArchiver.INSTANCE); + } + + public void playerLoggedOut(PlayerEvent.PlayerLoggedOutEvent event) { + if (event.getEntity() instanceof ServerPlayer) { + Backups.getServerInstance().playersOnlineSinceLastBackup = true; + } + } + + public void levelTick(ServerTickEvent.Post event) { + Backups.getServerInstance().tick(event.getServer(), System.currentTimeMillis()); + } + + public static boolean isDisabledByEnvironmentVar() { + return System.getenv().containsKey("FTB_BACKUPS_DISABLED"); + } + + public static ResourceLocation id(String path) { + return ResourceLocation.fromNamespaceAndPath(MOD_ID, path); + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/api/Backup.java b/src/main/java/dev/ftb/mods/ftbbackups/api/Backup.java new file mode 100644 index 00000000..5a9c90e5 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/api/Backup.java @@ -0,0 +1,71 @@ +package dev.ftb.mods.ftbbackups.api; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.ftb.mods.ftbbackups.Backups; +import net.minecraft.resources.ResourceLocation; +import org.apache.commons.io.FileUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * Holds information about one specific backup. + * + * @param time time the backup was created (milliseconds since the epoch) + * @param archivalPlugin the archival plugin used to create this backup + * @param fileId name of the backup on disk, relative to the backup directory defined in config (could be a file or a directory) + * @param worldName human-readable name of the world that is backed up + * @param index a monotonically increasing numeric index + * @param success true if the backup succeeded, false if there were any problems + * @param size nominal size of the backup in bytes + */ +public record Backup(long time, ResourceLocation archivalPlugin, String fileId, String worldName, int index, boolean success, long size, int fileCount) implements Comparable { + public static final Codec CODEC = RecordCodecBuilder.create(builder -> builder.group( + Codec.LONG.fieldOf("time").forGetter(Backup::time), + ResourceLocation.CODEC.fieldOf("archival_plugin").forGetter(Backup::archivalPlugin), + Codec.STRING.fieldOf("file").forGetter(Backup::fileId), + Codec.STRING.optionalFieldOf("world_name", "").forGetter(Backup::worldName), + Codec.INT.fieldOf("index").forGetter(Backup::index), + Codec.BOOL.fieldOf("success").forGetter(Backup::success), + Codec.LONG.fieldOf("size").forGetter(Backup::size), + Codec.INT.fieldOf("file_count").forGetter(Backup::fileCount) + ).apply(builder, Backup::new)); + + public static final Codec> LIST_CODEC = Codec.list(CODEC); + + public int hashCode() { + return Long.hashCode(time); + } + + public String toString() { + return fileId; + } + + public boolean equals(Object o) { + return o == this || (o instanceof Backup && ((Backup) o).time == time); + } + + public boolean deleteFiles(Path backupsFolder) { + Path path = backupsFolder.resolve(fileId); + try { + if (Files.isRegularFile(path)) { + Files.delete(path); + } else if (Files.isDirectory(path)) { + FileUtils.deleteDirectory(path.toFile()); + } + } catch (IOException e) { + Backups.LOGGER.error("Can't delete backup {}: {} / {}", fileId(), e.getClass(), e.getMessage()); + return false; + } + Backups.LOGGER.info("Deleted backup: {}", fileId()); + return true; + } + + @Override + public int compareTo(Backup o) { + return Long.compare(time, o.time); + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/api/IArchivalPlugin.java b/src/main/java/dev/ftb/mods/ftbbackups/api/IArchivalPlugin.java new file mode 100644 index 00000000..99fceb82 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/api/IArchivalPlugin.java @@ -0,0 +1,124 @@ +package dev.ftb.mods.ftbbackups.api; + +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; +import org.slf4j.Logger; + +import java.nio.file.Path; +import java.util.Map; + +/** + * Implementations of this interface can be provided to + * {@link dev.ftb.mods.ftbbackups.api.event.RegisterArchivalPluginEvent#register(IArchivalPlugin)} to register new + * archival plugins. The event is fired on the server, when the server is starting up, and lazily on the client + * when populating GUI data. + */ +public interface IArchivalPlugin { + /** + * {@return a unique ID for this plugin; it should be namespaced with your mod name} + */ + ResourceLocation getId(); + + /** + * Get the file extension for backup files created by this plugin. By default, this is the path component + * of the ID returned by {@link #getId()}, and is appended (along with a ".") to the filename. + *

+ * An empty string return is valid here to indicate no file extension. + * + * @return the file extension + */ + default String getFileExtension() { + return getId().getPath(); + } + + /** + * Called when actually creating the archive file + * + * @param context the archival context + * @return the nominal size of the archived, in bytes + * @throws Exception thrown if anything goes wrong + */ + @ApiStatus.OverrideOnly + long createArchive(ArchivalContext context) throws Exception; + + /** + * Called to restore a backup archive, overwriting existing files in a game instance. This is only + * called from the client world selection screen for SSP instances, while no world is actually running; + * it's not relevant for dedicated server instances. + * + * @param context the restoration context + * @throws Exception thrown if anything goes wrong + */ + @ApiStatus.OverrideOnly + void restoreArchive(RestorationContext context) throws Exception; + + /** + * Convenience method to append a file extension + * @param fileName the base file name + * @return a filename with the archive extension appended + */ + @ApiStatus.NonExtendable + default String addFileExtension(String fileName) { + return getFileExtension().isEmpty() ? fileName : fileName + "." + getFileExtension(); + } + + /** + * Base interface for archival and restoration contexts. + */ + @ApiStatus.NonExtendable + interface Context { + /** + * {@return a Logger instance, which can be used for any logging that's needed} + */ + Logger logger(); + + /** + * {@return the path to the file (or directory) where the archive should be written to or extracted from} + */ + Path archivePath(); + + /** + * This method should be called by the plugin implementation for each file that is being backed-up or + * restored. This is primarily used to provide feedback to players about backup/restoration progress. + * + * @param filename the filename currently being handled + */ + void notifyProcessingFile(String filename); + } + + /** + * Passed to {@link IArchivalPlugin#createArchive(ArchivalContext)}. This provides information about the backup + * to be made, along with some methods to provide feedback about the backup process. + */ + @ApiStatus.NonExtendable + interface ArchivalContext extends Context { + /** + * Get a manifest of the files to be archived; this is a map of the absolute file path to a string path within + * the archive that is relative to the game instance directory, + * e.g. "/path/to/your/instance/saves/New World/level.dat" -> "saves/New World/level.dat" + * @return the archive manifest + */ + Map manifest(); + + /** + * An integer compression level, in the range 0..9. The plugin is free to interpret this value as appropriate + * (or ignore it completely), but a value of 0 indicates no compression is desired, and a value of 9 + * indicates the maximum possible compression is desired. + * + * @return the desired compression level + */ + int compressionLevel(); + } + + /** + * Passed to {@link IArchivalPlugin#restoreArchive(RestorationContext)}. This provides information about the backup + * being restored, along with some methods to provide feedback about the restoration process. + */ + @ApiStatus.NonExtendable + interface RestorationContext extends Context { + /** + * {@return the folder into which files should be extracted, typically the game instance folder} + */ + Path destinationFolder(); + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/api/event/BackupEvent.java b/src/main/java/dev/ftb/mods/ftbbackups/api/event/BackupEvent.java new file mode 100644 index 00000000..f1efc9f8 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/api/event/BackupEvent.java @@ -0,0 +1,55 @@ +package dev.ftb.mods.ftbbackups.api.event; + +import dev.ftb.mods.ftbbackups.api.Backup; +import net.neoforged.bus.api.Event; + +import javax.annotation.Nullable; +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.Consumer; + +public class BackupEvent extends Event { + /** + * Fired just before a backup is made. This can be used to add extra files to the backup; files added + * must be relative to the game instance directory. + */ + public static class Pre extends BackupEvent { + private final Consumer callback; + + public Pre(Consumer callback) { + this.callback = callback; + } + + public void add(Path file) { + callback.accept(file); + } + } + + /** + * Fired immediately after a backup is made. + */ + public static class Post extends BackupEvent { + private final Backup backup; + private final Exception error; + + public Post(Backup backup, @Nullable Exception error) { + this.backup = backup; + this.error = error; + } + + /** + * {@return the details for the backup that was just made. + */ + public Backup getBackup() { + return backup; + } + + /** + * Get the error, if any, that occurred during this backup. + * @return the exception that was thrown, or {@code Optional.empty()} if the backup was successful + */ + public Optional getError() { + return Optional.ofNullable(error); + } + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/api/event/RegisterArchivalPluginEvent.java b/src/main/java/dev/ftb/mods/ftbbackups/api/event/RegisterArchivalPluginEvent.java new file mode 100644 index 00000000..edcffb4f --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/api/event/RegisterArchivalPluginEvent.java @@ -0,0 +1,24 @@ +package dev.ftb.mods.ftbbackups.api.event; + +import dev.ftb.mods.ftbbackups.api.IArchivalPlugin; +import net.neoforged.bus.api.Event; + +import java.util.function.Consumer; + +/** + * Listen to this event to register custom archival plugins; alternative methods of creating an archive file. Note + * that the mod by default provides archive plugins to create ZIP files, and do a simple recursive file copy. + *

+ * This event is fired on both client and server and plugins should be registered on both sides. + */ +public class RegisterArchivalPluginEvent extends Event { + private final Consumer consumer; + + public RegisterArchivalPluginEvent(Consumer consumer) { + this.consumer = consumer; + } + + public void register(IArchivalPlugin plugin) { + consumer.accept(plugin); + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/archival/ArchivePluginManager.java b/src/main/java/dev/ftb/mods/ftbbackups/archival/ArchivePluginManager.java new file mode 100644 index 00000000..072d5161 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/archival/ArchivePluginManager.java @@ -0,0 +1,52 @@ +package dev.ftb.mods.ftbbackups.archival; + +import dev.ftb.mods.ftbbackups.api.IArchivalPlugin; +import dev.ftb.mods.ftbbackups.api.event.RegisterArchivalPluginEvent; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.neoforge.common.NeoForge; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ArchivePluginManager { + private static final ArchivePluginManager CLIENT = new ArchivePluginManager(); + private static final ArchivePluginManager SERVER = new ArchivePluginManager(); + + private final Map plugins = new ConcurrentHashMap<>(); + + private static boolean pluginsRegistered = false; + + public static ArchivePluginManager clientInstance() { + if (!pluginsRegistered) { + NeoForge.EVENT_BUS.post(new RegisterArchivalPluginEvent(CLIENT::register)); + pluginsRegistered = true; + } + return CLIENT; + } + + public static ArchivePluginManager serverInstance() { + return SERVER; + } + + public synchronized void register(IArchivalPlugin plugin) { + if (plugins.containsKey(plugin.getId())) { + throw new IllegalStateException("archival plugin " + plugin.getId() + " already registered!"); + } + plugins.put(plugin.getId(), plugin); + } + + @Nullable + public IArchivalPlugin getPlugin(ResourceLocation id) { + return plugins.get(id); + } + + public void clear() { + plugins.clear(); + } + + public Map plugins() { + return Collections.unmodifiableMap(plugins); + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/archival/FileCopyArchiver.java b/src/main/java/dev/ftb/mods/ftbbackups/archival/FileCopyArchiver.java new file mode 100644 index 00000000..06ab2b51 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/archival/FileCopyArchiver.java @@ -0,0 +1,63 @@ +package dev.ftb.mods.ftbbackups.archival; + +import dev.ftb.mods.ftbbackups.FTBBackups; +import dev.ftb.mods.ftbbackups.api.IArchivalPlugin; +import net.minecraft.resources.ResourceLocation; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Map; + +public enum FileCopyArchiver implements IArchivalPlugin { + INSTANCE; + + public static final ResourceLocation ID = FTBBackups.id("filecopy"); + + @Override + public ResourceLocation getId() { + return ID; + } + + @Override + public String getFileExtension() { + return ""; + } + + @Override + public long createArchive(ArchivalContext context) { + long archiveSize = 0L; + for (Map.Entry entry : context.manifest().entrySet()) { + try { + Path file = entry.getKey(); + context.notifyProcessingFile(entry.getValue()); + Path newFile = context.archivePath().resolve(Path.of(entry.getValue())); + Files.createDirectories(newFile.getParent()); + Files.copy(file, newFile, StandardCopyOption.COPY_ATTRIBUTES); + archiveSize += Files.size(newFile); + } catch (Exception ex) { + context.logger().error("Failed to copy file {}: {}", entry.getValue(), ex); + } + } + return archiveSize; + } + + @Override + public void restoreArchive(RestorationContext context) throws Exception { + Path srcDir = context.archivePath(); + try (var s = Files.walk(srcDir)) { + s.forEach(source -> { + if (Files.isRegularFile(source)) { + Path destination = context.destinationFolder().resolve(srcDir.relativize(source)); + try { + Files.createDirectories(destination.getParent()); + context.notifyProcessingFile(source.toString()); + Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); + } catch (Exception ex) { + context.logger().error("Failed to copy {} -> {}: {}", source, destination, ex); + } + } + }); + } + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/archival/ZipArchiver.java b/src/main/java/dev/ftb/mods/ftbbackups/archival/ZipArchiver.java new file mode 100644 index 00000000..9e49bf4b --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/archival/ZipArchiver.java @@ -0,0 +1,100 @@ +package dev.ftb.mods.ftbbackups.archival; + +import dev.ftb.mods.ftbbackups.FTBBackups; +import dev.ftb.mods.ftbbackups.config.FTBBackupsServerConfig; +import dev.ftb.mods.ftbbackups.api.IArchivalPlugin; +import net.minecraft.resources.ResourceLocation; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +public enum ZipArchiver implements IArchivalPlugin { + INSTANCE; + + public static final ResourceLocation ID = FTBBackups.id("zip"); + + @Override + public ResourceLocation getId() { + return ID; + } + + @Override + public long createArchive(ArchivalContext context) throws IOException { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(context.archivePath().toFile())); + zos.setLevel(context.compressionLevel()); + + byte[] buffer = new byte[FTBBackupsServerConfig.BUFFER_SIZE.get()]; + + context.logger().info("Zipping {} files...", context.manifest().size()); + + for (Map.Entry entry : context.manifest().entrySet()) { + Path absPath = entry.getKey(); + String archiveEntry = entry.getValue(); + try { + context.notifyProcessingFile(archiveEntry); + ZipEntry ze = new ZipEntry(archiveEntry); + + zos.putNextEntry(ze); + FileInputStream fis = new FileInputStream(absPath.toFile()); + + int len; + while ((len = fis.read(buffer)) > 0) { + zos.write(buffer, 0, len); + } + zos.closeEntry(); + fis.close(); + } catch (IOException ex) { + throw new IOException(String.format("Failed to add file %s to ZIP archive: %s", archiveEntry, ex)); + } + } + + zos.close(); + return Files.size(context.archivePath()); + } + + @Override + public void restoreArchive(RestorationContext context) throws IOException { + ZipInputStream zis = new ZipInputStream(new FileInputStream(context.archivePath().toFile())); + + byte[] buffer = new byte[FTBBackupsServerConfig.BUFFER_SIZE.get()]; + + ZipEntry zipEntry = zis.getNextEntry(); + while (zipEntry != null) { + context.notifyProcessingFile(zipEntry.getName()); + if (zipEntry.isDirectory()) { + context.logger().warn("ignoring directory entry {}", zipEntry.getName()); + continue; + } + + Path destFile = safePath(context.destinationFolder(), zipEntry); + Files.createDirectories(destFile.getParent()); + context.logger().info("extract to {}", destFile); + try (FileOutputStream fos = new FileOutputStream(destFile.toFile())) { + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + } + + zipEntry = zis.getNextEntry(); + } + + zis.closeEntry(); + zis.close(); + } + + private static Path safePath(Path destDir, ZipEntry zipEntry) throws IOException { + Path subPath = Path.of(zipEntry.getName()); + if (subPath.isAbsolute()) { + throw new IOException("absolute paths not allowed!"); + } + return destDir.resolve(subPath); + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/client/BackupOverlayLayer.java b/src/main/java/dev/ftb/mods/ftbbackups/client/BackupOverlayLayer.java new file mode 100644 index 00000000..5ff5b0bf --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/client/BackupOverlayLayer.java @@ -0,0 +1,74 @@ +package dev.ftb.mods.ftbbackups.client; + +import dev.ftb.mods.ftbbackups.FTBBackups; +import dev.ftb.mods.ftbbackups.config.FTBBackupsClientConfig; +import dev.ftb.mods.ftblibrary.icon.Color4I; +import dev.ftb.mods.ftblibrary.ui.GuiHelper; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.LayeredDraw; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; + +public class BackupOverlayLayer implements LayeredDraw.Layer { + public static final ResourceLocation ID = FTBBackups.id("overlay"); + + private int prevProgress; + + @Override + public void render(GuiGraphics guiGraphics, DeltaTracker deltaTracker) { + if (!FTBBackupsClientConfig.SHOW_OVERLAY.get()) { + return; + } + + BackupsClient.BackupProgress progress = BackupsClient.getBackupProgress(); + if (!progress.inProgress()) { + return; + } + + Component line1 = progress.finished() ? + Component.translatable("ftbbackups3.lang.finished") : + Component.translatable("ftbbackups3.lang.already_running"); + Component line2 = BackupsClient.progressMessage(); + + Minecraft mc = Minecraft.getInstance(); + Font font = mc.font; + int width = mc.getWindow().getGuiScaledWidth() / 4; + + int progressBarHeight = font.lineHeight + 4; + int height = font.lineHeight + progressBarHeight + 8; + float scale = 1f; + + int insetX = FTBBackupsClientConfig.OVERLAY_INSET_X.get(); + int insetY = FTBBackupsClientConfig.OVERLAY_INSET_Y.get(); + var pos = FTBBackupsClientConfig.OVERLAY_POS.get().getPanelPos( + mc.getWindow().getGuiScaledWidth(), mc.getWindow().getGuiScaledHeight(), + (int) (width * scale), (int) (height * scale), + insetX, insetY + ); + + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(pos.x(), pos.y(), 100); + guiGraphics.pose().scale(scale, scale, 1F); + + // panel background + Color4I.rgba(0x80200020).draw(guiGraphics, -2, -2, width + 4, height + 4); + GuiHelper.drawHollowRect(guiGraphics, -2, -2, width + 4, height + 4, Color4I.rgba(0x80400040), false); + + guiGraphics.drawString(font, line1, 2, 2, 0xFFFAF9F6, false); + + int pw = (int) ((width - 4) * Mth.lerp(deltaTracker.getGameTimeDeltaTicks(), prevProgress, progress.current()) / progress.total()); + int sy = font.lineHeight + 4; + guiGraphics.fill(2, sy - 1, width - 3, sy + progressBarHeight + 1, 0xC00060E0); + guiGraphics.fill(3, sy, pw, sy + progressBarHeight, 0xC00040A0); + + guiGraphics.drawString(font, line2, 14, 2 + font.lineHeight + 5, 0xFFFAF9F6, false); + + guiGraphics.pose().popPose(); + + prevProgress = progress.current(); + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/client/BackupsClient.java b/src/main/java/dev/ftb/mods/ftbbackups/client/BackupsClient.java new file mode 100644 index 00000000..0b5f7f87 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/client/BackupsClient.java @@ -0,0 +1,92 @@ +package dev.ftb.mods.ftbbackups.client; + +import dev.ftb.mods.ftbbackups.Backups; +import dev.ftb.mods.ftbbackups.FTBBackups; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent; +import net.neoforged.neoforge.client.event.ScreenEvent; +import net.neoforged.neoforge.common.NeoForge; +import org.jetbrains.annotations.NotNull; + +@EventBusSubscriber(modid = FTBBackups.MOD_ID, value = Dist.CLIENT, bus = EventBusSubscriber.Bus.MOD) +public class BackupsClient { + private static BackupProgress backupProgress = BackupProgress.NONE; + + private static int progressTicker = 0; + + public static void onModConstruction() { + NeoForge.EVENT_BUS.addListener(BackupsClient::onClientDisconnected); + NeoForge.EVENT_BUS.addListener(BackupsClient::addRestoreButton); + NeoForge.EVENT_BUS.addListener(BackupsClient::clientTick); + } + + private static void clientTick(ClientTickEvent.Post event) { + if (progressTicker > 0 && --progressTicker == 0) { + backupProgress = BackupProgress.NONE; + } + } + + private static void addRestoreButton(ScreenEvent.Init.Post event) { + if (event.getScreen() instanceof SelectWorldScreen s) { + Backups backups = Backups.getClientInstance(); + if (!backups.backups().isEmpty()) { + Component btnLabel = Component.translatable("ftbbackups3.gui.restore").append("..."); + Component title = Component.translatable("ftbbackups3.gui.restore"); + int w = s.getMinecraft().font.width(btnLabel) + 20; + event.addListener(Button.builder(btnLabel, b -> Minecraft.getInstance().setScreen(new RestoreBackupScreen(s, title))) + .bounds(s.width - w - 10, 22, w, 20) + .tooltip(Tooltip.create(Component.translatable("ftbbackups3.gui.restore.tooltip"))) + .build()); + } + } + } + + @SubscribeEvent + public static void registerGuiLayer(RegisterGuiLayersEvent event) { + // needs to be done with SubscribeEvent, addListener() is called too late + if (!FTBBackups.isDisabledByEnvironmentVar()) { + event.registerAboveAll(BackupOverlayLayer.ID, new BackupOverlayLayer()); + } + } + + public static void setBackupProgress(int current, int total) { + backupProgress = new BackupProgress(current, total); + if (backupProgress.finished()) { + progressTicker = 40; + } + } + + private static void onClientDisconnected(ClientPlayerNetworkEvent.LoggingOut ignoredEvent) { + backupProgress = BackupProgress.NONE; + } + + public static @NotNull MutableComponent progressMessage() { + return Component.translatable("ftbbackups3.lang.timer_progress", backupProgress.current() * 100 / backupProgress.total(), backupProgress.current(), backupProgress.total()); + } + + public static BackupProgress getBackupProgress() { + return backupProgress; + } + + public record BackupProgress(int current, int total) { + public static final BackupProgress NONE = new BackupProgress(0, 0); + + public boolean inProgress() { + return total > 0; + } + + public boolean finished() { + return current == total; + } + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/client/RestoreBackupScreen.java b/src/main/java/dev/ftb/mods/ftbbackups/client/RestoreBackupScreen.java new file mode 100644 index 00000000..75650a40 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/client/RestoreBackupScreen.java @@ -0,0 +1,314 @@ +package dev.ftb.mods.ftbbackups.client; + +import dev.ftb.mods.ftbbackups.Backups; +import dev.ftb.mods.ftbbackups.config.FTBBackupsServerConfig; +import dev.ftb.mods.ftbbackups.api.Backup; +import dev.ftb.mods.ftbbackups.api.IArchivalPlugin; +import dev.ftb.mods.ftbbackups.archival.ArchivePluginManager; +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractSelectionList; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.fml.loading.FMLPaths; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; + +public class RestoreBackupScreen extends Screen { + private Button restoreButton; + private BackupsList backupsList; + private EditBox searchBox; + private long totalFiles; + private final AtomicLong currentFile = new AtomicLong(0L); + private String restoringPath = ""; + private int completedTimer = 0; + private String lastError = ""; + + private static final ResourceLocation SUCCESS = ResourceLocation.withDefaultNamespace("pending_invite/accept"); + private static final ResourceLocation FAILURE = ResourceLocation.withDefaultNamespace("pending_invite/reject"); + + static final int UPPER_HEIGHT = 80; + static final int LOWER_HEIGHT = 40; + private final Screen prevScreen; + + protected RestoreBackupScreen(Screen prevScreen, Component title) { + super(title); + + this.prevScreen = prevScreen; + } + + @Override + protected void init() { + super.init(); + + backupsList = new BackupsList(minecraft, width, height - UPPER_HEIGHT - LOWER_HEIGHT, UPPER_HEIGHT); + + searchBox = new EditBox(font, width / 2 - 160 / 2, 40, 160, 20, Component.empty()); + searchBox.setResponder(backupsList::onFilterChanged); + + addRenderableWidget(Button.builder(CommonComponents.GUI_BACK, btn -> onClose()) + .size(100, 20).pos(width / 2 - 130, height - 30).build()); + + restoreButton = Button.builder(Component.translatable("ftbbackups3.gui.restore_now"), btn -> onActivate()) + .size(150, 20).pos(width / 2 - 20, height - 30).build(); + addRenderableWidget(restoreButton); + restoreButton.active = false; + + addRenderableWidget(searchBox); + addRenderableWidget(backupsList); + } + + private void onActivate() { + BackupsList.Entry selected = backupsList.getSelected(); + + if (selected != null) { + minecraft.setScreen(new ConfirmScreen(confirmed -> doRestore(confirmed, selected), + Component.translatable("ftbbackups3.gui.confirm_restore", selected.backup.worldName(), selected.backup.fileId()), + Component.translatable("ftbbackups3.gui.confirm_restore.line_2")) + ); + } + } + + private void doRestore(boolean confirmed, BackupsList.Entry selected) { + minecraft.setScreen(this); + + if (confirmed) { + Path backupsFolder = FTBBackupsServerConfig.getBackupFolder(); + + Backup backup = selected.backup; + + IArchivalPlugin plugin = ArchivePluginManager.clientInstance().getPlugin(backup.archivalPlugin()); + + if (plugin != null) { + CompletableFuture.runAsync(() -> { + IArchivalPlugin.RestorationContext ctx = new RestoreContext(backupsFolder.resolve(backup.fileId()), FMLPaths.GAMEDIR.get(), Backups.LOGGER, this); + totalFiles = backup.fileCount(); + currentFile.set(0L); + Backups.LOGGER.info("STUB: do restore! {} -> {}", ctx.archivePath(), ctx.destinationFolder()); + try { + plugin.restoreArchive(ctx); + } catch (Exception e) { + throw new RuntimeException(e); + } + }).whenComplete((unused, ex) -> { + if (ex != null) { + Backups.LOGGER.error("backup restoration failed: {} -> {}", ex.getClass(), ex.getMessage()); + lastError = ex.getMessage(); + } else { + Backups.LOGGER.info("backup restoration complete!"); + lastError = ""; + } + totalFiles = 0L; + currentFile.set(0L); + restoringPath = ""; + completedTimer = 50; + }); + } else { + Backups.LOGGER.info("unknown archival plugin {}!", backup.archivalPlugin()); + } + } + } + + @Override + protected void setInitialFocus() { + setInitialFocus(searchBox); + } + + @Override + public void onClose() { + minecraft.setScreen(prevScreen); + } + + @Override + public boolean shouldCloseOnEsc() { + return totalFiles == 0L && super.shouldCloseOnEsc(); + } + + @Override + public void tick() { + super.tick(); + + if (completedTimer > 0) { + completedTimer--; + } + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + super.render(guiGraphics, mouseX, mouseY, partialTick); + + guiGraphics.drawCenteredString(font, title, width / 2, 8, 0xFFFFFF); + + renderExtractionProgress(guiGraphics, totalFiles); + } + + private void renderExtractionProgress(GuiGraphics guiGraphics, long total) { + if (total == 0L && completedTimer == 0) { + return; + } + + int pw = guiGraphics.guiWidth() * 3 / 4; + int ph = guiGraphics.guiHeight() / 4; + int px = (guiGraphics.guiWidth() - pw) / 2; + int py = (guiGraphics.guiHeight() - ph) / 2; + + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(0, 0, 200); + guiGraphics.fill(px - 2, py - 2, px + pw + 2, py + ph + 2, 0xFF404040); + guiGraphics.fill(px, py, px + pw, py + ph, 0xFFC0C0C0); + + if (total > 0L) { + guiGraphics.drawString(font, Component.translatable("ftbbackups3.gui.restore.in_progress"), px + 5, py + 5, 0xFF404040, false); + guiGraphics.drawString(font, Component.literal(restoringPath), px + 5, py + ph - font.lineHeight - 5, 0xFF404040, false); + + // progress bar + int sh = ph / 4; + int sy = py + (ph - sh) / 2; + guiGraphics.fill(px + 10, sy, px + pw - 10, sy + sh, 0xFF202020); + int sw = (int) ((pw - 24) * currentFile.get() / total); + guiGraphics.fill(px + 12, sy + 2, px + 10 + sw, sy + sh - 2, 0xFF0040C0); + + guiGraphics.pose().popPose(); + } else if (completedTimer > 0) { + if (lastError.isEmpty()) { + guiGraphics.blitSprite(SUCCESS, px + 10, py + (ph - 32) / 2, 0, 32, 32); + guiGraphics.drawString(font, Component.translatable("ftbbackups3.gui.restore.success"), px + 50, py + (ph - font.lineHeight) / 2, 0x404040, false); + } else { + guiGraphics.blitSprite(FAILURE, px + 10, py + (ph - 32) / 2, 0, 32, 32); + guiGraphics.drawString(font, Component.translatable("ftbbackups3.gui.restore.failure", lastError), px + 50, py + (ph - font.lineHeight) / 2, 0x404040, false); + } + } + } + + private class BackupsList extends AbstractSelectionList { + public BackupsList(Minecraft minecraft, int width, int height, int y) { + super(minecraft, width, height, y, 30); + + addChildren(""); + } + + private void addChildren(String filter) { + List entries = new ArrayList<>(); + + String filterL = filter.toLowerCase(Locale.ROOT); + + for (Backup b : Backups.getClientInstance().backups()) { + if (b.success() && b.fileCount() > 0 && (filter.isEmpty() || b.worldName().toLowerCase(Locale.ROOT).contains(filterL))) { + entries.add(new Entry(b)); + } + } + + // TODO controls to alter sort order + + children().addAll(entries.stream().sorted((o1, o2) -> o2.backup.compareTo(o1.backup)).toList()); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput ignored) { + } + + @Override + public int getRowWidth() { + return 400; + } + + @Override + protected int getScrollbarPosition() { + return width / 2 + 200; + } + + @Override + public void setSelected(@Nullable RestoreBackupScreen.BackupsList.Entry entry) { + RestoreBackupScreen.this.restoreButton.active = entry != null; + super.setSelected(entry); + } + + private void onFilterChanged(String filter) { + children().clear(); + addChildren(filter); + } + + private class Entry extends AbstractSelectionList.Entry { + private static final String DATE_FORMAT = "MMM d, yyyy HH:mm a"; + + private final Backup backup; + private long lastClickTime; + + public Entry(Backup backup) { + this.backup = backup; + } + + @Override + public boolean mouseClicked(double x, double y, int partialTick) { + BackupsList.this.setSelected(this); + + if (Util.getMillis() - lastClickTime < 250L) { + RestoreBackupScreen.this.onActivate(); + return true; + } else { + lastClickTime = Util.getMillis(); + return false; + } + } + + @Override + public void render(GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean hovering, float partialTick) { + Font font = Minecraft.getInstance().font; + + int startX = left + 80; + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT); + ZonedDateTime ldt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(backup.time()), ZoneId.systemDefault()); + + int y0 = top + 5; + int lh = font.lineHeight + 2; + + guiGraphics.drawString(font, Component.translatable(backup.worldName()), startX, y0, 0xFFFFFF); + guiGraphics.drawString(font, Component.literal(ldt.format(formatter)), startX + 10, y0 + lh, 0xC0C0C0); + + if (isMouseOver(mouseX, mouseY)) { + setTooltipForNextRenderPass(Stream.of( + Component.translatable("ftbbackups3.gui.list.file", + Component.literal(backup.fileId()).withStyle(ChatFormatting.GRAY)), + Component.translatable("ftbbackups3.gui.list.plugin", + Component.literal(backup.archivalPlugin().toString()).withStyle(ChatFormatting.GRAY)) + ) + .map(MutableComponent::getVisualOrderText) + .toList() + ); + } + } + } + } + + private record RestoreContext(Path archivePath, Path destinationFolder, Logger logger, RestoreBackupScreen screen) implements IArchivalPlugin.RestorationContext { + @Override + public void notifyProcessingFile(String filename) { + screen.currentFile.incrementAndGet(); + screen.restoringPath = filename; + logger.debug("restore file: {}", filename); + } + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/client/SelectArchivalPluginScreen.java b/src/main/java/dev/ftb/mods/ftbbackups/client/SelectArchivalPluginScreen.java new file mode 100644 index 00000000..8a80065f --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/client/SelectArchivalPluginScreen.java @@ -0,0 +1,79 @@ +package dev.ftb.mods.ftbbackups.client; + +import dev.ftb.mods.ftbbackups.archival.ArchivePluginManager; +import dev.ftb.mods.ftbbackups.config.ArchivalPluginConfig; +import dev.ftb.mods.ftblibrary.config.ConfigCallback; +import dev.ftb.mods.ftblibrary.icon.Color4I; +import dev.ftb.mods.ftblibrary.ui.Panel; +import dev.ftb.mods.ftblibrary.ui.SimpleTextButton; +import dev.ftb.mods.ftblibrary.ui.Theme; +import dev.ftb.mods.ftblibrary.ui.input.Key; +import dev.ftb.mods.ftblibrary.ui.input.MouseButton; +import dev.ftb.mods.ftblibrary.ui.misc.AbstractButtonListScreen; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +public class SelectArchivalPluginScreen extends AbstractButtonListScreen { + private final ArchivalPluginConfig config; + private final ConfigCallback callback; + + public SelectArchivalPluginScreen(ArchivalPluginConfig config, ConfigCallback callback) { + this.config = config; + this.callback = callback; + + setTitle(Component.translatable("ftbbackups3.general.archival_plugin")); + showBottomPanel(false); + showCloseButton(true); + } + + @Override + public void addButtons(Panel panel) { + ArchivePluginManager.clientInstance().plugins().keySet() + .forEach(k -> panel.add(new PluginButton(panel, k))); + } + + @Override + public boolean onClosedByKey(Key key) { + if (super.onClosedByKey(key)) { + callback.save(false); + return true; + } + + return false; + } + + @Override + protected void doCancel() { + callback.save(false); + } + + @Override + protected void doAccept() { + callback.save(true); + } + + private class PluginButton extends SimpleTextButton { + private final ResourceLocation value; + + PluginButton(Panel panel, ResourceLocation value) { + super(panel, Component.literal(value.toString()), Color4I.empty()); + this.value = value; + } + + @Override + public void drawBackground(GuiGraphics graphics, Theme theme, int x, int y, int w, int h) { + if (isMouseOver) { + Color4I.WHITE.withAlpha(30).draw(graphics, x, y, w, h); + } + Color4I.GRAY.withAlpha(40).draw(graphics, x, y + h, w, 1); + } + + @Override + public void onClicked(MouseButton mouseButton) { + playClickSound(); + config.setCurrentValue(value); + callback.save(true); + } + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/config/ArchivalPluginConfig.java b/src/main/java/dev/ftb/mods/ftbbackups/config/ArchivalPluginConfig.java new file mode 100644 index 00000000..49e4b0b4 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/config/ArchivalPluginConfig.java @@ -0,0 +1,17 @@ +package dev.ftb.mods.ftbbackups.config; + +import dev.ftb.mods.ftbbackups.client.SelectArchivalPluginScreen; +import dev.ftb.mods.ftblibrary.config.ConfigCallback; +import dev.ftb.mods.ftblibrary.config.ConfigValue; +import dev.ftb.mods.ftblibrary.ui.Widget; +import dev.ftb.mods.ftblibrary.ui.input.MouseButton; +import net.minecraft.resources.ResourceLocation; + +public class ArchivalPluginConfig extends ConfigValue { + @Override + public void onClicked(Widget widget, MouseButton mouseButton, ConfigCallback configCallback) { + if (getCanEdit()) { + new SelectArchivalPluginScreen(this, configCallback).openGui(); + } + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/config/ArchivalPluginValue.java b/src/main/java/dev/ftb/mods/ftbbackups/config/ArchivalPluginValue.java new file mode 100644 index 00000000..cc86ef23 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/config/ArchivalPluginValue.java @@ -0,0 +1,35 @@ +package dev.ftb.mods.ftbbackups.config; + +import dev.ftb.mods.ftblibrary.config.ConfigGroup; +import dev.ftb.mods.ftblibrary.snbt.SNBTCompoundTag; +import dev.ftb.mods.ftblibrary.snbt.config.BaseValue; +import dev.ftb.mods.ftblibrary.snbt.config.SNBTConfig; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class ArchivalPluginValue extends BaseValue { + public ArchivalPluginValue(@Nullable SNBTConfig config, String key, ResourceLocation def) { + super(config, key, def); + } + + @Override + public void write(SNBTCompoundTag tag) { + List s = new ArrayList<>(comment); + s.add("Default: " + defaultValue); + tag.comment(key, String.join("\n", s)); + tag.putString(key, get().toString()); + } + + @Override + public void read(SNBTCompoundTag snbtCompoundTag) { + set(ResourceLocation.parse(snbtCompoundTag.getString(key))); + } + + @Override + public void createClientConfig(ConfigGroup group) { + group.add(key, new ArchivalPluginConfig(), get(), this::set, defaultValue); + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/config/FTBBackupsClientConfig.java b/src/main/java/dev/ftb/mods/ftbbackups/config/FTBBackupsClientConfig.java new file mode 100644 index 00000000..5c2347c6 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/config/FTBBackupsClientConfig.java @@ -0,0 +1,25 @@ +package dev.ftb.mods.ftbbackups.config; + +import dev.ftb.mods.ftbbackups.FTBBackups; +import dev.ftb.mods.ftblibrary.snbt.config.BooleanValue; +import dev.ftb.mods.ftblibrary.snbt.config.EnumValue; +import dev.ftb.mods.ftblibrary.snbt.config.IntValue; +import dev.ftb.mods.ftblibrary.snbt.config.SNBTConfig; +import dev.ftb.mods.ftblibrary.util.PanelPositioning; + +public interface FTBBackupsClientConfig { + String KEY = FTBBackups.MOD_ID + "-client"; + SNBTConfig CONFIG = SNBTConfig.create(KEY) + .comment("Client-specific configuration for FTB Backups 3", + "Modpack defaults should be defined in /config/" + KEY + ".snbt", + " (may be overwritten on modpack update)", + "Players may locally override this by copying into /local/" + KEY + ".snbt", + " (will NOT be overwritten on modpack update)" + ); + + SNBTConfig OVERLAY = CONFIG.addGroup("overlay"); + BooleanValue SHOW_OVERLAY = OVERLAY.addBoolean("show_overlay", true); + EnumValue OVERLAY_POS = OVERLAY.addEnum("overlay_pos", PanelPositioning.NAME_MAP, PanelPositioning.TOP_LEFT); + IntValue OVERLAY_INSET_X = OVERLAY.addInt("overlay_inset_x", 4); + IntValue OVERLAY_INSET_Y = OVERLAY.addInt("overlay_inset_y", 4); +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/config/FTBBackupsServerConfig.java b/src/main/java/dev/ftb/mods/ftbbackups/config/FTBBackupsServerConfig.java new file mode 100644 index 00000000..60b66c04 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/config/FTBBackupsServerConfig.java @@ -0,0 +1,136 @@ +package dev.ftb.mods.ftbbackups.config; + + +import dev.ftb.mods.ftbbackups.BackupUtils; +import dev.ftb.mods.ftbbackups.Backups; +import dev.ftb.mods.ftbbackups.FTBBackups; +import dev.ftb.mods.ftbbackups.archival.ZipArchiver; +import dev.ftb.mods.ftblibrary.snbt.config.*; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.common.util.Lazy; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; + +public interface FTBBackupsServerConfig { + String KEY = FTBBackups.MOD_ID + "-server"; + SNBTConfig CONFIG = SNBTConfig.create(KEY) + .comment("Server-specific configuration for FTB Backups 3", + "Modpack defaults should be defined in /config/" + KEY + ".snbt", + " (may be overwritten on modpack update)", + "Server admins may locally override this by copying into /world/serverconfig/" + KEY + ".snbt", + " (will NOT be overwritten on modpack update)" + ); + + // relative to the game directory: /ftbbackups3 + String DEFAULT_BACKUP_FOLDER = FTBBackups.MOD_ID; + + BooleanValue AUTO = CONFIG.addBoolean("auto", true) + .comment("Enables backups to run automatically"); + + BooleanValue SILENT = CONFIG.addBoolean("silent", false) + .comment("If set to true, no messages will be displayed in chat/status bar"); + + IntValue BACKUPS_TO_KEEP = CONFIG.addInt("backups_to_keep", 12, 0, 32000) + .comment("The number of backup files to keep.", + "More backups = more space used", + "0 - Infinite" + ); + + IntValue BACKUP_TIMER_MINUTES = CONFIG.addInt("backup_timer", 120, 1, 43800) + .comment( + "Backup frequency in minutes.", + "5 - backups every 5 minutes", + "60 - backups every hour", + "360 - backups every 6 hours", + "1440 - backups once every day" + ); + + IntValue COMPRESSION_LEVEL = CONFIG.addInt("compression_level", 5, 0, 9) + .comment("Compression level for archived files. Note that this is dependent on the particular plugin in use.", + "Higher values typically mean backups take longer to do but take less space on disk.", + "0 - No compression", + "1 - Minimal compression", + "9 - Full compression" + ); + + StringValue FOLDER = CONFIG.addString("folder", "") + .comment("Absolute path to backups folder. Default of \"\" means to use \"ftbackups3\" within the game instance folder."); + + BooleanValue DISPLAY_FILE_SIZE = CONFIG.addBoolean("display_file_size", true) + .comment("Broadcasts to all online players a \"(current size | total size)\" message when backup is done."); + + StringListValue EXTRA_FILES = CONFIG.addStringList("extra_files", new ArrayList<>()) + .comment("Add extra files/folders to be backed up, in addition to the world folder. These files *must* be within the game instance!"); + + StringValue MAX_TOTAL_SIZE_RAW = CONFIG.addString("max_total_size", "50 GB") + .comment("Maximum total size that is allowed in backups folder. Older backups will be deleted to free space for newer ones."); + + BooleanValue ONLY_IF_PLAYERS_ONLINE = CONFIG.addBoolean("only_if_players_online", true) + .comment("Only create backups when at least one player is online."); + + BooleanValue FORCE_ON_SHUTDOWN = CONFIG.addBoolean("force_on_shutdown", false) + .comment("Create a backup when server is stopped."); + + SNBTConfig ADVANCED = CONFIG.addGroup("advanced") + .comment("Advanced features that shouldn't be changed unless you know what you are doing."); + + ArchivalPluginValue ARCHIVAL_PLUGIN = CONFIG.add(new ArchivalPluginValue(CONFIG, "archival_plugin", ZipArchiver.ID)) + .comment("Method to use to create a backup archive.", + "Builtin methods are \"ftbbackups:zip\" (create a ZIP file) and \"ftbbackups:filecopy\" (simple recursive copy of files with no compression)", + "More archival plugins may be added by other mods."); + + BooleanValue NOTIFY_ADMINS_ONLY = CONFIG.addBoolean("notify_admins_only", false) + .comment("If true, only player with permission level >= 2 (or SSP integrated server owners) will be notified on-screen about backup progress"); + + IntValue BUFFER_SIZE = ADVANCED.addInt("buffer_size", 4096, 256, 65536) + .comment("Buffer size for reading/writing files."); + + BooleanValue ADD_BACKUP_COMMAND_ALIAS = CONFIG.addBoolean("add_backup_command_alias", true) + .comment("If true, the /backup command will be aliased to /ftbbackups3 backup"); + + Lazy MAX_TOTAL_SIZE = Lazy.of(() -> { + String mts = MAX_TOTAL_SIZE_RAW.get(); + + long maxTotalSize; + long l = Long.parseLong(mts.substring(0, mts.length() - 2).trim()); + if (mts.endsWith("TB")) { + maxTotalSize = l * BackupUtils.TB; + } else if (mts.endsWith("GB")) { + maxTotalSize = l * BackupUtils.GB; + } else if (mts.endsWith("MB")) { + maxTotalSize = l * BackupUtils.MB; + } else if (mts.endsWith("KB")) { + maxTotalSize = l * BackupUtils.KB; + } else { + maxTotalSize = Long.parseLong(mts.trim()); + } + return maxTotalSize; + }); + + static long getBackupTimerMillis() { + return BACKUP_TIMER_MINUTES.get() * 60000L; // 60000 millisecs in a minute + } + + static void onConfigChanged(boolean ignoredIsServer) { + MAX_TOTAL_SIZE.invalidate(); + } + + static ResourceLocation archivalPlugin() { + ResourceLocation rl = ARCHIVAL_PLUGIN.get(); + if (rl == null) { + Backups.LOGGER.error("Invalid archive plugin id {}, defaulting to ftbbackups:zip!", ARCHIVAL_PLUGIN.get()); + return ZipArchiver.ID; + } + return rl; + } + + static Path getBackupFolder() { + String folder = FTBBackupsServerConfig.FOLDER.get(); + return folder.trim().isEmpty() ? + FMLPaths.GAMEDIR.get().resolve(DEFAULT_BACKUP_FOLDER) : + Paths.get(folder); + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/net/BackupProgressPacket.java b/src/main/java/dev/ftb/mods/ftbbackups/net/BackupProgressPacket.java new file mode 100644 index 00000000..02df7758 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/net/BackupProgressPacket.java @@ -0,0 +1,59 @@ +package dev.ftb.mods.ftbbackups.net; + +import dev.ftb.mods.ftbbackups.Backups; +import dev.ftb.mods.ftbbackups.FTBBackups; +import dev.ftb.mods.ftbbackups.config.FTBBackupsServerConfig; +import dev.ftb.mods.ftbbackups.client.BackupsClient; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.neoforge.network.PacketDistributor; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +import java.util.List; + +public record BackupProgressPacket(int current, int total) implements CustomPacketPayload { + public static final Type TYPE = new Type<>(FTBBackups.id("backup_progress")); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.VAR_INT, BackupProgressPacket::current, + ByteBufCodecs.VAR_INT, BackupProgressPacket::total, + BackupProgressPacket::new + ); + + public static void handler(BackupProgressPacket message, IPayloadContext context) { + BackupsClient.setBackupProgress(message.current, message.total); + } + + public static BackupProgressPacket start() { + return new BackupProgressPacket(0, Backups.getServerInstance().getTotalFileCount()); + } + + public static BackupProgressPacket update() { + return new BackupProgressPacket(Backups.getServerInstance().getCurrentFileIndex(), Backups.getServerInstance().getTotalFileCount()); + } + + public static BackupProgressPacket complete() { + return new BackupProgressPacket(Backups.getServerInstance().getTotalFileCount(), Backups.getServerInstance().getTotalFileCount()); + } + + public static void sendProgress(MinecraftServer server, BackupProgressPacket packet) { + getPlayersToNotify(server).forEach(sp -> PacketDistributor.sendToPlayer(sp, packet)); + } + + private static List getPlayersToNotify(MinecraftServer server) { + return FTBBackupsServerConfig.NOTIFY_ADMINS_ONLY.get() ? + server.getPlayerList().getPlayers().stream() + .filter(sp -> sp.hasPermissions(2) || server.isSingleplayerOwner(sp.getGameProfile())) + .toList() : + server.getPlayerList().getPlayers(); + } + + @Override + public Type type() { + return TYPE; + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/net/FTBBackupsNetHandler.java b/src/main/java/dev/ftb/mods/ftbbackups/net/FTBBackupsNetHandler.java new file mode 100644 index 00000000..b1ff10af --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/net/FTBBackupsNetHandler.java @@ -0,0 +1,9 @@ +package dev.ftb.mods.ftbbackups.net; + +import net.neoforged.neoforge.network.registration.PayloadRegistrar; + +public class FTBBackupsNetHandler { + public static void init(final PayloadRegistrar registrar) { + registrar.playToClient(BackupProgressPacket.TYPE, BackupProgressPacket.STREAM_CODEC, BackupProgressPacket::handler); + } +} diff --git a/src/main/java/dev/ftb/mods/ftbbackups/net/package-info.java b/src/main/java/dev/ftb/mods/ftbbackups/net/package-info.java new file mode 100644 index 00000000..5332d546 --- /dev/null +++ b/src/main/java/dev/ftb/mods/ftbbackups/net/package-info.java @@ -0,0 +1,2 @@ +@javax.annotation.ParametersAreNonnullByDefault +package dev.ftb.mods.ftbbackups.net; \ No newline at end of file diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml deleted file mode 100644 index f4c37a11..00000000 --- a/src/main/resources/META-INF/mods.toml +++ /dev/null @@ -1,28 +0,0 @@ -modLoader = "javafml" -loaderVersion = "[32,)" -license = "All Rights Reserved" - -[[mods]] -modId = "ftbbackups" -version = "${file.jarVersion}" -displayName = "FTB Backups" -displayURL = "https://www.curseforge.com/minecraft/mc-mods/ftb-backups" -logoFile = "logo.png" -authors = "LatvianModder" -description = ''' -Creates backups of your world -''' - -[[dependencies.ftbbackups]] -modId="forge" -mandatory = true -versionRange = "[32,)" -ordering = "NONE" -side="BOTH" - -[[dependencies.ftbbackups]] -modId="minecraft" -mandatory = true -versionRange = "[1.16,)" -ordering = "NONE" -side="BOTH" \ No newline at end of file diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..f0e52529 --- /dev/null +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,21 @@ +modLoader = "javafml" +loaderVersion = "${loader_version_range}" +license = "All Rights Reserved" + +[[mods]] +modId = "ftbbackups3" +version = "${version}" +displayName = "FTB Backups 3" +displayURL = "https://www.curseforge.com/minecraft/mc-mods/ftb-backups" +logoFile = "logo.png" +authors = "FTB Team" +description = ''' +Creates backups of your world +''' + +[[dependencies.ftbbackups3]] +modId = "neoforge" +mandatory = true +versionRange = "${mod_loader_range}" +ordering = "NONE" +side="BOTH" diff --git a/src/main/resources/assets/ftbbackups/lang/de_de.json b/src/main/resources/assets/ftbbackups/lang/de_de.json deleted file mode 100644 index 1be98af8..00000000 --- a/src/main/resources/assets/ftbbackups/lang/de_de.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "_comment": "Config", - "ftbbackups.general": "Allgemein", - "ftbbackups.general.silent": "Still", - "ftbbackups.general.backups_to_keep": "Sicherungen zum Behalten", - "ftbbackups.general.backup_timer": "Sicherungszeitgeber", - "ftbbackups.general.compression_level": "Komprimierungsstufe", - "ftbbackups.general.folder": "Ordner", - "ftbbackups.general.display_file_size": "Dateigröße Anzeigen", - "ftbbackups.general.use_separate_thread": "Separaten Aktivitätsträger Nutzen", - "ftbbackups.general.extra_files": "Zusätzliche Dateien", - "ftbbackups.general.max_total_size": "Maximale Gesamtgröße", - "ftbbackups.general.disable_level_saving": "Stufenspeicherung Deaktivieren", - "ftbbackups.general.only_if_players_online": "Sicherung machen, nur wenn Spieler online sind.", - "ftbbackups.general.force_on_shutdown": "Das Erstellen einer Sicherung erzwingen beim Herunterfahren.", - "ftbbackups.general.buffer_size": "Puffergröße", - "_comment": "Misc", - "ftbbackups.lang.timer": "Serversicherung in: %s", - "ftbbackups.lang.timer_progress": "[%d%% | %d / %d]", - "ftbbackups.lang.start": "Serversicherung startete!", - "ftbbackups.lang.end_1": "Serversicherung ist in %s fertig!", - "ftbbackups.lang.end_2": "Serversicherung ist in %s fertig! (%s)", - "ftbbackups.lang.fail": "Die Speicherung der Welt ist fehlgeschlagen! (%s)", - "ftbbackups.lang.manual_launch": "%s startete eine manuelle Sicherung!", - "ftbbackups.lang.already_running": "Sicherung in Arbeit!", - "ftbbackups.lang.size.current": "Aktuelle Weltgröße: %s", - "ftbbackups.lang.size.total": "Die Größe des Sicherungsordners insgesamt: %s", - "ftbbackups.lang.saving_failed": "Die Speicherung der Welt ist fehlgeschlagen!", - "_comment": "Commands", - "commands.backup.usage": "/backup", - "commands.backup.start.usage": "/backup start [benutzerdefinierter Name]", - "commands.backup.time.usage": "/backup time", - "commands.backup.size.usage": "/backup size" -} \ No newline at end of file diff --git a/src/main/resources/assets/ftbbackups/lang/en_us.json b/src/main/resources/assets/ftbbackups/lang/en_us.json deleted file mode 100644 index e05778b5..00000000 --- a/src/main/resources/assets/ftbbackups/lang/en_us.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "_comment": "Config", - "ftbbackups.general": "General", - "ftbbackups.general.auto": "Auto", - "ftbbackups.general.silent": "Silent", - "ftbbackups.general.backups_to_keep": "Backups to Keep", - "ftbbackups.general.backup_timer": "Backup Timer", - "ftbbackups.general.compression_level": "Compression Level", - "ftbbackups.general.folder": "Folder", - "ftbbackups.general.display_file_size": "Display File Size", - "ftbbackups.general.use_separate_thread": "Use Separate Thread", - "ftbbackups.general.extra_files": "Extra Files", - "ftbbackups.general.max_total_size": "Maximum Total Size", - "ftbbackups.general.disable_level_saving": "Disable Level Saving", - "ftbbackups.general.only_if_players_online": "Backup Only If Players are Online", - "ftbbackups.general.force_on_shutdown": "Force Backup on Shutdown", - "ftbbackups.general.buffer_size": "Buffer Size", - "_comment": "Misc", - "ftbbackups.lang.timer": "Server backup in: %s", - "ftbbackups.lang.timer_progress": "[%d%% | %d / %d]", - "ftbbackups.lang.start": "Server Backup started!", - "ftbbackups.lang.end_1": "Server backup done in %s!", - "ftbbackups.lang.end_2": "Server backup done in %s! (%s)", - "ftbbackups.lang.fail": "Failed to save world! (%s)", - "ftbbackups.lang.manual_launch": "%s launched a manual backup!", - "ftbbackups.lang.already_running": "Backup in progress!", - "ftbbackups.lang.size.current": "Current world size: %s", - "ftbbackups.lang.size.total": "Total backups folder size: %s", - "ftbbackups.lang.size.available": "Available: %s", - "ftbbackups.lang.saving_failed": "World saving failed!", - "_comment": "Commands", - "commands.backup.usage": "/backup", - "commands.backup.start.usage": "/backup start [custom name]", - "commands.backup.time.usage": "/backup time", - "commands.backup.size.usage": "/backup size" -} \ No newline at end of file diff --git a/src/main/resources/assets/ftbbackups/lang/pl_pl.json b/src/main/resources/assets/ftbbackups/lang/pl_pl.json deleted file mode 100644 index 593c311d..00000000 --- a/src/main/resources/assets/ftbbackups/lang/pl_pl.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "_comment": "Konfiguracja", - "ftbbackups.general": "Ogólny", - "ftbbackups.general.auto": "Automatyczny", - "ftbbackups.general.silent": "Cichy", - "ftbbackups.general.backups_to_keep": "Kopia zapasowa do zachowania", - "ftbbackups.general.backup_timer": "Czas kopii zapasowej", - "ftbbackups.general.compression_level": "Poziom kompresji", - "ftbbackups.general.folder": "Folder", - "ftbbackups.general.display_file_size": "Wyświetl rozmiar pliku", - "ftbbackups.general.use_separate_thread": "Użyj oddzielnego wątku", - "ftbbackups.general.extra_files": "Dodatkowe pliki", - "ftbbackups.general.max_total_size": "Maksymalny całkowity rozmiar", - "ftbbackups.general.disable_level_saving": "Wyłącz zapisywanie poziomu", - "ftbbackups.general.only_if_players_online": "Rób tylko kopie zapasowe kiedy gracze są w grze", - "ftbbackups.general.force_on_shutdown": "Wymuś kopię zapasową podczas zamykania", - "ftbbackups.general.buffer_size": "Rozmiar bufora", - "_comment": "Misc", - "ftbbackups.lang.timer": "Serwerowa kopia zapasowa zapisze się w: %s", - "ftbbackups.lang.timer_progress": "[%d%% | %d / %d]", - "ftbbackups.lang.start": "Robienie kopii zapasowej serwera zaczęło się!", - "ftbbackups.lang.end_1": "Robienie kopii zapasowej skończyło się w %s!", - "ftbbackups.lang.end_2": "Robienie kopii zapasowej skończyło się w %s! (%s)", - "ftbbackups.lang.fail": "Niepowodzenie w zapisaniu świata! (%s)", - "ftbbackups.lang.manual_launch": "%s odpalił ręczną kopię zapasową!", - "ftbbackups.lang.already_running": "Kopia zapasowa się robi!", - "ftbbackups.lang.size.current": "Aktualny rozmiar świata: %s", - "ftbbackups.lang.size.total": "Całkowity rozmiar folderu kopii zapasowych: %s", - "ftbbackups.lang.size.available": "Dostępne: %s", - "ftbbackups.lang.saving_failed": "Niepowodzenie w zapisywaniu świata!", - "_comment": "Commands", - "commands.backup.usage": "/backup", - "commands.backup.start.usage": "/backup start [custom name]", - "commands.backup.time.usage": "/backup time", - "commands.backup.size.usage": "/backup size" -} diff --git a/src/main/resources/assets/ftbbackups/lang/ru_ru.json b/src/main/resources/assets/ftbbackups/lang/ru_ru.json deleted file mode 100644 index 1ce7bb59..00000000 --- a/src/main/resources/assets/ftbbackups/lang/ru_ru.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "_comment": "Config", - "ftbbackups.general": "Общее", - "ftbbackups.general.auto": "Автоматический режим", - "ftbbackups.general.silent": "Тихий режим", - "ftbbackups.general.backups_to_keep": "Количество резервных копий", - "ftbbackups.general.backup_timer": "Частота создания резервных копий", - "ftbbackups.general.compression_level": "Уровень сжатия", - "ftbbackups.general.folder": "Папка", - "ftbbackups.general.display_file_size": "Отображать размер файла", - "ftbbackups.general.use_separate_thread": "Использовать отдельный поток", - "ftbbackups.general.extra_files": "Дополнительные файлы", - "ftbbackups.general.max_total_size": "Максимальный общий размер", - "ftbbackups.general.disable_level_saving": "Отключить сохранение уровня", - "ftbbackups.general.only_if_players_online": "Резервное копирование только если игроки находятся в сети", - "ftbbackups.general.force_on_shutdown": "Принудительное резервное копирование при завершении работы", - "ftbbackups.general.buffer_size": "Размер буфера", - "_comment": "Misc", - "ftbbackups.lang.timer": "Резервное копирование сервера в: %s", - "ftbbackups.lang.timer_progress": "[%d%% | %d / %d]", - "ftbbackups.lang.start": "Резервное копирование сервера началось!", - "ftbbackups.lang.end_1": "Резервное копирование сервера выполнено в %s!", - "ftbbackups.lang.end_2": "Резервное копирование сервера выполнено в %s! (%s)", - "ftbbackups.lang.fail": "Не удалось сохранить мир! (%s)", - "ftbbackups.lang.manual_launch": "%s запустил ручное резервное копирование!", - "ftbbackups.lang.already_running": "Резервное копирование выполняется!", - "ftbbackups.lang.size.current": "Текущий размер мира: %s", - "ftbbackups.lang.size.total": "Общий размер папки резервных копий: %s", - "ftbbackups.lang.size.available": "Доступно: %s", - "ftbbackups.lang.saving_failed": "Не удалось сохранить мир!", - "_comment": "Commands", - "commands.backup.usage": "/backup", - "commands.backup.start.usage": "/backup start [пользовательское имя]", - "commands.backup.time.usage": "/backup time", - "commands.backup.size.usage": "/backup size" -} \ No newline at end of file diff --git a/src/main/resources/assets/ftbbackups/lang/zh_cn.json b/src/main/resources/assets/ftbbackups/lang/zh_cn.json deleted file mode 100644 index a63f2505..00000000 --- a/src/main/resources/assets/ftbbackups/lang/zh_cn.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "_comment": "Config", - "ftbbackups.general.silent": "静默模式", - "ftbbackups.general.backups_to_keep": "备份保留数", - "ftbbackups.general.backup_timer": "备份间隔", - "ftbbackups.general.compression_level": "压缩等级", - "ftbbackups.general.folder": "文件夹", - "ftbbackups.general.display_file_size": "显示文件大小", - "ftbbackups.general.use_separate_thread": "使用独立线程", - "ftbbackups.general.extra_files": "额外文件", - "ftbbackups.general.max_total_size": "最大总大小", - "ftbbackups.general.disable_level_saving": "禁用等级保存", - "_comment": "Misc", - "ftbbackups.lang.timer": "服务器备份:%s", - "ftbbackups.lang.start": "服务器备份开始!", - "ftbbackups.lang.end_1": "服务器备份完成,耗时 %s!", - "ftbbackups.lang.end_2": "服务器备份完成,耗时 %s!(%s)", - "ftbbackups.lang.fail": "保存世界失败!(%s)", - "ftbbackups.lang.manual_launch": "%s 启动了一个手动备份!", - "ftbbackups.lang.already_running": "正在备份中!", - "ftbbackups.lang.size.current": "当前世界大小:%s", - "ftbbackups.lang.size.total": "总备份文件夹大小:%s", - "ftbbackups.lang.saving_failed": "世界存储失败!", - "commands.backup.start.usage": "/backup start [自定义名称]" -} \ No newline at end of file diff --git a/src/main/resources/assets/ftbbackups3/lang/en_us.json b/src/main/resources/assets/ftbbackups3/lang/en_us.json new file mode 100644 index 00000000..723f007e --- /dev/null +++ b/src/main/resources/assets/ftbbackups3/lang/en_us.json @@ -0,0 +1,69 @@ +{ + "ftbbackups3.general": "General", + "ftbbackups3.general.auto": "Automatic Backups", + "ftbbackups3.general.auto.tooltip": "Run backups automatically, based on the \"Backup Timer\" setting", + "ftbbackups3.general.silent": "Run Silently", + "ftbbackups3.general.silent.tooltip": "If true, online players are not notified when a backup runs", + "ftbbackups3.general.backups_to_keep": "Backups to Keep", + "ftbbackups3.general.backups_to_keep.tooltip": "Number of backups to keep.\nOldest backup will be deleted when a backup runs", + "ftbbackups3.general.backup_timer": "Backup Timer", + "ftbbackups3.general.backup_timer.tooltip": "Frequency, in minutes, to run backups, when \"Automatic Backups\" is true", + "ftbbackups3.general.compression_level": "Compression Level", + "ftbbackups3.general.compression_level.tooltip": "File compression level for backups\n9 = maximum compression\n1 = minimum compression\n0 = no compression at all", + "ftbbackups3.general.folder": "Backup Folder", + "ftbbackups3.general.folder.tooltip": "If not empty, an absolute path to the backup folder\nIf empty, backups are placed in the \"ftbbackups3\" folder in your game instance", + "ftbbackups3.general.display_file_size": "Display File Size", + "ftbbackups3.general.display_file_size.tooltip": "When true (and \"Run Silently\" is false), also display backed-up file size to players", + "ftbbackups3.general.extra_files": "Extra Files", + "ftbbackups3.general.extra_files.tooltip": "By default, just the files in the world folder are backed up\nThis can be used to add extra files from outside the world folder (but inside the game instance!) to the backup\nWorld folder is \"world\" on a dedicated server,\n and \"saves/\" on a single-player instance", + "ftbbackups3.general.max_total_size": "Maximum Total Size", + "ftbbackups3.general.max_total_size.tooltip": "The maximum size to allow for backups.\nOldest backups will be deleted to keep the total backup size under this amount\nValid suffixes: KB, MB, GB, TB", + "ftbbackups3.general.only_if_players_online": "Only if Players are Online", + "ftbbackups3.general.only_if_players_online.tooltip": "Automated backups are only run if at least one player is online", + "ftbbackups3.general.force_on_shutdown": "Force Backup on Shutdown", + "ftbbackups3.general.force_on_shutdown.tooltip": "If true (and \"Automatic Backups\" is true), always run a backup when the server is being stopped", + "ftbbackups3.general.archival_plugin": "Archival Plugin", + "ftbbackups3.general.archival_plugin.tooltip": "The plugin used to actually create files on disk.\nBuiltin options create ZIP files or flat file copies.", + "ftbbackups3.general.notify_admins_only": "Notify Admins Only", + "ftbbackups3.general.notify_admins_only.tooltip": "If true, only admin-level players (and SSP server owners) are notified on-screen about current backup progress\nIf false, all players are notified", + "ftbbackups3.general.add_backup_command_alias": "Add backup command alias", + "ftbbackups3.general.add_backup_command_alias.tooltip": "If true, register a /backups command as an alias to /ftbbackups3", + "ftbbackups3.general.advanced": "Advanced", + "ftbbackups3.general.advanced.buffer_size": "Buffer Size", + "ftbbackups3.general.advanced.buffer_size.tooltip": "Buffer size used when reading files to be backed up.\nDon't adjust this unless you understand the implications!", + "ftbbackups3.general.overlay.show_overlay": "Show Backup Progress Panel", + "ftbbackups3.general.overlay.overlay_inset_x": "Progress Panel X Inset", + "ftbbackups3.general.overlay.overlay_inset_y": "Progress Panel Y Inset", + "ftbbackups3.general.overlay.overlay_pos": "Progress Panel Position", + "ftbbackups3.lang.server": "Server", + "ftbbackups3.lang.timer": "Next backup in: %s", + "ftbbackups3.lang.timer.in_past": "Next backup was due: %s ago", + "ftbbackups3.lang.timer.no_players": "Backup will be run on next player login", + "ftbbackups3.lang.timer_progress": "[%d%% | %d / %d]", + "ftbbackups3.lang.start": "Server Backup started!", + "ftbbackups3.lang.end_1": "Server backup done in %s!", + "ftbbackups3.lang.end_2": "Server backup done in %s! (%s)", + "ftbbackups3.lang.fail": "Failed to save world! (%s)", + "ftbbackups3.lang.manual_launch": "%s launched a manual backup!", + "ftbbackups3.lang.already_running": "Backup in progress!", + "ftbbackups3.lang.finished": "Backup complete!", + "ftbbackups3.lang.size.current": "Current world size: %s", + "ftbbackups3.lang.size.total": "Total backups folder size: %s", + "ftbbackups3.lang.size.available": "Available: %s", + "ftbbackups3.lang.saving_failed": "World saving failed!", + "ftbbackups3.lang.autosave_status": "Autosave for [%s]: %s", + "ftbbackups3.lang.autosave.enabled": "Enabled", + "ftbbackups3.lang.autosave.disabled": "Disabled!", + "ftbbackups3.gui.confirm_restore": "Restore world '%s' from %s ?", + "ftbbackups3.gui.confirm_restore.line_2": "This cannot be undone!", + "ftbbackups3.gui.restore": "Restore World", + "ftbbackups3.gui.restore.tooltip": "Restore from a backup created by FTB Backups 3", + "ftbbackups3.gui.restore.in_progress": "Restoring world...", + "ftbbackups3.gui.restore.success": "Backup successfully restored!", + "ftbbackups3.gui.restore.failure": "Restoration failed: %s", + "ftbbackups3.gui.restore_now": "Restore", + "ftbbackups3.gui.list.file": "Filename: %s", + "ftbbackups3.gui.list.plugin": "Archival Plugin: %s", + "sidebar_button.ftbbackups3.config": "FTB Backups 3 Configuration", + "sidebar_button.ftbbackups3.config.tooltip": "Settings for the FTB Backups 3 mod" +} \ No newline at end of file diff --git a/src/main/resources/assets/ftbbackups3/sidebar_buttons/config.json b/src/main/resources/assets/ftbbackups3/sidebar_buttons/config.json new file mode 100644 index 00000000..51b42e1a --- /dev/null +++ b/src/main/resources/assets/ftbbackups3/sidebar_buttons/config.json @@ -0,0 +1,8 @@ +{ + "icon": [ + "ftbbackups3:textures/icons/ftb-backups3-icon.png" + ], + "sort_index": 700, + "click": ["command:/ftbbackups3 config"], + "environment_condition": "!FTB_BACKUPS_DISABLED" +} \ No newline at end of file diff --git a/src/main/resources/assets/ftbbackups3/textures/icons/ftb-backups3-icon.png b/src/main/resources/assets/ftbbackups3/textures/icons/ftb-backups3-icon.png new file mode 100644 index 00000000..349b373e Binary files /dev/null and b/src/main/resources/assets/ftbbackups3/textures/icons/ftb-backups3-icon.png differ diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index db13abda..5bfff908 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,6 +1,6 @@ { - "pack": { - "description": "Mod Resources", - "pack_format": 4 - } + "pack": { + "description": "Mod Resources", + "pack_format": 4 + } }