Compare commits

...

102 Commits

Author SHA1 Message Date
独孤伶俜 50d09881ff Modify the log translation to chinese language 2023-07-03 16:26:05 +08:00
独孤伶俜 9bdba6e962 Add chinese Translation for AutoConfig. 2023-07-03 16:15:33 +08:00
Szum123321 04533f89fd
Merge pull request #121 from Szum123321/validate_backups_research_1
Validate backups research 1
2023-06-08 23:04:54 +02:00
Szum123321 3e1fe9f655 forgot to remove debug, 1.20 final toolchain update 2023-06-08 23:02:19 +02:00
Szum123321 d32b3e8f0c repaired if condition causing crashes on windows 2023-06-08 22:57:18 +02:00
Szum123321 8d3dd3f012 added filename to Exceptions thrown by HashingInputStream 2023-06-08 22:56:22 +02:00
Szum123321 c1bd31aec5 repaired windows crash 2023-06-08 22:23:00 +02:00
Szum123321 955fbd0f83
Merge pull request #120 from Szum123321/validate_backups_research_1
Validate backups research 1
2023-06-04 11:56:20 +02:00
Szum123321 4b4d42cda6 final prod version fo 1.20 2023-06-04 11:54:36 +02:00
Szum123321 bb34166ba3 unretireing bzip lzma 2023-05-12 18:52:52 +02:00
Szum123321 1e70d605f1 removed unused imports 2023-05-12 18:48:58 +02:00
Szum123321 be20e2b17a some cleanup 2023-05-12 18:47:54 +02:00
Szum123321 41934c32cf FileTreeHashBuilder::update now also takes the number of bytes as an argument. I/OHashingStreams count no. of bytes written. 2023-05-12 18:36:39 +02:00
Szum123321 b9af3b3777 YYYYEEEEEEEEEEEEEEEEEEe I finally debugged the hash!!!!!
The problem was due to an error in usage o FileTreeHashBuilder during unpacking.
2023-05-10 23:43:55 +02:00
Szum123321 fe9f9f3e0d 1.20 snapshot update 2023-05-10 08:56:37 +02:00
Szum123321 6782c4fe5f Merged BackuContext, MakeBackupRunnable, MakeBackupRunnableFactory into ExecutableBackup 2023-05-10 08:55:54 +02:00
Szum123321 27d6d68e97 moved catch incrementing into hash builder 2023-03-09 21:46:25 +01:00
Szum123321 9ffaff1a2d formatting 2023-03-09 21:18:56 +01:00
Szum123321 cf3078259b bugfixes 2022-12-27 15:35:03 +01:00
Szum123321 7d31e6710c repaired hash & added tests 2022-12-27 13:31:23 +01:00
Szum123321 97c607f9b2 formatting&spelling 2022-12-20 00:48:20 +01:00
Szum123321 ed9c9a16fc moved name hashing to streams 2022-12-20 00:47:57 +01:00
Szum123321 9b3b908d0a removed redundant counter 2022-12-20 00:46:46 +01:00
Szum123321 663089a7a3 improved naming 2022-12-20 00:46:03 +01:00
Szum123321 cffc659001 don't re-pack Textile data 2022-12-01 07:45:57 +01:00
Szum123321 3af7d75042 changed built fle names and moved errorErrorHandlingMode to the end of config, so it appears as far down as possible 2022-11-29 22:43:16 +01:00
Szum123321 458ab01822 potential bug repair 2022-11-29 22:35:15 +01:00
Szum123321 c2048c5039 ehhhhhhhh 2022-11-29 22:35:06 +01:00
Szum123321 19cfb3cb27 finalized verification code 2022-11-29 22:34:47 +01:00
Szum123321 5f1706eed3 added game&mod version to the status file 2022-11-29 22:34:02 +01:00
Szum123321 472aeda184 improved comments and formatting 2022-11-29 22:31:04 +01:00
Szum123321 8385044154 typos 2022-11-29 15:06:30 +01:00
Szum123321 4622f3fd0d all works now*. more debugging still needed. 2022-11-29 15:05:51 +01:00
Szum123321 86ae95b02e Added mod version info to metadata 2022-11-29 00:55:41 +01:00
Szum123321 2efe112157 Repaired race condition with ParallelZip. Hashing now works! 2022-11-29 00:55:18 +01:00
Szum123321 5367a00cdc individual file hashing works. filetree still fails 2022-11-28 19:43:30 +01:00
Szum123321 2053df7311 debugging in progress... 2022-11-28 18:53:55 +01:00
Szum123321 993d6359ad typos 2022-11-27 23:20:05 +01:00
Szum123321 7734c16e06 those didnt work 2022-11-27 23:19:37 +01:00
Szum123321 febbb95b97 A bit of cleanup of BalticHash. Added wip simd version 2022-11-27 23:12:25 +01:00
Szum123321 300fe18b10 Added support of virtual file (in this case textile_status.data). 2022-11-27 23:11:41 +01:00
Szum123321 d871fc14cb moved hash-related stuff core.digest package. Renamed XorSeaHash to BalticHash :). 2022-11-27 23:09:21 +01:00
Szum123321 2f96f7728e Updated all copyright headers 2022-11-27 23:04:21 +01:00
Szum123321 c040d05bd8 Added toString method to CompressionStatus, version bump to 3.0.0-a, few small tweaks 2022-11-27 13:58:45 +01:00
Szum123321 afe9c8d051 Removed redundant BufferStreams and LivingServer subsystem 2022-11-27 13:57:02 +01:00
Szum123321 f6cd361fff Selected hashing algorithm. It's a custom job which merges SeaHash with Xoroshift64*. Should be fast and correct enough to for this use case. hope I will be able to speed it up with SIMD, as java is scheduled to soon include Vector API (a part of project Panama) 2022-11-27 13:55:34 +01:00
Szum123321 9c37affacd Backup integrity is now checked 2022-11-25 14:00:18 +01:00
Szum123321 dbb9a71749 Moved around error handling. LocalDateTime is now passed with BackupContext. Replaced equals method with matches in ParallelZipCompressor.SimpleStackTraceElement to avert warning. 2022-11-25 09:54:44 +01:00
Szum123321 4007d8f86d Do I really need hashes? idk.... Either way it has to get ugly before it gets beautiful ... I think 2022-11-24 00:38:55 +01:00
Szum123321 85452efbca , 2022-11-22 15:09:21 +01:00
Szum123321 c816c70a6b added FileTreeHashBuilder for intelligently building single hash of file tree 2022-11-22 14:16:12 +01:00
Szum123321 2774ebd2b4 starting work on backup verification 2022-11-21 23:31:48 +01:00
Szum123321 ef4c69b4d1 ffffffffffffffffffffff 2022-11-06 11:46:40 +01:00
Szum123321 64e8e06161
Merge pull request #111 from Szum123321/refactor_2022
Refactor 2022
2022-11-06 11:05:22 +01:00
Szum123321 aaf9a54523 dep update + version bump + added testmod 2022-11-06 11:02:02 +01:00
Szum123321 8427eebfcc Added 'latest' keyword to restore (#85)
RestoreHelper::getAvailableBackups now returns sorted LinkedList
2022-11-06 11:00:14 +01:00
Szum123321 2f11548fef bugfix + typos 2022-11-06 10:59:03 +01:00
Szum123321 015184a232 added dev notes 2022-11-06 10:56:27 +01:00
Szum123321 14e82639a8 further dev environment shenanigans 2022-11-05 13:33:49 +01:00
Szum123321 3f2658ed96 Cleanup is now implements Callable 2022-11-05 13:31:44 +01:00
Szum123321 fe25b1eec5 typos 2022-11-05 13:30:45 +01:00
Szum123321 53a5639373 repaired bad error handling 2022-10-04 18:56:48 +02:00
Szum123321 dc974aa35b improved comment quality 2022-10-04 18:56:08 +02:00
Szum123321 13a114baa6 gradlew update 2022-09-04 10:55:06 +02:00
Szum123321 4cbe18f318 removed deprecated code 2022-09-04 10:49:49 +02:00
Szum123321 dc24d51674 stream is closed 2022-09-04 10:49:08 +02:00
Szum123321 8b7dbdc8e8 added better catch and changed default executorService to null 2022-09-02 21:05:27 +02:00
Szum123321 2bde644c76 loom update 2022-08-31 23:00:48 +02:00
Szum123321 b908e651a1 Forgot to commit 2022-08-31 23:00:24 +02:00
Szum123321 81c5cd04cb Commented some more code.
BackupScheduler is now static
2022-08-31 22:53:30 +02:00
Szum123321 6707e813b2 explicit presence declaration 2022-08-31 22:50:48 +02:00
Szum123321 91447b7b3c This shouldn't be here... 2022-08-31 22:42:39 +02:00
Szum123321 8cede17568 because of that return, the finally block would never run 2022-08-31 22:36:50 +02:00
Szum123321 c7fd6d3f8e Did I forget the sort? 2022-08-31 22:32:56 +02:00
Szum123321 c22eb7a3b2 made cleanup more readable 2022-08-30 14:46:15 +02:00
Szum123321 21cf46a56a Moved all filename paring into its own class 2022-08-30 13:28:30 +02:00
Szum123321 b3a340deab Separated BackupHelper into the factory and the cleanup (MakeBackupRunnableFactory & Cleanup) 2022-08-30 01:31:43 +02:00
Szum123321 b7da7dbc6f Replaced Statics with Globals - now a singleton
Moved updateTMPFSFlag from Utilities to Statics/Globals

Added proper shutdown procedure for executorService (shutdownQueueExecutor) - should repair #37

Refactored some commands
2022-08-30 01:15:56 +02:00
Szum123321 03743dbdc9 1.19 update 2022-08-25 21:50:57 +02:00
Szum123321 2f8857f630
Update README.md
Typo
2022-07-02 16:47:16 +02:00
Szum123321 e063c26d16
Delete README_zh-CN.md 2022-06-27 21:51:04 +02:00
Szum123321 cb72067275
Update README.md 2022-06-23 23:39:01 +02:00
Szum123321 1e60c05477
Update README.md 2022-06-23 23:31:30 +02:00
Szum123321 4fa30024cf
Update build.gradle
NIO methods are only available in Apache 1.21
2022-06-22 23:02:46 +02:00
Szum123321 a9befa2301 Repaired BackupHelper 2022-06-22 21:32:20 +02:00
Szum123321 267776789d
Merge pull request #102 from Szum123321/safe_restore
Safe restore
2022-06-22 20:04:25 +02:00
Szum123321 27aa40027c dropped unnecessary accessors in BackupContext (record) 2022-06-22 20:02:37 +02:00
Szum123321 fd02df7022 Cleaner cleanup algorithm 2022-06-18 08:30:36 +02:00
Szum123321 3f1f1704b1 Added safe restore 2022-06-17 23:14:14 +02:00
Szum123321 b4597f6f1f pt III, slowly moving to the new file handling api (File -> Path) 2022-06-17 22:43:41 +02:00
Szum123321 a85db99d82 pt II, slowly moving to the new file handling api (File -> Path) 2022-06-17 21:33:13 +02:00
Szum123321 6897b94afc slowly moving to the new file handling api (File -> Path) 2022-06-17 21:15:50 +02:00
Szum123321 a4bf645ef7 final dependency update. rolled back release version 2022-06-16 21:40:05 +02:00
Szum123321 04fec113d9
Merge pull request #98 from IzzyBizzy45/2.x
1.19 update
TMP filesystem read-only fix
2022-06-12 22:02:29 +02:00
IzzyBizzy f06cdb6e3c "TMP filesystem ({}) is read-only!" Fix
TMP filesystem ({}) is read-only! was showing up when running on windows environment. May be wrong in removing the .resolve part, but found that Java was trying to locate a file that wasn't being created when the server/client was ran, resulting in the check failing. Removing the resolve still seems to pickup if a directory is read only so should be good still (hopefully lol)
2022-06-10 19:13:29 +10:00
IzzyBizzy d0ffab9339 1.19 Changes 2022-06-10 14:08:46 +10:00
Szum123321 18b7c2b9ae
Merge pull request #82 from LilyRose2798/replace-reserved-characters
Add replacment of reserved filename chars from comment
2022-01-09 15:32:35 +01:00
Lily Rose c4549e8f0a
Add replacment of reserved filename chars from comment 2022-01-09 03:12:46 +11:00
Szum123321 93f2b31e2a Merge remote-tracking branch 'origin/2.x' into 2.x 2021-12-23 20:59:06 +01:00
Szum123321 bbb07451bd #80 test 2021-12-23 20:58:55 +01:00
Szum123321 4d881fe940 Update 1.18.1 2021-12-23 20:58:31 +01:00
Szum123321 756a3152ac
Update README.md 2021-12-19 21:42:56 +01:00
68 changed files with 2425 additions and 1539 deletions

3
DEV_NOTES Normal file
View File

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

View File

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

View File

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

View File

@ -1,28 +1,19 @@
plugins {
id 'fabric-loom' version '0.10-SNAPSHOT'
id 'fabric-loom' version '1.2-SNAPSHOT'
id 'maven-publish'
}
sourceCompatibility = JavaVersion.VERSION_16
targetCompatibility = JavaVersion.VERSION_16
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
archivesBaseName = project.archives_base_name
version = "${project.mod_version}-${getMcMinor(project.minecraft_version)}"
group = project.maven_group
minecraft {
}
repositories{
maven { url 'https://server.bbkr.space/artifactory/libs-release' }
repositories {
maven { url 'https://jitpack.io' }
maven { url "https://maven.shedaniel.me/" }
maven {
url "https://maven.terraformersmc.com/releases/"
content {
includeGroup "com.terraformersmc"
}
}
maven { url "https://maven.terraformersmc.com/releases/" }
mavenCentral()
}
@ -44,21 +35,20 @@ dependencies {
modImplementation("com.terraformersmc:modmenu:${project.modmenu_version}")
//General compression library
modImplementation "org.apache.commons:commons-compress:1.19"
include "org.apache.commons:commons-compress:1.19"
implementation "org.apache.commons:commons-compress:1.22"
include "org.apache.commons:commons-compress:1.22"
//LZMA support
modImplementation 'org.tukaani:xz:1.9'
implementation 'org.tukaani:xz:1.9'
include "org.tukaani:xz:1.9"
//Gzip compression, parallel, GITHUB
modImplementation "com.github.shevek:parallelgzip:${project.pgzip_commit_hash}"
implementation "com.github.shevek:parallelgzip:${project.pgzip_commit_hash}"
include "com.github.shevek:parallelgzip:${project.pgzip_commit_hash}"
// Lazy DFU makes the dev env start up much faster by loading DataFixerUpper lazily, which would otherwise take a long time. We rarely need it anyway.
modRuntime("com.github.astei:lazydfu:${project.lazydfu_version}") {
exclude(module: "fabric-loader")
}
// I couldn't get this working in my environment - IzzyBizzy
//modLocalRuntime("com.github.astei:lazydfu:${project.lazydfu_version}")
}
processResources {
@ -115,9 +105,9 @@ publishing {
}
static def getMcMinor(ver) {
String[] arr = ((String)ver).split("\\.");
String[] arr = ((String)ver).split("[.-]")
if(arr.length < 2) return ver;
if(arr.length < 2) return ver
return (String)(arr[0] + "." + arr[1]);
return (String)(arr[0] + "." + arr[1])
}

View File

@ -1,25 +1,25 @@
# Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G
minecraft_version=1.18
yarn_mappings=1.18+build.1
loader_version=0.12.6
minecraft_version=1.20
yarn_mappings=1.20+build.1
loader_version=0.14.21
#Fabric api
fabric_version=0.43.1+1.18
fabric_version=0.83.0+1.20
#Cloth Config
cloth_version=6.0.42
cloth_version=11.0.98
#ModMenu
modmenu_version=3.0.0
modmenu_version=7.0.0-beta.2
#Lazy DFU for faster dev start
lazydfu_version=0.1.2
databreaker_version=0.2.10
#Hash of commit form which parallel gzip will be build
pgzip_commit_hash=af5f5c297e735f3f2df7aa4eb0e19a5810b8aff6
# Mod Properties
mod_version = 2.3.0
mod_version = 3.1.0
maven_group = net.szum123321
archives_base_name = textile_backup

Binary file not shown.

View File

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

270
gradlew vendored
View File

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

25
gradlew.bat vendored
View File

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

View File

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

View File

@ -0,0 +1,146 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup;
import net.minecraft.server.MinecraftServer;
import net.szum123321.textile_backup.core.digest.BalticHash;
import net.szum123321.textile_backup.core.digest.Hash;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.restore.AwaitThread;
import org.apache.commons.io.FileUtils;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.zip.CRC32;
public class Globals {
public static final Globals INSTANCE = new Globals();
private static final TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static final DateTimeFormatter defaultDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss");
public static final Supplier<Hash> CHECKSUM_SUPPLIER = BalticHash::new;/*() -> new Hash() {
private final CRC32 crc = new CRC32();
@Override
public void update ( int b){
crc.update(b);
}
@Override
public void update ( long b) {
ByteBuffer v = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
v.putLong(b);
crc.update(v.array());
}
@Override
public void update ( byte[] b, int off, int len){
crc.update(b, off, len);
}
@Override
public long getValue () {
return crc.getValue();
}
};*/
private ExecutorService executorService = null;//TODO: AAAAAAAAAAAAAAA MEMORY LEAK!!!!!!!!!
public final AtomicBoolean globalShutdownBackupFlag = new AtomicBoolean(true);
public boolean disableWatchdog = false;
private boolean disableTMPFiles = false;
private AwaitThread restoreAwaitThread = null;
private Path lockedPath = null;
private String combinedVersionString;
private Globals() {}
public ExecutorService getQueueExecutor() { return executorService; }
public void resetQueueExecutor() {
if(Objects.nonNull(executorService) && !executorService.isShutdown()) return;
executorService = Executors.newSingleThreadExecutor();
}
public void shutdownQueueExecutor(long timeout) {
if(executorService.isShutdown()) return;
executorService.shutdown();
try {
if(!executorService.awaitTermination(timeout, TimeUnit.MICROSECONDS)) {
log.error("在等待当前运行的备份完成时发生了超时!");
executorService.shutdownNow().stream()
// .filter(r -> r instanceof ExecutableBackup)
// .map(r -> (ExecutableBackup)r)
.forEach(r -> log.error("Dropping: {}", r.toString()));
if(!executorService.awaitTermination(1000, TimeUnit.MICROSECONDS))
log.error("无法关闭执行器!");
}
} catch (InterruptedException e) {
log.error("发生了一个异常!(灾难性的错误)", e);
}
}
public Optional<AwaitThread> getAwaitThread() { return Optional.ofNullable(restoreAwaitThread); }
public void setAwaitThread(AwaitThread th) { restoreAwaitThread = th; }
public Optional<Path> getLockedFile() { return Optional.ofNullable(lockedPath); }
public void setLockedFile(Path p) { lockedPath = p; }
public synchronized boolean disableTMPFS() { return disableTMPFiles; }
public synchronized void updateTMPFSFlag(MinecraftServer server) {
disableTMPFiles = false;
Path tmp_dir = Path.of(System.getProperty("java.io.tmpdir"));
if(
FileUtils.sizeOfDirectory(Utilities.getWorldFolder(server).toFile()) >=
tmp_dir.toFile().getUsableSpace()
) {
log.error("TMP(临时)文件目录没有可用空间 ({})", tmp_dir);
disableTMPFiles = true;
}
if(!Files.isWritable(tmp_dir)) {
log.error("TMP(临时)文件目录({})是只读的", tmp_dir);
disableTMPFiles = true;
}
if(disableTMPFiles) log.error("Might cause: https://github.com/Szum123321/textile_backup/wiki/ZIP-Problems");
}
public String getCombinedVersionString() {
return combinedVersionString;
}
public void setCombinedVersionString(String combinedVersionString) {
this.combinedVersionString = combinedVersionString;
}
}

View File

@ -1,40 +0,0 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2020 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup;
import net.szum123321.textile_backup.core.restore.AwaitThread;
import java.io.File;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
public class Statics {
public final static DateTimeFormatter defaultDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss");
public static ExecutorService executorService = Executors.newSingleThreadExecutor();
public static final AtomicBoolean globalShutdownBackupFlag = new AtomicBoolean(true);
public static boolean disableWatchdog = false;
public static AwaitThread restoreAwaitThread = null;
public static Optional<File> untouchableFile = Optional.empty();
public static boolean disableTMPFiles = false;
}

View File

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

View File

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

View File

@ -1,3 +1,21 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.client;
import com.terraformersmc.modmenu.api.ConfigScreenFactory;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2020 Szum123321
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -23,6 +23,7 @@ import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.RestoreableFile;
import net.szum123321.textile_backup.core.restore.RestoreHelper;
import java.util.*;
@ -33,17 +34,17 @@ public class ListBackupsCommand {
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("list")
.executes(ctx -> { StringBuilder builder = new StringBuilder();
List<RestoreHelper.RestoreableFile> backups = RestoreHelper.getAvailableBackups(ctx.getSource().getServer());
var backups = RestoreHelper.getAvailableBackups(ctx.getSource().getServer());
if(backups.size() == 0) {
builder.append("There a no backups available for this world.");
builder.append("该世界没有可用的备份文件. ");
} else if(backups.size() == 1) {
builder.append("There is only one backup available: ");
builder.append("只有一个可用的备份文件: ");
builder.append(backups.get(0).toString());
} else {
backups.sort(null);
Iterator<RestoreHelper.RestoreableFile> iterator = backups.iterator();
builder.append("Available backups:\n");
Iterator<RestoreableFile> iterator = backups.iterator();
builder.append("可用的备份文件:\n");
builder.append(iterator.next());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2020 Szum123321
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -18,14 +18,15 @@
package net.szum123321.textile_backup.core;
/**
* Enum representing possible sources of action
*/
public enum ActionInitiator {
Player("Player", "by"),
ServerConsole("Server Console", "from"),
Timer("Timer", "by"),
ServerConsole("Server Console", "from"), //some/ting typed a command and it was not a player (command blocks and server console count)
Timer("Timer", "by"), //a.k.a scheduler
Shutdown("Server Shutdown", "by"),
Restore("Backup Restoration", "because of"),
Null("Null (That shouldn't have happened)", "form");
Restore("Backup Restoration", "because of");
private final String name;
private final String prefix;

View File

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

View File

@ -0,0 +1,100 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core;
import net.szum123321.textile_backup.core.restore.RestoreContext;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Optional;
public record CompressionStatus(long treeHash, Map<String, Exception> brokenFiles, LocalDateTime date, long startTimestamp, long finishTimestamp, String version) implements Serializable {
public static final String DATA_FILENAME = "textile_status.data";
public Optional<String> validate(long hash, RestoreContext ctx) throws RuntimeException {
if(hash != treeHash)
return Optional.of("Tree Hash mismatch!\n Expected: " + hex(treeHash) + ", got: " + hex(hash));
if(!brokenFiles.isEmpty()) return Optional.of("Damaged files present! ^");
if(ctx.restoreableFile().getCreationTime().equals(date))
return Optional.of(
"Creation date mismatch!\n Expected: " +
date.format(DateTimeFormatter.ISO_DATE_TIME) + ", got: " +
ctx.restoreableFile().getCreationTime().format(DateTimeFormatter.ISO_DATE_TIME)
);
return Optional.empty();
}
public static Path resolveStatusFilename(Path directory) { return directory.resolve(DATA_FILENAME); }
public static CompressionStatus readFromFile(Path directory) throws IOException, ClassNotFoundException {
try(InputStream i = Files.newInputStream(directory.resolve(DATA_FILENAME));
ObjectInputStream obj = new ObjectInputStream(i)) {
return (CompressionStatus) obj.readObject();
}
}
public byte[] serialize() throws IOException {
try (ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream o = new ObjectOutputStream(bo)) {
o.writeObject(this);
return bo.toByteArray();
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{ ");
builder.append("Hash: ")
.append(hex(treeHash))
.append(", Date: ")
.append(date.format(DateTimeFormatter.ISO_DATE_TIME))
.append(", Start timestamp: ").append(startTimestamp)
.append(", Finish timestamp: ").append(finishTimestamp)
.append(", Mod Version: ").append(version);
builder.append(", Broken files: ");
if(brokenFiles.isEmpty()) builder.append("[]");
else {
builder.append("[\n");
for(String i: brokenFiles.keySet()) {
builder.append(i)
.append(":");
ByteArrayOutputStream o = new ByteArrayOutputStream();
brokenFiles.get(i).printStackTrace(new PrintStream(o));
builder.append(o).append("\n");
}
builder.append("]");
}
builder.append(" }");
return builder.toString();
}
private static String hex(long val) { return "0x" + Long.toHexString(val).toUpperCase(); }
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,133 +0,0 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2020 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.create;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.util.Util;
import net.szum123321.textile_backup.core.ActionInitiator;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public record BackupContext(@NotNull MinecraftServer server,
ServerCommandSource commandSource,
ActionInitiator initiator,
boolean save,
String comment) {
public MinecraftServer getServer() {
return server;
}
public ServerCommandSource getCommandSource() {
return commandSource;
}
public ActionInitiator getInitiator() {
return initiator;
}
public boolean startedByPlayer() {
return initiator == ActionInitiator.Player;
}
public boolean shouldSave() {
return save;
}
public String getComment() {
return comment;
}
public UUID getInitiatorUUID() {
return initiator.equals(ActionInitiator.Player) && commandSource.getEntity() != null ? commandSource.getEntity().getUuid(): Util.NIL_UUID;
}
public static class Builder {
private MinecraftServer server;
private ServerCommandSource commandSource;
private ActionInitiator initiator;
private boolean save;
private String comment;
private boolean guessInitiator;
public Builder() {
this.server = null;
this.commandSource = null;
this.initiator = null;
this.save = false;
this.comment = null;
guessInitiator = false;
}
public static Builder newBackupContextBuilder() {
return new Builder();
}
public Builder setCommandSource(ServerCommandSource commandSource) {
this.commandSource = commandSource;
return this;
}
public Builder setServer(MinecraftServer server) {
this.server = server;
return this;
}
public Builder setInitiator(ActionInitiator initiator) {
this.initiator = initiator;
return this;
}
public Builder setComment(String comment) {
this.comment = comment;
return this;
}
public Builder guessInitiator() {
this.guessInitiator = true;
return this;
}
public Builder saveServer() {
this.save = true;
return this;
}
public BackupContext build() {
if (guessInitiator) {
initiator = commandSource.getEntity() instanceof PlayerEntity ? ActionInitiator.Player : ActionInitiator.ServerConsole;
} else if (initiator == null) {
initiator = ActionInitiator.Null;
}
if (server == null) {
if (commandSource != null) setServer(commandSource.getServer());
else
throw new RuntimeException("Neither MinecraftServer or ServerCommandSource were provided!");
}
return new BackupContext(server, commandSource, initiator, save, comment);
}
}
}

View File

@ -1,129 +0,0 @@
/*
A simple backup mod for Fabric
Copyright (C) 2020 Szum123321
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.create;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.Utilities;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Comparator;
public class BackupHelper {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public static Runnable create(BackupContext ctx) {
if(config.get().broadcastBackupStart) {
Utilities.notifyPlayers(ctx.getServer(),
ctx.getInitiatorUUID(),
"Warning! Server backup will begin shortly. You may experience some lag."
);
} else {
log.sendInfoAL(ctx, "Warning! Server backup will begin shortly. You may experience some lag.");
}
StringBuilder builder = new StringBuilder();
builder.append("Backup started ");
builder.append(ctx.getInitiator().getPrefix());
if(ctx.startedByPlayer())
builder.append(ctx.getCommandSource().getDisplayName().getString());
else
builder.append(ctx.getInitiator().getName());
builder.append(" on: ");
builder.append(Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
log.info(builder.toString());
if (ctx.shouldSave()) {
log.sendInfoAL(ctx, "Saving server...");
ctx.getServer().getPlayerManager().saveAllPlayerData();
try {
ctx.getServer().save(false, true, true);
} catch (Exception e) {
log.sendErrorAL(ctx,"An exception occurred when trying to save the world!");
}
}
return new MakeBackupRunnable(ctx);
}
public static int executeFileLimit(ServerCommandSource ctx, String worldName) {
File root = Utilities.getBackupRootPath(worldName);
int deletedFiles = 0;
if (root.isDirectory() && root.exists() && root.listFiles() != null) {
if (config.get().maxAge > 0) { // delete files older that configured
final LocalDateTime now = LocalDateTime.now();
deletedFiles += Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)// We check if we can get file's creation date so that the next line won't throw an exception
.filter(f -> now.toEpochSecond(ZoneOffset.UTC) - Utilities.getFileCreationTime(f).get().toEpochSecond(ZoneOffset.UTC) > config.get().maxAge)
.map(f -> deleteFile(f, ctx))
.filter(b -> b).count(); //a bit awkward
}
if (config.get().backupsToKeep > 0 && root.listFiles().length > config.get().backupsToKeep) {
deletedFiles += Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime((File) f).get()).reversed())
.skip(config.get().backupsToKeep)
.map(f -> deleteFile(f, ctx))
.filter(b -> b).count();
}
if (config.get().maxSize > 0 && FileUtils.sizeOfDirectory(root) / 1024 > config.get().maxSize) {
deletedFiles += Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get()))
.takeWhile(f -> FileUtils.sizeOfDirectory(root) / 1024 > config.get().maxSize)
.map(f -> deleteFile(f, ctx))
.filter(b -> b).count();
}
}
return deletedFiles;
}
private static boolean deleteFile(File f, ServerCommandSource ctx) {
if(Statics.untouchableFile.isEmpty()|| !Statics.untouchableFile.get().equals(f)) {
if(f.delete()) {
log.sendInfoAL(ctx, "Deleting: {}", f.getName());
return true;
} else {
log.sendErrorAL(ctx, "Something went wrong while deleting: {}.", f.getName());
}
}
return false;
}
}

View File

@ -1,6 +1,6 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2020 Szum123321
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -19,62 +19,73 @@
package net.szum123321.textile_backup.core.create;
import net.minecraft.server.MinecraftServer;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.ActionInitiator;
import java.time.Instant;
/**
* Runs backup on a preset interval
* <br><br>
* The important thing to note: <br>
* The decision of whether to do a backup or not is made at the time of scheduling, that is, whenever the <code>nextBackup</code>
* flag is set. This means that even if doBackupsOnEmptyServer=false, the backup that was scheduled with players online will
* still go through. <br>
* It might appear as though there has been made a backup with no players online despite the config. This is the expected behaviour
* <br><br>
* Furthermore, it uses system time
*/
public class BackupScheduler {
private final static ConfigHelper config = ConfigHelper.INSTANCE;
private boolean scheduled;
private long nextBackup;
//Scheduled flag tells whether we have decided to run another backup
private static boolean scheduled = false;
private static long nextBackup = - 1;
public BackupScheduler() {
scheduled = false;
nextBackup = -1;
}
public void tick(MinecraftServer server) {
public static void tick(MinecraftServer server) {
if(config.get().backupInterval < 1) return;
long now = Instant.now().getEpochSecond();
if(config.get().doBackupsOnEmptyServer || server.getPlayerManager().getCurrentPlayerCount() > 0) {
//Either just run backup with no one playing or there's at least one player
if(scheduled) {
if(nextBackup <= now) {
Statics.executorService.submit(
BackupHelper.create(
BackupContext.Builder
.newBackupContextBuilder()
.setServer(server)
.setInitiator(ActionInitiator.Timer)
.saveServer()
.build()
)
//It's time to run
Globals.INSTANCE.getQueueExecutor().submit(
ExecutableBackup.Builder
.newBackupContextBuilder()
.setServer(server)
.setInitiator(ActionInitiator.Timer)
.saveServer()
.announce()
.build()
);
nextBackup = now + config.get().backupInterval;
}
} else {
//Either server just started or a new player joined after the last backup has finished
//So let's schedule one some time from now
nextBackup = now + config.get().backupInterval;
scheduled = true;
}
} else if(!config.get().doBackupsOnEmptyServer && server.getPlayerManager().getCurrentPlayerCount() == 0) {
//Do the final backup. No one's on-line and doBackupsOnEmptyServer == false
if(scheduled && nextBackup <= now) {
Statics.executorService.submit(
BackupHelper.create(
BackupContext.Builder
.newBackupContextBuilder()
.setServer(server)
.setInitiator(ActionInitiator.Timer)
.saveServer()
.build()
)
//Verify we hadn't done the final one, and it's time to do so
Globals.INSTANCE.getQueueExecutor().submit(
ExecutableBackup.Builder
.newBackupContextBuilder()
.setServer(server)
.setInitiator(ActionInitiator.Timer)
.saveServer()
.announce()
.build()
);
scheduled = false;
}
}
}
}
}

View File

@ -0,0 +1,34 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.create;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
public class BrokenFileHandler {
private final Map<String, Exception> store = new HashMap<>();
public void handle(Path file, Exception e) { store.put(file.toString(), e); }
public boolean valid() { return store.isEmpty(); }
public Map<String, Exception> get() {
return store;
}
}

View File

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

View File

@ -0,0 +1,70 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.create;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.digest.FileTreeHashBuilder;
import net.szum123321.textile_backup.core.digest.HashingInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
public record FileInputStreamSupplier(Path path, String name, FileTreeHashBuilder hashTreeBuilder, BrokenFileHandler brokenFileHandler) implements InputSupplier {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
@Override
public InputStream getInputStream() throws IOException {
try {
return new HashingInputStream(Files.newInputStream(path), path, hashTreeBuilder, brokenFileHandler);
} catch (IOException e) {
//Probably good idea to just put it here. In the case an exception is thrown here, it could be possible
//The latch would have never been lifted
hashTreeBuilder.update(path, 0, 0);
brokenFileHandler.handle(path, e);
throw e;
}
}
@Override
public Optional<Path> getPath() { return Optional.of(path); }
@Override
public long size() throws IOException { return Files.size(path); }
@Override
public String getName() {
return name;
}
@Override
public InputStream get() {
try {
return getInputStream();
} catch (IOException e) {
log.error("尝试从文件{}创建输入流时发生了异常!", path.toString(), e);
}
return null;
}
}

View File

@ -0,0 +1,35 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.create;
import org.apache.commons.compress.parallel.InputStreamSupplier;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Optional;
public interface InputSupplier extends InputStreamSupplier {
InputStream getInputStream() throws IOException;
//If an entry is virtual (a.k.a. there is no actual file to open, only input stream)
Optional<Path> getPath();
String getName();
long size() throws IOException;
}

View File

@ -1,140 +0,0 @@
/*
A simple backup mod for Fabric
Copyright (C) 2020 Szum123321
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.create;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.create.compressors.*;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.create.compressors.tar.AbstractTarArchiver;
import net.szum123321.textile_backup.core.create.compressors.tar.ParallelBZip2Compressor;
import net.szum123321.textile_backup.core.create.compressors.tar.ParallelGzipCompressor;
import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.time.LocalDateTime;
public class MakeBackupRunnable implements Runnable {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
private final BackupContext context;
public MakeBackupRunnable(BackupContext context){
this.context = context;
}
@Override
public void run() {
try {
Utilities.disableWorldSaving(context.getServer());
Statics.disableWatchdog = true;
Utilities.updateTMPFSFlag(context.getServer());
log.sendInfoAL(context, "Starting backup");
File world = Utilities.getWorldFolder(context.getServer());
log.trace("Minecraft world is: {}", world);
File outFile = Utilities
.getBackupRootPath(Utilities.getLevelName(context.getServer()))
.toPath()
.resolve(getFileName())
.toFile();
log.trace("Outfile is: {}", outFile);
outFile.getParentFile().mkdirs();
try {
outFile.createNewFile();
} catch (IOException e) {
log.error("An exception occurred when trying to create new backup file!", e);
if(context.getInitiator() == ActionInitiator.Player)
log.sendError(context, "An exception occurred when trying to create new backup file!");
return;
}
int coreCount;
if(config.get().compressionCoreCountLimit <= 0) {
coreCount = Runtime.getRuntime().availableProcessors();
} else {
coreCount = Math.min(config.get().compressionCoreCountLimit, Runtime.getRuntime().availableProcessors());
}
log.trace("Running compression on {} threads. Available cores: {}", coreCount, Runtime.getRuntime().availableProcessors());
switch (config.get().format) {
case ZIP -> {
if (coreCount > 1 && !Statics.disableTMPFiles)
ParallelZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
else
ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
}
case BZIP2 -> ParallelBZip2Compressor.getInstance().createArchive(world, outFile, context, coreCount);
case GZIP -> ParallelGzipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
case LZMA -> new AbstractTarArchiver() {
protected OutputStream getCompressorOutputStream(OutputStream stream, BackupContext ctx, int coreLimit) throws IOException {
return new LZMACompressorOutputStream(stream);
}
}.createArchive(world, outFile, context, coreCount);
case TAR -> new AbstractTarArchiver().createArchive(world, outFile, context, coreCount);
default -> {
log.warn("Specified compressor ({}) is not supported! Zip will be used instead!", config.get().format);
if (context.getInitiator() == ActionInitiator.Player)
log.sendError(context.getCommandSource(), "Error! No correct compression format specified! Using default compressor!");
ZipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
}
}
BackupHelper.executeFileLimit(context.getCommandSource(), Utilities.getLevelName(context.getServer()));
if(config.get().broadcastBackupDone) {
Utilities.notifyPlayers(
context.getServer(),
context.getInitiatorUUID(),
"Done!"
);
} else {
log.sendInfoAL(context, "Done!");
}
} finally {
Utilities.enableWorldSaving(context.getServer());
Statics.disableWatchdog = false;
}
}
private String getFileName(){
LocalDateTime now = LocalDateTime.now();
return Utilities.getDateTimeFormatter().format(now) +
(context.getComment() != null ? "#" + context.getComment().replace("#", "") : "") +
config.get().format.getCompleteString();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,100 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.digest;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
/**
* This algorithm copies the construction of <a href="https://ticki.github.io/blog/seahash-explained/">SeaHash</a> including its IV.
* What it differs in is that it uses Xoroshift64* instead of PCG as its pseudo-random function. Although it might lower
* the output quality, I don't think it matters that much, honestly. One advantage of xoroshift is that it should be
* easier to implement with AVX. Java should soon ship its vector api by default.
*/
public class BalticHash implements Hash {
//SeaHash IV
protected final static long[] IV = { 0x16f11fe89b0d677cL, 0xb480a793d8e6c86cL, 0x6fe2e5aaf078ebc9L, 0x14f994a4c5259381L };
private final long[] state = Arrays.copyOf(IV, IV.length);
protected final int buffer_limit = state.length * Long.BYTES;
protected final byte[] _byte_buffer = new byte[(state.length + 1) * Long.BYTES];
//Enforce endianness
protected final ByteBuffer buffer = ByteBuffer.wrap(_byte_buffer).order(ByteOrder.LITTLE_ENDIAN);
protected long hashed_data_length = 0;
public void update(int b) {
buffer.put((byte)b);
hashed_data_length += 1;
if (buffer.position() >= buffer_limit) round();
}
public void update(long b) {
buffer.putLong(b);
hashed_data_length += Long.BYTES;
if(buffer.position() >= buffer_limit) round();
}
public void update(byte[] data, int off, int len) {
int pos = 0;
while(pos < len) {
int n = Math.min(len - pos, buffer_limit - buffer.position());
System.arraycopy(data, off + pos, _byte_buffer, buffer.position(), n);
pos += n;
buffer.position(buffer.position() + n);
if(buffer.position() >= buffer_limit) round();
}
hashed_data_length += len;
}
public long getValue() {
if(buffer.position() != 0) {
while(buffer.position() < buffer_limit) buffer.put((byte)0);
round();
}
long result = state[0];
result ^= state[1];
result ^= state[2];
result ^= state[3];
result ^= hashed_data_length;
return xorshift64star(result);
}
protected void round() {
int p = buffer.position();
buffer.rewind();
for(int i = 0; i < 4; i++) state[i] ^= buffer.getLong();
for(int i = 0; i < 4; i++) state[i] = xorshift64star(state[i]);
if(p > buffer_limit) {
System.arraycopy(_byte_buffer, buffer_limit, _byte_buffer, 0, buffer.limit() - p);
buffer.position(buffer.limit() - p);
} else buffer.rewind();
}
long xorshift64star(long s) {
s ^= (s >> 12);
s ^= (s << 25);
s ^= (s >> 27);
return s * 0x2545F4914F6CDD1DL;
}
}

View File

@ -0,0 +1,73 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.digest;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.CompressionStatus;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.CountDownLatch;
/**
* What this class does is it collects the hashed files and combines them into a single number,
* thus we can verify file tree integrity
*/
public class FileTreeHashBuilder {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final Object lock = new Object();
private long hash = 0, filesProcessed = 0, filesTotalSize = 0;
private final CountDownLatch latch;
public FileTreeHashBuilder(int filesToProcess) {
latch = new CountDownLatch(filesToProcess);
}
public void update(Path path, long newHash, long bytes) throws IOException {
if(path.getFileName().toString().equals(CompressionStatus.DATA_FILENAME)) return;
latch.countDown();
synchronized (lock) {
this.hash ^= newHash;
filesTotalSize += bytes;
filesProcessed++;
}
}
public int getRemaining() { return (int) latch.getCount(); }
public long getValue(boolean lock) throws InterruptedException {
long leftover = latch.getCount();
if(lock) latch.await();
else if(leftover != 0) log.warn("处理中,剩余{}个文件未处理!", leftover);
var hasher = Globals.CHECKSUM_SUPPLIER.get();
log.debug("文件数:{},字节数:{},原始哈希值:{}", filesProcessed, filesTotalSize, hash);
hasher.update(hash);
hasher.update(filesProcessed);
hasher.update(filesTotalSize);
return hasher.getValue();
}
}

View File

@ -0,0 +1,32 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.digest;
public interface Hash {
void update(int b);
void update(long b);
default void update(byte[] b) { update(b, 0, b.length); }
void update(byte[] b, int off, int len);
long getValue();
}

View File

@ -0,0 +1,98 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.digest;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.core.DataLeftException;
import net.szum123321.textile_backup.core.create.BrokenFileHandler;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
/**
* This class calculates a hash of the file on the input stream, submits it to FileTreeHashBuilder.
* In case the underlying stream hasn't been read completely in, puts it into BrokeFileHandler
* Furthermore, ParallelZip works by putting all the file requests into a queue and then compressing them
* with multiple threads. Thus, we have to make sure that all the files have been read before requesting the final value
* That is what CountDownLatch does
*/
public class HashingInputStream extends FilterInputStream {
private final Path path;
private final Hash hash = Globals.CHECKSUM_SUPPLIER.get();
private final FileTreeHashBuilder hashBuilder;
private final BrokenFileHandler brokenFileHandler;
private long bytesWritten = 0;
public HashingInputStream(InputStream in, Path path, FileTreeHashBuilder hashBuilder, BrokenFileHandler brokenFileHandler) {
super(in);
this.path = path;
this.hashBuilder = hashBuilder;
this.brokenFileHandler = brokenFileHandler;
}
@Override
public int read(byte @NotNull [] b, int off, int len) throws IOException {
int i;
try {
i = in.read(b, off, len);
} catch(IOException e) {
throw new IOException("An exception occurred while trying to access: [" + path.toString() + "]", e);
}
if(i != -1) {
hash.update(b, off, i);
bytesWritten += i;
}
return i;
}
@Override
public int read() throws IOException {
int i;
try {
i = in.read();
} catch(IOException e) {
throw new IOException("An exception occurred while trying to access: [" + path.toString() + "]", e);
}
if(i != -1) {
hash.update(i);
bytesWritten++;
}
return i;
}
@Override
public boolean markSupported() {
return false;
}
@Override
public void close() throws IOException {
hash.update(path.getFileName().toString().getBytes(StandardCharsets.UTF_8));
hashBuilder.update(path, hash.getValue(), bytesWritten);
if(in.available() != 0) brokenFileHandler.handle(path, new DataLeftException(in.available()));
super.close();
}
}

View File

@ -0,0 +1,63 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.digest;
import net.szum123321.textile_backup.Globals;
import org.jetbrains.annotations.NotNull;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
public class HashingOutputStream extends FilterOutputStream {
private final Path path;
private final Hash hash = Globals.CHECKSUM_SUPPLIER.get();
private final FileTreeHashBuilder hashBuilder;
private long bytesWritten = 0;
public HashingOutputStream(OutputStream out, Path path, FileTreeHashBuilder hashBuilder) {
super(out);
this.path = path;
this.hashBuilder = hashBuilder;
}
@Override
public void write(int b) throws IOException {
out.write(b);
hash.update(b);
bytesWritten++;
}
@Override
public void write(byte @NotNull [] b, int off, int len) throws IOException {
out.write(b, off, len);
hash.update(b, off, len);
bytesWritten += len;
}
@Override
public void close() throws IOException {
hash.update(path.getFileName().toString().getBytes(StandardCharsets.UTF_8));
hashBuilder.update(path, hash.getValue(), bytesWritten);
super.close();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2021 Szum123321
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -20,16 +20,20 @@ package net.szum123321.textile_backup.mixin;
import net.minecraft.server.dedicated.DedicatedServerWatchdog;
import net.minecraft.util.Util;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.Globals;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
/**
* This mixin should numb Watchdog while a backup runs.
* If works as intended solves issues with watchdog errors
*/
@Mixin(DedicatedServerWatchdog.class)
public class DedicatedServerWatchdogMixin {
@ModifyVariable(method = "run()V", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/util/Util;getMeasuringTimeMs()J"), ordinal = 0, name = "l")
private long redirectedCall(long original) {
return Statics.disableWatchdog ? Util.getMeasuringTimeMs() : original;
return Globals.INSTANCE.disableWatchdog ? Util.getMeasuringTimeMs() : original;
}
}

View File

@ -1,44 +0,0 @@
/*
A simple backup mod for Fabric
Copyright (C) 2020 Szum123321
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.mixin;
import net.minecraft.server.MinecraftServer;
import net.szum123321.textile_backup.core.LivingServer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(MinecraftServer.class)
public class MinecraftServerMixin implements LivingServer {
@Unique
private boolean isAlive = true;
@Inject(method = "shutdown", at = @At("TAIL"))
public void onFinalWorldSave(CallbackInfo ci) {
isAlive = false;
}
@Unique
@Override
public boolean isAlive() {
return isAlive;
}
}

View File

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

View File

@ -0,0 +1,63 @@
/*
* A simple backup mod for Fabric
* Copyright (C) 2022 Szum123321
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.test;
import net.minecraft.util.math.random.Random;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.digest.BalticHash;
public class BalticHashTest {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
final static int TEST_LEN = 21377; //simple prime
public static void run() throws RuntimeException {
log.info("Running hash test");
Random r = Random.create(2137);
long x = 0;
byte[] data = new byte[TEST_LEN];
for(int i = 0; i < TEST_LEN; i++) data[i] = (byte)r.nextInt();
//Test block mode
for(int i = 0; i < 5*2; i++) x ^= randomHash(data, r);
if(x != 0) throw new RuntimeException("Hash mismatch!");
log.info("Test passed");
}
static long randomHash(byte[] data, Random r) {
int n = data.length;
BalticHash h = new BalticHash();
int m = r.nextBetween(1, n);
int nn = n, p = 0;
for(int i = 0; i < m; i++) {
int k = r.nextBetween(1, nn - (m - i - 1));
h.update(data, p, k);
p += k;
nn -= k;
}
return h.getValue();
}
}

View File

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

View File

@ -24,7 +24,7 @@
"text.autoconfig.textile_backup.option.perWorldBackup": "Use separate folders for different worlds",
"text.autoconfig.textile_backup.option.path": "Path to backup folder",
"text.autoconfig.textile_backup.option.backupDirectoryPath": "Path to backup folder",
"text.autoconfig.textile_backup.option.fileBlacklist": "Blacklisted files",
@ -36,7 +36,7 @@
"text.autoconfig.textile_backup.option.maxAge.@Tooltip": "In seconds since creation",
"text.autoconfig.textile_backup.option.maxSize": "Max size of backup folder",
"text.autoconfig.textile_backup.option.maxSize.@Tooltip": "In KBytes",
"text.autoconfig.textile_backup.option.maxSize.@Tooltip": "In KiBytes",
"text.autoconfig.textile_backup.option.compression": "Compression level",
"text.autoconfig.textile_backup.option.compression.@Tooltip": "Only affects zip",
@ -47,6 +47,9 @@
"text.autoconfig.textile_backup.option.format": "Archive and compression format",
"text.autoconfig.textile_backup.option.format.@Tooltip": "See: https://github.com/Szum123321/textile_backup/wiki/Configuration#format",
"text.autoconfig.textile_backup.option.integrityVerificationMode": "Verify backup integrity",
"text.autoconfig.textile_backup.option.integrityVerificationMode.@Tooltip": "DO NOT ALTER unless fully aware of consequences",
"text.autoconfig.textile_backup.option.permissionLevel": "Min permission level",
"text.autoconfig.textile_backup.option.alwaysSingleplayerAllowed": "Always allow on single-player",

View File

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

View File

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

View File

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