Compare commits

...

187 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
Szum123321 569892ac5a Changed mappings 2021-11-30 20:58:13 +01:00
Szum123321 fd878e5b1b Version Bump 2021-11-28 10:34:24 +01:00
Szum123321 9caf851783 Replaced ZipArchiveInputStream based zip decompressor with one based on ZipFile (might be useful for partial restore, but no promise) 2021-11-27 20:11:43 +01:00
Szum123321 50e465fc17 Added more accurate detection of insufficient /tmp 2021-11-27 19:58:51 +01:00
Szum123321 eb6f4fdb10 Added Config option to enable/disable global announcement of backup starting and finishing 2021-11-27 19:56:11 +01:00
Szum123321 3bf91ea915 1.18 dep and name update 2021-11-27 19:54:35 +01:00
Szum123321 b258523fd5 Merge remote-tracking branch 'origin/2.x' into 2.x 2021-10-03 10:12:45 +02:00
Szum123321 51330a036d Added backup done message, Moved notifyPlayer to Utilities (closed #71) 2021-10-03 10:12:33 +02:00
Szum123321 e319a00169
Update README.md 2021-08-04 16:32:41 +02:00
Szum123321 c736c1db08
Update README.md 2021-08-04 16:32:20 +02:00
Szum123321 7957602f83
Update README.md 2021-08-04 16:26:57 +02:00
Szum123321 cb55d5c9a4 Repaired merge error 2021-08-03 23:47:56 +02:00
Szum123321 d24aa5c9e8
Merge pull request #70 from Szum123321/auto_config
Auto config
2021-08-03 23:44:37 +02:00
Szum123321 5b152927f0 update copyright notice 2021-08-03 23:42:39 +02:00
Szum123321 50db7efe08 lang update 2021-08-03 23:19:02 +02:00
Szum123321 a6881b68cd Text correction 2021-08-03 22:13:00 +02:00
Szum123321 2fab8c67f3 Repaired fabric.mod.json 2021-08-03 22:12:36 +02:00
Szum123321 76d7326237 Locked parallel gzip version 2021-08-03 22:12:18 +02:00
Szum123321 bf5f046985 This was stupid 2021-08-03 21:27:10 +02:00
Szum123321 33f01e8189 Dep update 2021-08-03 21:26:42 +02:00
Szum123321 111fe12ae7 Moved close to finally 2021-08-03 21:26:11 +02:00
szymon 8fe0cae73a Added dependency info to fabric.mod.json 2021-07-09 22:43:47 +02:00
szymon d6430c9bef version bump 2021-07-08 21:46:22 +02:00
szymon 121d6269cd Added missing copyright headers 2021-07-08 21:45:58 +02:00
szymon 585dd01c02 Moved one perWorldBackup to the top so that General category appears on the top. I might rearrange other settings in more logical way 2021-07-07 14:28:51 +02:00
szymon 47f8c5cbc3 Style correction, removed unnecessary constructors from records 2021-07-07 14:23:54 +02:00
szymon 8858bd498b Use stream instead of for loop 2021-07-07 14:21:49 +02:00
szymon a0c7b478cb Added missing save command 2021-07-07 14:21:22 +02:00
szymon cdd2b78c43 getMinecraftServer was renamed to getServer
Also updated to mc 1.17.1
2021-07-06 22:33:00 +02:00
szymon e9f6a29eb2 Yes! Zero changes to config file look! 2021-07-06 22:15:30 +02:00
Szum123321 d991dc0339
Merge pull request #68 from Szum123321/2.1.0
2.1.0
2021-06-26 13:25:02 +02:00
Szum123321 0d093d75c4
Merge branch '2.x' into 2.1.0 2021-06-26 13:24:42 +02:00
szymon a8f98c460e Many, many changes:
- I migrated from cotton config to Cloth config + Auto config, that way it's easy to provide modmenu integration while keeping backward-compatibility. Because settings might change at runtime it's now necessary to keep the config reference up-to-date. I done this by using wrapper class around ConfigHolder<> and having a reference to main instance in each class accessing the config.

 - Switched form using one global logger to creating new instance in every class.

 - Removed BackupScheduler from Statics. Why did I put it there in the first place?
2021-06-26 13:10:35 +02:00
szymon cc912d322e added copyright notice into output jar 2021-06-19 19:03:13 +02:00
szymon fee04a9943 small correction 2021-06-19 18:48:46 +02:00
szymon fcc1dca958 BackupHelper cleanup 2021-06-19 15:39:00 +02:00
szymon 725d4098be Replaced File with Optional<File> in Statics to make sure I won't forget to check if it exists 2021-06-19 14:44:06 +02:00
szymon af8e14f092 Repaired #42 2021-06-19 14:42:22 +02:00
szymon d0772b44eb i think it's time 2021-06-17 22:24:58 +02:00
szymon 0a5b5e5c54 Forgot to remove LZMACompressor.java 2021-06-17 22:20:37 +02:00
szymon 78128bc63b Slightly beeter logs 2021-06-17 22:17:55 +02:00
szymon adc14273d0 Java 16 update 2021-06-16 22:42:11 +02:00
szymon 24455d2e5c Last version didn't work with different system languages. This now works but basically depends on a regex 2021-06-14 23:08:56 +02:00
szymon d322f6dc26 Mc version bump 2021-06-13 22:26:17 +02:00
szymon 2ba6ba444c Changed chat message prefix color to match mod's theme. Slightly improved functions notifyPlayer in BackupHelper and RestoreHelper 2021-06-13 22:26:05 +02:00
szymon 95653d066d Added slightly more graceful error handling for THE ISSUE 2021-06-13 21:44:38 +02:00
szymon 5cfb5c393a Version bump 2021-06-13 21:27:08 +02:00
szymon ab32bd6ead Make it compile and a bit of cleanup 2021-06-13 21:26:57 +02:00
szymon 18e8860f42 1.17 update 2021-06-13 21:25:52 +02:00
szymon f3982d78c0 . 2021-01-02 17:20:56 +01:00
szymon 3c5976f198 Added uncompressed option 2021-01-02 17:20:12 +01:00
szymon acba0d0cb8 File names now end with minecraft minor version and should not contain patch id. 2020-12-25 19:34:36 +01:00
szymon c344e01c26 Better CRC32 2020-12-14 13:18:16 +01:00
szymon 927638e0b6 consistency 2020-12-14 12:56:36 +01:00
szymon 09b809de8f Fixed #52 2020-12-13 12:30:58 +01:00
Szum123321 75eec3e040
Update README.md 2020-12-07 00:05:53 +01:00
szymon e92ef1fb28 Version bump 2020-12-07 00:02:54 +01:00
szymon 1b321b7a64 Changed default dateTimeFormat to ISO standard 2020-12-06 22:09:19 +01:00
szymon 7e7280bb82 v 2020-12-06 21:56:23 +01:00
szymon 39d3c3dacd Improved error catching 2020-12-06 21:26:11 +01:00
szymon 8506bef30b Added error catching 2020-12-06 19:19:16 +01:00
szymon 445f7a5613 Some improvements to logging, Player data now gets saved. Few other things... 2020-12-05 23:14:35 +01:00
szymon 7f82664ae7 Added Delete command.
Rearranged command package
2020-12-05 22:17:22 +01:00
szymon d29b9049a4 Typos + missing import 2020-12-02 21:12:58 +01:00
szymon 7e2e399829 Improved RestoreBackupCommand 2020-12-02 21:10:48 +01:00
szymon b68640b37f Added CRC32 calculation to Stored entries in Zip (caused exceptions) 2020-12-02 21:06:46 +01:00
szymon eba22e8464 Added newBackupContextBuilder function to BackupContext.Builder and simplified StartBackupCommand 2020-12-02 21:05:16 +01:00
szymon 06f80a9175 Moved locking world form BackupHelper to MakeBackupRunnable 2020-12-02 21:00:46 +01:00
szymon 163626fb3d Added RestoreContext for good measure 2020-12-02 20:59:47 +01:00
szymon 7c07d2934c Finally no double output! 2020-12-02 20:57:49 +01:00
szymon d27568c20f Renamed BackupInitiator to ActionInitiator and moved it to separate class 2020-12-02 20:49:49 +01:00
szymon 8ba3dbc955 Added comment to isDotDat method 2020-11-30 12:00:29 +01:00
szymon 6f9e98f104 File -> RestoreableFile 2020-11-30 10:35:27 +01:00
szymon 870f133273 Logger.warn -> error 2020-11-30 10:34:48 +01:00
szymon 2de5f4c80b Added missing import, removed duplicate method 2020-11-30 10:34:20 +01:00
szymon 5d7aaf1938 Added some more info to Config 2020-11-30 10:32:58 +01:00
szymon 3b522e2881 Redesign of Compression an archival logic.
Added ZipCompressor
Permanently fixed java.util.concurrent.ExecutionException: java.io.IOException: No space left on device.
2020-11-30 10:32:22 +01:00
szymon a5114b49cc Renamed getFileExtension -> getArchiveExtension
Some stylistic improvements
2020-11-30 10:27:26 +01:00
szymon aac63d45ac Partial redesign of RestoreHelper. It now uses RestoreableFile everywhere.
RestoreableFile is now comparable
2020-11-30 10:25:16 +01:00
szymon 0ef89c2a88 Added missing dot 2020-11-30 10:20:21 +01:00
szymon 335ab845cf Added getPrefix method, changed prefixText from Text to MutableText 2020-11-30 10:19:27 +01:00
szymon 2a211065a3 Solves #48 2020-11-30 10:18:35 +01:00
szymon ead057d916 Added custom thread names 2020-11-30 10:17:39 +01:00
szymon ccd87ee942 Added contributors list 2020-11-30 10:16:34 +01:00
szymon 59c691bd59 Dependency update + version bump 2020-11-30 10:15:59 +01:00
75 changed files with 3858 additions and 1990 deletions

View File

@ -1,10 +1,14 @@
This project uses third party libraries as its dependencies and includes them in jar. Those are :
Apache Commons Compress licensed under Apache License Version 2.0 which can be found at http://www.apache.org/licenses/
Cotton config, Cotton logging, and Jankson-Fabric all by Cotton team licensed under MIT license which can be found at https://github.com/CottonMC/Cotton
Cloth config by Shedaniel(https://github.com/shedaniel/cloth-config) under GPL 3
XZ for Java by Tukaani released as public domain. https://tukaani.org/xz/java.html
parallelgzip by shevek under Apache 2.0 http://www.apache.org/licenses/
parallelgzip by shevek (https://github.com/shevek/parallelgzip) under Apache 2.0 http://www.apache.org/licenses/
To save on space Parallel BZip2 was unpacked
Parallel BZip2 compression by Karl Gustafsson at http://at4j.sourceforge.net/ under GPL v3
Some code was partially or fully inspired by:
Parallel zip compression: https://stackoverflow.com/questions/54624695/how-to-implement-parallel-zip-creation-with-scatterzipoutputstream-with-zip64-su
answer by: https://stackoverflow.com/users/2987755/dkb
Cotton logging by Cotton team licensed under MIT license which can be found at https://github.com/CottonMC/Cotton

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,36 +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 <version> [comment]`
* killR - terminate current restoration.
* list - lists all avaliable backups.
* cleanup - forces cleanup procedure (deletes old backups according to config)
* 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*.
* _This mod contains **Cotton Config** and its dependencies as jars in a jar, which are property of **CottonMC**_.
### 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)
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,21 +1,20 @@
plugins {
id 'fabric-loom' version '0.8-SNAPSHOT'
id 'fabric-loom' version '1.2-SNAPSHOT'
id 'maven-publish'
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
archivesBaseName = project.archives_base_name
version = "${project.mod_version}-${project.minecraft_version}"
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/" }
mavenCentral()
}
dependencies {
@ -27,20 +26,29 @@ dependencies {
// Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
modImplementation "io.github.cottonmc.cotton:cotton-config:1.0.0-rc.7"
//General config library
modApi("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_version}") {
exclude(group: "net.fabricmc.fabric-api")
}
include "io.github.cottonmc:Jankson-Fabric:3.0.0+j1.2.0"
include "io.github.cottonmc.cotton:cotton-logging:1.0.0-rc.4"
include "io.github.cottonmc.cotton:cotton-config:1.0.0-rc.7"
//Mod menu
modImplementation("com.terraformersmc:modmenu:${project.modmenu_version}")
modImplementation "org.apache.commons:commons-compress:1.19"
include "org.apache.commons:commons-compress:1.19"
//General compression library
implementation "org.apache.commons:commons-compress:1.22"
include "org.apache.commons:commons-compress:1.22"
modImplementation "org.tukaani:xz:1.8"
include "org.tukaani:xz:1.8"
//LZMA support
implementation 'org.tukaani:xz:1.9'
include "org.tukaani:xz:1.9"
modImplementation 'com.github.shevek:parallelgzip:master-SNAPSHOT'
include 'com.github.shevek:parallelgzip:master-SNAPSHOT'
//Gzip compression, parallel, GITHUB
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.
// I couldn't get this working in my environment - IzzyBizzy
//modLocalRuntime("com.github.astei:lazydfu:${project.lazydfu_version}")
}
processResources {
@ -57,8 +65,6 @@ processResources {
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
java {
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
// if it is present.
@ -70,8 +76,9 @@ jar {
from("LICENSE") {
rename { "${it}_${project.archivesBaseName}"}
}
}
from("Copyright_Notice")
}
// configure the maven publication
publishing {
publications {
@ -89,6 +96,18 @@ publishing {
// select the repositories you want to publish to
repositories {
// uncomment to publish to the local maven
// mavenLocal()
maven {
name = 'myRepo'
url = layout.buildDirectory.dir("repo")
}
}
}
static def getMcMinor(ver) {
String[] arr = ((String)ver).split("[.-]")
if(arr.length < 2) return ver
return (String)(arr[0] + "." + arr[1])
}

View File

@ -1,14 +1,25 @@
# Done to increase the memory available to gradle.
org.gradle.jvmargs=-Xmx1G
minecraft_version=1.17
yarn_mappings=1.17+build.10
loader_version=0.11.5
minecraft_version=1.20
yarn_mappings=1.20+build.1
loader_version=0.14.21
#Fabric api
fabric_version=0.35.1+1.17
fabric_version=0.83.0+1.20
#Cloth Config
cloth_version=11.0.98
#ModMenu
modmenu_version=7.0.0-beta.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.0.2
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

@ -1,141 +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 blue.endless.jankson.Comment;
import io.github.cottonmc.cotton.config.annotations.ConfigFile;
import java.io.File;
import java.time.format.DateTimeFormatter;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
@ConfigFile(name = Statics.MOD_ID)
public class ConfigHandler {
@Comment("\nTime between automatic backups in seconds\n" +
"When set to 0 backups will not be performed automatically\n")
public long backupInterval = 3600;
@Comment("\nDelay in seconds between typing-in /backup restore and it actually starting\n")
public int restoreDelay = 30;
@Comment("\nShould backups be done even if there are no players?\n")
public boolean doBackupsOnEmptyServer = false;
@Comment("\nShould backup be made on server shutdown?\n")
public boolean shutdownBackup = true;
@Comment("\nShould world be backed up before restoring a backup?\n")
public boolean backupOldWorlds = true;
@Comment("\nShould every world have its own backup folder?\n")
public boolean perWorldBackup = true;
@Comment("\nA path to the backup folder\n")
public String path = "backup/";
@Comment("\nThis setting allows you to exclude files form being backedup.\n"+
"Be very careful when setting it, as it is easy corrupt your world!\n")
public Set<String> fileBlacklist = new HashSet<>();
@Comment("\nShould backups be deleted after being restored?\n")
public boolean deleteOldBackupAfterRestore = true;
@Comment("\nMaximum number of backups to keep. If set to 0 then no backup will be deleted based their amount\n")
public int backupsToKeep = 10;
@Comment("\nMaximum age of backups to keep in seconds.\n If set to 0 then backups will not be deleted based their age \n")
public long maxAge = 0;
@Comment("\nMaximum size of backup folder in kilo bytes (1024).\n" +
"If set to 0 then backups will not be deleted\n")
public int maxSize = 0;
@Comment("\nCompression level \n0 - 9\n Only affects zip compression.\n")
public int compression = 7;
@Comment("\nLimit how many cores can be used for compression.\n")
public int compressionCoreCountLimit = 0;
@Comment(value = "\nAvailable formats are:\n" +
"ZIP - normal zip archive using standard deflate compression\n" +
"GZIP - tar.gz using gzip compression\n" +
"BZIP2 - tar.bz2 archive using bzip2 compression\n" +
"LZMA - tar.xz using lzma compression\n")
public ArchiveFormat format = ArchiveFormat.ZIP;
@Comment("\nMinimal permission level required to run commands\n")
public int permissionLevel = 4;
@Comment("\nPlayer on singleplayer is always allowed to run command. Warning! On lan party everyone will be allowed to run it.\n")
public boolean alwaysSingleplayerAllowed = true;
@Comment("\nPlayers allowed to run backup commands without sufficient permission level\n")
public Set<String> playerWhitelist = new HashSet<>();
@Comment("\nPlayers banned from running backup commands besides their sufficient permission level\n")
public Set<String> playerBlacklist = new HashSet<>();
@Comment("\nFormat of date&time used to name backup files.\n" +
"Remember not to use '#' symbol or any other character that is not allowed by your operating system such as:\n" +
"':', '\\', etc...\n" +
"For more info: https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html\n")
public String dateTimeFormat = "dd.MM.yyyy_HH-mm-ss";
public Optional<String> sanitize() {
if(compressionCoreCountLimit > Runtime.getRuntime().availableProcessors())
return Optional.of("compressionCoreCountLimit is too big! Your system only has: " + Runtime.getRuntime().availableProcessors() + " cores!");
try {
DateTimeFormatter.ofPattern(dateTimeFormat);
} catch (IllegalArgumentException e) {
return Optional.of("dateTimeFormat is wrong!\n" + e.getMessage() + "\n See: https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html");
}
File path = new File(Statics.CONFIG.path).getAbsoluteFile();
if (!path.exists()) {
try {
path.mkdirs();
} catch (Exception e) {
return Optional.of("Something went wrong while creating backup folder!\n" + e.getMessage());
}
}
return Optional.empty();
}
public enum ArchiveFormat {
ZIP(".zip"),
GZIP(".tar.gz"),
BZIP2(".tar.bz2"),
LZMA(".tar.xz");
private final String extension;
private ArchiveFormat(String extension){
this.extension = extension;
}
public String getString() {
return extension;
}
}
}

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,45 +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.CustomLogger;
import net.szum123321.textile_backup.core.create.BackupScheduler;
import net.szum123321.textile_backup.core.restore.AwaitThread;
import java.io.File;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
public class Statics {
public static final String MOD_ID = "textile_backup";
public static final String MOD_NAME = "Textile Backup";
public static final CustomLogger LOGGER = new CustomLogger(MOD_ID, MOD_NAME);
public static ConfigHandler CONFIG;
public static final BackupScheduler scheduler = new BackupScheduler();
public static ExecutorService executorService = Executors.newSingleThreadExecutor();
public final static DateTimeFormatter defaultDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss");
public static final AtomicBoolean globalShutdownBackupFlag = new AtomicBoolean(true);
public static AwaitThread restoreAwaitThread = null;
public static File untouchableFile;
}

View File

@ -1,88 +1,102 @@
/*
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;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import io.github.cottonmc.cotton.config.ConfigManager;
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;
import net.szum123321.textile_backup.commands.permission.BlacklistCommand;
import net.szum123321.textile_backup.commands.permission.WhitelistCommand;
import net.szum123321.textile_backup.commands.manage.BlacklistCommand;
import net.szum123321.textile_backup.commands.manage.DeleteCommand;
import net.szum123321.textile_backup.commands.manage.WhitelistCommand;
import net.szum123321.textile_backup.commands.restore.KillRestoreCommand;
import net.szum123321.textile_backup.commands.restore.ListBackupsCommand;
import net.szum123321.textile_backup.commands.manage.ListBackupsCommand;
import net.szum123321.textile_backup.commands.restore.RestoreBackupCommand;
import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.core.create.BackupHelper;
import java.util.Optional;
import java.util.concurrent.Executors;
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.create.BackupScheduler;
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";
public static final String MOD_ID = "textile_backup";
private final static TextileLogger log = new TextileLogger(MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
@Override
public void onInitialize() {
Statics.LOGGER.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()
);
Statics.CONFIG = ConfigManager.loadConfig(ConfigHandler.class);
Optional<String> errorMessage = Statics.CONFIG.sanitize();
log.info("Starting Textile Backup {} by Szum123321", Globals.INSTANCE.getCombinedVersionString());
if(errorMessage.isPresent()) {
Statics.LOGGER.fatal("TextileBackup config file has wrong settings!\n{}", errorMessage.get());
System.exit(1);
}
ConfigHelper.updateInstance(AutoConfig.register(ConfigPOJO.class, JanksonConfigSerializer::new));
if(Statics.CONFIG.backupInterval > 0)
ServerTickEvents.END_SERVER_TICK.register(Statics.scheduler::tick);
ServerTickEvents.END_SERVER_TICK.register(BackupScheduler::tick);
ServerLifecycleEvents.SERVER_STARTING.register(ignored -> {
if(Statics.executorService.isShutdown())
Statics.executorService = Executors.newSingleThreadExecutor();
//Restart Executor Service in single-player
ServerLifecycleEvents.SERVER_STARTING.register(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 (Statics.CONFIG.shutdownBackup && Statics.globalShutdownBackupFlag.get()) {
BackupHelper.create(
new BackupContext.Builder()
if (config.get().shutdownBackup && Globals.INSTANCE.globalShutdownBackupFlag.get()) {
try {
ExecutableBackup.Builder
.newBackupContextBuilder()
.setServer(server)
.setInitiator(BackupContext.BackupInitiator.Shutdown)
.setInitiator(ActionInitiator.Shutdown)
.setComment("shutdown")
.announce()
.build()
).run();
.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 {
return ((Statics.CONFIG.playerWhitelist.contains(ctx.getEntityOrThrow().getEntityName()) ||
ctx.hasPermissionLevel(Statics.CONFIG.permissionLevel)) &&
!Statics.CONFIG.playerBlacklist.contains(ctx.getEntityOrThrow().getEntityName())) ||
(ctx.getMinecraftServer().isSinglePlayer() &&
Statics.CONFIG.alwaysSingleplayerAllowed);
return ((config.get().playerWhitelist.contains(ctx.getEntityOrThrow().getEntityName()) ||
ctx.hasPermissionLevel(config.get().permissionLevel)) &&
!config.get().playerBlacklist.contains(ctx.getEntityOrThrow().getEntityName())) ||
(ctx.getServer().isSingleplayer() &&
config.get().alwaysSingleplayerAllowed);
} catch (Exception ignored) { //Command was called from server console.
return true;
}
@ -94,6 +108,7 @@ public class TextileBackup implements ModInitializer {
.then(BlacklistCommand.register())
.then(RestoreBackupCommand.register())
.then(ListBackupsCommand.register())
.then(DeleteCommand.register())
.then(KillRestoreCommand.register())
));
}

View File

@ -0,0 +1,149 @@
/*
* 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.command.ServerCommandSource;
import net.minecraft.text.Text;
import net.minecraft.text.MutableText;
import net.minecraft.util.Formatting;
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;
import org.apache.logging.log4j.message.MessageFactory;
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
import org.apache.logging.log4j.util.StackLocatorUtil;
/*
This is practically just a copy-pate of Cotton's ModLogger with a few changes
*/
public class TextileLogger {
//private final boolean isDev = FabricLoader.getInstance().isDevelopmentEnvironment();
private final MessageFactory messageFactory;
private final Logger logger;
private final String prefix;
private final MutableText prefixText;
public TextileLogger(String prefix) {
this.messageFactory = ParameterizedMessageFactory.INSTANCE;
this.logger = LogManager.getLogger(StackLocatorUtil.getCallerClass(2), messageFactory);
this.prefix = "[" + prefix + "]" + " ";
this.prefixText = Text.literal(this.prefix).styled(style -> style.withColor(0x5B23DA));
}
public MutableText getPrefixText() {
return prefixText.copy();
}
public void log(Level level, String msg, Object... data) {
logger.log(level, prefix + msg, data);
}
public void trace(String msg, Object... data) {
log(Level.TRACE, msg, data);
}
public void debug(String msg, Object... data) {
log(Level.DEBUG, msg, data);
}
public void info(String msg, Object... data) {
log(Level.INFO, msg, data);
}
public void warn(String msg, Object... data) {
log(Level.WARN, msg, data);
}
public void error(String msg, Object... data) {
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 && 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.copy().append(text), false);
return true;
} else {
log(level, msg, args);
return false;
}
}
public void sendHint(ServerCommandSource source, String msg, Object... args) {
sendFeedback(Level.TRACE, source, msg, args);
}
public void sendInfo(ServerCommandSource source, String msg, Object... args) {
sendFeedback(Level.INFO, source, 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(ExecutableBackup context, String msg, Object... args) {
sendError(context.commandSource(), msg, args);
}
public void sendToPlayerAndLog(Level level, ServerCommandSource source, String msg, Object... args) {
if(sendFeedback(level, source, msg, args))
log(level, msg, args);
}
//send info and log
public void sendInfoAL(ServerCommandSource source, String msg, Object... args) {
sendToPlayerAndLog(Level.INFO, source, 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(ExecutableBackup context, String msg, Object... args) {
sendErrorAL(context.commandSource(), msg, args);
}
}

View File

@ -0,0 +1,31 @@
/*
* 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;
import com.terraformersmc.modmenu.api.ModMenuApi;
import me.shedaniel.autoconfig.AutoConfig;
import net.szum123321.textile_backup.config.ConfigPOJO;
public class ModMenuEntry implements ModMenuApi {
@Override
public ConfigScreenFactory<?> getModConfigScreenFactory() {
return parent -> AutoConfig.getConfigScreen(ConfigPOJO.class, parent).get();
}
}

View File

@ -0,0 +1,39 @@
/*
* 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;
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
import net.minecraft.text.Text;
import net.minecraft.text.MutableText;
import java.time.format.DateTimeParseException;
public class CommandExceptions {
public static final DynamicCommandExceptionType DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE = new DynamicCommandExceptionType(o -> {
DateTimeParseException e = (DateTimeParseException)o;
MutableText message = Text.literal("An exception occurred while trying to parse:\n")
.append(e.getParsedString())
.append("\n");
for (int i = 0; i < e.getErrorIndex(); i++) message.append(" ");
return message.append("^");
});
}

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.commands;
import com.mojang.brigadier.LiteralMessage;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.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;
public final class FileSuggestionProvider implements SuggestionProvider<ServerCommandSource> {
private static final FileSuggestionProvider INSTANCE = new FileSuggestionProvider();
public static FileSuggestionProvider Instance() { return INSTANCE; }
@Override
public CompletableFuture<Suggestions> getSuggestions(CommandContext<ServerCommandSource> ctx, SuggestionsBuilder builder) {
String remaining = builder.getRemaining();
var files = RestoreHelper.getAvailableBackups(ctx.getSource().getServer());
for (RestoreableFile file: files) {
String formattedCreationTime = file.getCreationTime().format(Globals.defaultDateTimeFormatter);
if (formattedCreationTime.startsWith(remaining)) {
if (Utilities.wasSentByPlayer(ctx.getSource())) { //was typed by player
if (file.getComment().isPresent()) {
builder.suggest(formattedCreationTime, new LiteralMessage("Comment: " + file.getComment().get()));
} else {
builder.suggest(formattedCreationTime);
}
} else { //was typed from server console
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,42 +1,44 @@
/*
A simple backup mod for Fabric
Copyright (C) 2020 Szum123321
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
* 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;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.LiteralText;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.core.create.BackupHelper;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.Cleanup;
import net.szum123321.textile_backup.core.Utilities;
public class CleanupCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("cleanup")
.executes(ctx -> execute(ctx.getSource()));
}
private static int execute(ServerCommandSource source) {
Statics.LOGGER.sendInfo(
log.sendInfo(
source,
"Deleted: {} files.",
BackupHelper.executeFileLimit(source, Utilities.getLevelName(source.getMinecraftServer()))
"删除了: {} 文件.",
new Cleanup(source, Utilities.getLevelName(source.getServer())).call()
);
return 1;

View File

@ -1,66 +1,53 @@
/*
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;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.core.create.BackupHelper;
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.ExecutableBackup;
import javax.annotation.Nullable;
public class StartBackupCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("start")
.then(CommandManager.argument("comment", StringArgumentType.string())
.executes(StartBackupCommand::executeWithComment)
).executes(ctx -> execute(ctx.getSource()));
.executes(ctx -> execute(ctx.getSource(), StringArgumentType.getString(ctx, "comment")))
).executes(ctx -> execute(ctx.getSource(), null));
}
private static int executeWithComment(CommandContext<ServerCommandSource> ctx) {
if(!Statics.executorService.isShutdown())
Statics.executorService.submit(
BackupHelper.create(
new BackupContext.Builder()
.setCommandSource(ctx.getSource())
.setComment(StringArgumentType.getString(ctx, "comment"))
.guessInitiator()
.saveServer()
.build()
)
);
return 1;
}
private static int execute(ServerCommandSource source){
if(!Statics.executorService.isShutdown())
Statics.executorService.submit(
BackupHelper.create(
new BackupContext.Builder()
private static int execute(ServerCommandSource source, @Nullable String comment) {
Globals.INSTANCE.getQueueExecutor().submit(
ExecutableBackup.Builder
.newBackupContextBuilder()
.setCommandSource(source)
.setComment(comment)
.guessInitiator()
.saveServer()
.build()
)
);
return 1;

View File

@ -0,0 +1,119 @@
/*
* 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.manage;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.command.argument.EntityArgumentType;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
public class BlacklistCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("blacklist")
.then(CommandManager.literal("add")
.then(CommandManager.argument("player", EntityArgumentType.player())
.executes(BlacklistCommand::executeAdd)
)
).then(CommandManager.literal("remove")
.then(CommandManager.argument("player", EntityArgumentType.player())
.executes(BlacklistCommand::executeRemove)
)
).then(CommandManager.literal("list")
.executes(ctx -> executeList(ctx.getSource()))
).executes(ctx -> help(ctx.getSource()));
}
private static int help(ServerCommandSource source) {
log.sendInfo(source, "可用的命令有add [player]remove [player]list. ");
return 1;
}
private static int executeList(ServerCommandSource source) {
StringBuilder builder = new StringBuilder();
builder.append("目前在黑名单上的有:");
for(String name : config.get().playerBlacklist){
builder.append(name);
builder.append(", ");
}
log.sendInfo(source, builder.toString());
return 1;
}
private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(config.get().playerBlacklist.contains(player.getEntityName())) {
log.sendInfo(ctx.getSource(), "玩家: {} 已经在黑名单中!", player.getEntityName());
} else {
config.get().playerBlacklist.add(player.getEntityName());
config.save();
StringBuilder builder = new StringBuilder();
builder.append("玩家: ");
builder.append(player.getEntityName());
builder.append(" 被添加到黑名单");
if(config.get().playerWhitelist.contains(player.getEntityName())){
config.get().playerWhitelist.remove(player.getEntityName());
config.save();
builder.append(" 并且被移除白名单");
}
builder.append(" 成功.");
ctx.getSource().getServer().getCommandManager().sendCommandTree(player);
log.sendInfo(ctx.getSource(), builder.toString());
}
return 1;
}
private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(!config.get().playerBlacklist.contains(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.getEntityName());
}
return 1;
}
}

View File

@ -0,0 +1,88 @@
/*
* 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.manage;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.commands.CommandExceptions;
import net.szum123321.textile_backup.commands.FileSuggestionProvider;
import net.szum123321.textile_backup.core.RestoreableFile;
import net.szum123321.textile_backup.core.Utilities;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
import java.util.Optional;
public class DeleteCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("delete")
.then(CommandManager.argument("file", StringArgumentType.word())
.suggests(FileSuggestionProvider.Instance())
.executes(ctx -> execute(ctx.getSource(), StringArgumentType.getString(ctx, "file")))
);
}
private static int execute(ServerCommandSource source, String fileName) throws CommandSyntaxException {
LocalDateTime dateTime;
try {
dateTime = LocalDateTime.from(Globals.defaultDateTimeFormatter.parse(fileName));
} catch (DateTimeParseException e) {
throw CommandExceptions.DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE.create(e);
}
Path root = Utilities.getBackupRootPath(Utilities.getLevelName(source.getServer()));
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(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");
}
);
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
@ -16,33 +16,35 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.commands.restore;
package net.szum123321.textile_backup.commands.manage;
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.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.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.*;
public class ListBackupsCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("list")
.executes(ctx -> { StringBuilder builder = new StringBuilder();
List<RestoreHelper.RestoreableFile> backups = RestoreHelper.getAvailableBackups(ctx.getSource().getMinecraftServer());
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(Comparator.comparing(RestoreHelper.RestoreableFile::getCreationTime));
Iterator<RestoreHelper.RestoreableFile> iterator = backups.iterator();
builder.append("Available backups: ");
backups.sort(null);
Iterator<RestoreableFile> iterator = backups.iterator();
builder.append("可用的备份文件:\n");
builder.append(iterator.next());
@ -52,7 +54,7 @@ public class ListBackupsCommand {
}
}
Statics.LOGGER.sendInfo(ctx.getSource(), builder.toString());
log.sendInfo(ctx.getSource(), builder.toString());
return 1;
});

View File

@ -0,0 +1,119 @@
/*
* 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.manage;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.command.argument.EntityArgumentType;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
public class WhitelistCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public static LiteralArgumentBuilder<ServerCommandSource> register(){
return CommandManager.literal("whitelist")
.then(CommandManager.literal("add")
.then(CommandManager.argument("player", EntityArgumentType.player())
.executes(WhitelistCommand::executeAdd)
)
).then(CommandManager.literal("remove")
.then(CommandManager.argument("player", EntityArgumentType.player())
.executes(WhitelistCommand::executeRemove)
)
).then(CommandManager.literal("list")
.executes(ctx -> executeList(ctx.getSource()))
).executes(ctx -> help(ctx.getSource()));
}
private static int help(ServerCommandSource source){
log.sendInfo(source, "可用的命令有: add [player], remove [player], list.");
return 1;
}
private static int executeList(ServerCommandSource source){
StringBuilder builder = new StringBuilder();
builder.append("目前在白名单的有: ");
for(String name : config.get().playerWhitelist){
builder.append(name);
builder.append(", ");
}
log.sendInfo(source, builder.toString());
return 1;
}
private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(config.get().playerWhitelist.contains(player.getEntityName())) {
log.sendInfo(ctx.getSource(), "玩家: {} 已经在白名单列表里.", player.getEntityName());
} else {
config.get().playerWhitelist.add(player.getEntityName());
config.save();
StringBuilder builder = new StringBuilder();
builder.append("玩家: ");
builder.append(player.getEntityName());
builder.append(" 被添加的白名单");
if(config.get().playerBlacklist.contains(player.getEntityName())){
config.get().playerBlacklist.remove(player.getEntityName());
config.save();
builder.append(" 并且被移除黑名单");
}
builder.append(" 成功.");
ctx.getSource().getServer().getCommandManager().sendCommandTree(player);
log.sendInfo(ctx.getSource(), builder.toString());
}
return 1;
}
private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(!config.get().playerWhitelist.contains(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.getEntityName());
}
return 1;
}
}

View File

@ -1,96 +0,0 @@
package net.szum123321.textile_backup.commands.permission;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import io.github.cottonmc.cotton.config.ConfigManager;
import net.minecraft.command.argument.EntityArgumentType;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.szum123321.textile_backup.Statics;
public class BlacklistCommand {
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("blacklist")
.then(CommandManager.literal("add")
.then(CommandManager.argument("player", EntityArgumentType.player())
.executes(BlacklistCommand::executeAdd)
)
).then(CommandManager.literal("remove")
.then(CommandManager.argument("player", EntityArgumentType.player())
.executes(BlacklistCommand::executeRemove)
)
).then(CommandManager.literal("list")
.executes(ctx -> executeList(ctx.getSource()))
).executes(ctx -> help(ctx.getSource()));
}
private static int help(ServerCommandSource source) {
Statics.LOGGER.sendInfo(source, "Available command are: add [player], remove [player], list.");
return 1;
}
private static int executeList(ServerCommandSource source) {
StringBuilder builder = new StringBuilder();
builder.append("Currently on the blacklist are: ");
for(String name : Statics.CONFIG.playerBlacklist){
builder.append(name);
builder.append(", ");
}
Statics.LOGGER.sendInfo(source, builder.toString());
return 1;
}
private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(Statics.CONFIG.playerBlacklist.contains(player.getEntityName())) {
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} is already blacklisted.", player.getEntityName());
} else {
Statics.CONFIG.playerBlacklist.add(player.getEntityName());
ConfigManager.saveConfig(Statics.CONFIG);
StringBuilder builder = new StringBuilder();
builder.append("Player: ");
builder.append(player.getEntityName());
builder.append(" added to the blacklist");
if(Statics.CONFIG.playerWhitelist.contains(player.getEntityName())){
Statics.CONFIG.playerWhitelist.remove(player.getEntityName());
builder.append(" and removed form the whitelist");
}
builder.append(" successfully.");
ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player);
Statics.LOGGER.sendInfo(ctx.getSource(), builder.toString());
}
return 1;
}
private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(!Statics.CONFIG.playerBlacklist.contains(player.getEntityName())) {
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} newer was blacklisted.", player.getEntityName());
} else {
Statics.CONFIG.playerBlacklist.remove(player.getEntityName());
ConfigManager.saveConfig(Statics.CONFIG);
ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player);
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} removed from the blacklist successfully.", player.getEntityName());
}
return 1;
}
}

View File

@ -1,96 +0,0 @@
package net.szum123321.textile_backup.commands.permission;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import io.github.cottonmc.cotton.config.ConfigManager;
import net.minecraft.command.argument.EntityArgumentType;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.szum123321.textile_backup.Statics;
public class WhitelistCommand {
public static LiteralArgumentBuilder<ServerCommandSource> register(){
return CommandManager.literal("whitelist")
.then(CommandManager.literal("add")
.then(CommandManager.argument("player", EntityArgumentType.player())
.executes(WhitelistCommand::executeAdd)
)
).then(CommandManager.literal("remove")
.then(CommandManager.argument("player", EntityArgumentType.player())
.executes(WhitelistCommand::executeRemove)
)
).then(CommandManager.literal("list")
.executes(ctx -> executeList(ctx.getSource()))
).executes(ctx -> help(ctx.getSource()));
}
private static int help(ServerCommandSource source){
Statics.LOGGER.sendInfo(source, "Available command are: add [player], remove [player], list.");
return 1;
}
private static int executeList(ServerCommandSource source){
StringBuilder builder = new StringBuilder();
builder.append("Currently on the whitelist are: ");
for(String name : Statics.CONFIG.playerWhitelist){
builder.append(name);
builder.append(", ");
}
Statics.LOGGER.sendInfo(source, builder.toString());
return 1;
}
private static int executeAdd(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(Statics.CONFIG.playerWhitelist.contains(player.getEntityName())) {
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} is already whitelisted.", player.getEntityName());
} else {
Statics.CONFIG.playerWhitelist.add(player.getEntityName());
ConfigManager.saveConfig(Statics.CONFIG);
StringBuilder builder = new StringBuilder();
builder.append("Player: ");
builder.append(player.getEntityName());
builder.append(" added to the whitelist");
if(Statics.CONFIG.playerBlacklist.contains(player.getEntityName())){
Statics.CONFIG.playerBlacklist.remove(player.getEntityName());
builder.append(" and removed form the blacklist");
}
builder.append(" successfully.");
ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player);
Statics.LOGGER.sendInfo(ctx.getSource(), builder.toString());
}
return 1;
}
private static int executeRemove(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
ServerPlayerEntity player = EntityArgumentType.getPlayer(ctx, "player");
if(!Statics.CONFIG.playerWhitelist.contains(player.getEntityName())) {
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} newer was whitelisted.", player.getEntityName());
} else {
Statics.CONFIG.playerWhitelist.remove(player.getEntityName());
ConfigManager.saveConfig(Statics.CONFIG);
ctx.getSource().getMinecraftServer().getCommandManager().sendCommandTree(player);
Statics.LOGGER.sendInfo(ctx.getSource(), "Player: {} removed from the whitelist successfully.", 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,23 +21,36 @@ package net.szum123321.textile_backup.commands.restore;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.Utilities;
import 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.LOGGER.sendInfo(ctx.getSource(), "Backup restoration successfully stopped");
Statics.LOGGER.info("{} cancelled backup restoration.", ctx.getSource().getEntity() != null ?
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"
);
} else {
Statics.LOGGER.sendInfo(ctx.getSource(), "Failed to stop backup restoration");
}
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
@ -18,40 +18,41 @@
package net.szum123321.textile_backup.commands.restore;
import com.mojang.brigadier.LiteralMessage;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.LiteralText;
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.commands.CommandExceptions;
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 java.io.File;
import javax.annotation.Nullable;
import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class RestoreBackupCommand {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static LiteralArgumentBuilder<ServerCommandSource> register() {
return CommandManager.literal("restore")
.then(CommandManager.argument("file", StringArgumentType.word())
.suggests(new FileSuggestionProvider())
.suggests(FileSuggestionProvider.Instance())
.executes(ctx -> execute(
StringArgumentType.getString(ctx, "file"),
null,
ctx.getSource()
))
).then(CommandManager.argument("file", StringArgumentType.word())
.suggests(new FileSuggestionProvider())
.suggests(FileSuggestionProvider.Instance())
.then(CommandManager.argument("comment", StringArgumentType.word())
.executes(ctx -> execute(
StringArgumentType.getString(ctx, "file"),
@ -62,85 +63,57 @@ public class RestoreBackupCommand {
).executes(context -> {
ServerCommandSource source = context.getSource();
Statics.LOGGER.sendInfo(source, "To restore given backup you have to provide exact creation time in format:");
Statics.LOGGER.sendInfo(source, "[YEAR]-[MONTH]-[DAY]_[HOUR].[MINUTE].[SECOND]");
Statics.LOGGER.sendInfo(source, "Example: 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, String comment, ServerCommandSource source) throws CommandSyntaxException {
private static int execute(String file, @Nullable String comment, ServerCommandSource source) throws CommandSyntaxException {
if(Globals.INSTANCE.getAwaitThread().filter(Thread::isAlive).isPresent()) {
log.sendInfo(source, "已经有其他人开始了另一个恢复操作。");
return -1;
}
LocalDateTime dateTime;
Optional<RestoreableFile> backupFile;
try {
dateTime = LocalDateTime.from(Statics.defaultDateTimeFormatter.parse(file));
} catch (DateTimeParseException e) {
LiteralText message = new LiteralText("An exception occurred while trying to parse:\n");
message.append(e.getParsedString())
.append("\n");
for(int i = 0; i < e.getErrorIndex(); i++)
message.append(" ");
message.append("^");
throw new CommandSyntaxException(new SimpleCommandExceptionType(message), message);
}
Optional<File> backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getMinecraftServer());
if(backupFile.isPresent()) {
Statics.LOGGER.info("Found file to restore {}", backupFile.get().getName());
if(Objects.equals(file, "latest")) {
backupFile = RestoreHelper.getLatestAndLockIfPresent(source.getServer());
dateTime = backupFile.map(RestoreableFile::getCreationTime).orElse(LocalDateTime.now());
} else {
Statics.LOGGER.sendInfo(source, "No file created on {} was found!", dateTime.format(Statics.defaultDateTimeFormatter));
return 0;
try {
dateTime = LocalDateTime.from(Globals.defaultDateTimeFormatter.parse(file));
} catch (DateTimeParseException e) {
throw CommandExceptions.DATE_TIME_PARSE_COMMAND_EXCEPTION_TYPE.create(e);
}
if(Statics.restoreAwaitThread == null || !Statics.restoreAwaitThread.isAlive()) {
if(source.getEntity() != null)
Statics.LOGGER.info("Backup restoration was initiated by: {}", source.getName());
else
Statics.LOGGER.info("Backup restoration was initiated form Server Console");
Statics.restoreAwaitThread = RestoreHelper.create(backupFile.get(), source.getMinecraftServer(), comment);
Statics.restoreAwaitThread.start();
} else if(Statics.restoreAwaitThread != null && Statics.restoreAwaitThread.isAlive()) {
Statics.LOGGER.sendInfo(source, "Someone has already started another restoration.");
return 0;
backupFile = RestoreHelper.findFileAndLockIfPresent(dateTime, source.getServer());
}
if(backupFile.isEmpty()) {
log.sendInfo(source, "在{}上没有找到创建的文件!", dateTime.format(Globals.defaultDateTimeFormatter));
return -1;
} else {
log.info("找到要恢复的文件:{}", backupFile.get().getFile().getFileName().toString());
Globals.INSTANCE.setAwaitThread(
RestoreHelper.create(
RestoreContext.Builder.newRestoreContextBuilder()
.setCommandSource(source)
.setFile(backupFile.get())
.setComment(comment)
.build()
)
);
Globals.INSTANCE.getAwaitThread().get().start();
return 1;
}
private static final class FileSuggestionProvider implements SuggestionProvider<ServerCommandSource> {
@Override
public CompletableFuture<Suggestions> getSuggestions(CommandContext<ServerCommandSource> ctx, SuggestionsBuilder builder) throws CommandSyntaxException {
String remaining = builder.getRemaining();
for(RestoreHelper.RestoreableFile file : RestoreHelper.getAvailableBackups(ctx.getSource().getMinecraftServer())) {
String formattedCreationTime = file.getCreationTime().format(Statics.defaultDateTimeFormatter);
if(formattedCreationTime.startsWith(remaining)) {
if(ctx.getSource().getEntity() != null) { //was typed by player
if(file.getComment() != null) {
builder.suggest(formattedCreationTime, new LiteralMessage("Comment: " + file.getComment()));
} else {
builder.suggest(formattedCreationTime);
}
} else { //was typed from server console
if(file.getComment() != null) {
builder.suggest(file.getCreationTime() + "#" + file.getComment());
} else {
builder.suggest(formattedCreationTime);
}
}
}
}
return builder.buildFuture();
}
}
}

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
@ -16,21 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.create.compressors;
package net.szum123321.textile_backup.config;
import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream;
import me.shedaniel.autoconfig.ConfigHolder;
import java.io.*;
public class ConfigHelper {
public static final ConfigHelper INSTANCE = new ConfigHelper();
private ConfigHolder<ConfigPOJO> configHolder;
public class LZMACompressor extends AbstractTarCompressor {
private static final LZMACompressor INSTANCE = new LZMACompressor();
public static void updateInstance(ConfigHolder<ConfigPOJO> ch) { INSTANCE.configHolder = ch; }
public static LZMACompressor getInstance() {
return INSTANCE;
}
public ConfigPOJO get() { return configHolder.get(); }
@Override
protected OutputStream openCompressorStream(OutputStream outputStream, int coreCountLimit) throws IOException {
return new XZCompressorOutputStream(outputStream);
}
public void save() { configHolder.save(); }
}

View File

@ -0,0 +1,226 @@
/*
* 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.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 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")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Gui.Excluded
public boolean perWorldBackup = true;
@Comment("""
\nTime between automatic backups in seconds
When set to 0 backups will not be performed automatically
""")
@ConfigEntry.Gui.Tooltip()
@ConfigEntry.Category("Create")
public long backupInterval = 3600;
@Comment("\nDelay in seconds between typing-in /backup restore and it actually starting\n")
@ConfigEntry.Gui.Tooltip()
@ConfigEntry.Category("Restore")
public int restoreDelay = 30;
@Comment("\nShould backups be done even if there are no players?\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Create")
public boolean doBackupsOnEmptyServer = false;
@Comment("\nShould backup be made on server shutdown?\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Create")
public boolean shutdownBackup = true;
@Comment("\nShould world be backed up before restoring a backup?\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Restore")
public boolean backupOldWorlds = true;
@Comment("\nA path to the backup folder\n")
@SerializedName("path")
@ConfigEntry.Gui.NoTooltip()
public String backupDirectoryPath = "backup/";
@Comment("""
\nThis setting allows you to exclude files form being backed-up.
Be very careful when setting it, as it is easy corrupt your world!
""")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Create")
public List<String> fileBlacklist = new ArrayList<>();
@Comment("\nShould backups be deleted after being restored?\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Restore")
public boolean deleteOldBackupAfterRestore = true;
@Comment("\nMaximum number of backups to keep. If set to 0 then no backup will be deleted based their amount\n")
@ConfigEntry.Gui.NoTooltip()
public int backupsToKeep = 10;
@Comment("\nMaximum age of backups to keep in seconds.\n If set to 0 then backups will not be deleted based their age \n")
@ConfigEntry.Gui.NoTooltip()
public long maxAge = 0;
@Comment("""
\nMaximum size of backup folder in kibi bytes (1024).
If set to 0 then backups will not be deleted
""")
@ConfigEntry.Gui.Tooltip()
public long maxSize = 0;
@Comment("\nCompression level \n0 - 9\n Only affects zip compression.\n")
@ConfigEntry.Gui.Tooltip()
@ConfigEntry.BoundedDiscrete(max = 9)
@ConfigEntry.Category("Create")
public int compression = 7;
@Comment("""
\nLimit how many cores can be used for compression.
0 means that all available cores will be used
""")
@ConfigEntry.Gui.Tooltip()
@ConfigEntry.Category("Create")
public int compressionCoreCountLimit = 0;
@Comment("""
\nAvailable formats are:
ZIP - normal zip archive using standard deflate compression
GZIP - tar.gz using gzip compression
TAR - .tar with no compression
""")
@ConfigEntry.Gui.Tooltip()
@ConfigEntry.Category("Create")
@ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
public ArchiveFormat format = ArchiveFormat.ZIP;
@Comment("\nMinimal permission level required to run commands\n")
@ConfigEntry.Category("Manage")
@ConfigEntry.Gui.NoTooltip()
public int permissionLevel = 4;
@Comment("\nPlayer on singleplayer is always allowed to run command. Warning! On lan party everyone will be allowed to run it.\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Manage")
public boolean alwaysSingleplayerAllowed = true;
@Comment("\nPlayers allowed to run backup commands without sufficient permission level\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Manage")
public List<String> playerWhitelist = new ArrayList<>();
@Comment("\nPlayers banned from running backup commands besides their sufficient permission level\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Manage")
public List<String> playerBlacklist = new ArrayList<>();
@Comment("\nAnnounce to ALL players when backup starts\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Manage")
public boolean broadcastBackupStart = true;
@Comment("\nAnnounce to ALL players when backup finishes\n")
@ConfigEntry.Gui.NoTooltip()
@ConfigEntry.Category("Manage")
public boolean broadcastBackupDone = true;
@Comment("""
\nFormat of date&time used to name backup files.
Remember not to use '#' symbol or any other character that is not allowed by your operating system such as:
':', '\\', etc...
For more info: https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html
""")
@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())
throw new ValidationException("compressionCoreCountLimit is too high! Your system only has: " + Runtime.getRuntime().availableProcessors() + " cores!");
try {
DateTimeFormatter.ofPattern(dateTimeFormat);
} catch (IllegalArgumentException e) {
throw new ValidationException(
"dateTimeFormat is wrong! See: https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html",
e
);
}
}
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"),
BZIP2("tar", "bz2"),
LZMA("tar", "xz"),
TAR("tar");
private final List<String> extensionPieces;
ArchiveFormat(String... extensionParts) {
extensionPieces = Arrays.asList(extensionParts);
}
public String getCompleteString() {
StringBuilder builder = new StringBuilder();
extensionPieces.forEach(s -> builder.append('.').append(s));
return builder.toString();
}
boolean isMultipart() {
return extensionPieces.size() > 1;
}
public String getLastPiece() {
return extensionPieces.get(extensionPieces.size() - 1);
}
}
}

View File

@ -0,0 +1,45 @@
/*
* 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;
/**
* Enum representing possible sources of action
*/
public enum ActionInitiator {
Player("Player", "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");
private final String name;
private final String prefix;
ActionInitiator(String name, String prefix) {
this.name = name;
this.prefix = prefix;
}
public String getName() {
return name;
}
public String getPrefix() {
return 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,136 +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;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.LiteralText;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.szum123321.textile_backup.core.create.BackupContext;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.MessageFactory;
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
import org.apache.logging.log4j.spi.StandardLevel;
/*
This is practically just a copy-pate of Cotton's ModLogger with few changes
*/
public class CustomLogger {
private final boolean isDev = FabricLoader.getInstance().isDevelopmentEnvironment();
private final MessageFactory messageFactory;
private final Logger logger;
private final String prefix;
private final Text prefixText;
public CustomLogger(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 MutableText getPrefixText() {
return prefixText.shallowCopy();
}
public void log(Level level, String msg, Object... data) {
logger.log(level, prefix + msg, data);
}
public void trace(String msg, Object... data) {
log(Level.TRACE, msg, data);
}
public void debug(String msg, Object... data) {
log(Level.DEBUG, msg, data);
}
public void info(String msg, Object... data) {
log(Level.INFO, msg, data);
}
public void warn(String msg, Object... data) {
log(Level.WARN, msg, data);
}
public void error(String msg, Object... data) {
log(Level.ERROR, msg, data);
}
public void fatal(String msg, Object... data) {
log(Level.FATAL, msg, data);
}
public void devError(String msg, Object... data) {
if (isDev) error(msg, data);
}
public void devWarn(String msg, Object... data) {
if (isDev) warn(msg, data);
}
public void devInfo(String msg, Object... data) {
if (isDev) info(msg, data);
}
public void devDebug(String msg, Object... data) {
if (isDev) debug(msg, data);
}
public void devTrace(String msg, Object... data) {
if(isDev) trace(msg, data);
}
private void sendToPlayer(Level level, ServerCommandSource source, String msg, Object... args) {
if(source != null && source.getEntity() != null) {
LiteralText text = new LiteralText(messageFactory.newMessage(msg, args).getFormattedMessage());
if(level.intLevel() <= StandardLevel.WARN.intLevel())
text.formatted(Formatting.RED);
else
text.formatted(Formatting.WHITE);
source.sendFeedback(prefixText.shallowCopy().append(text), false);
} else {
logger.log(level, msg, args);
}
}
public void sendInfo(ServerCommandSource source, String msg, Object... args) {
sendToPlayer(Level.INFO, source, msg, args);
}
public void sendError(ServerCommandSource source, String msg, Object... args) {
sendToPlayer(Level.ERROR, source, msg, args);
}
public void sendInfo(BackupContext context, String msg, Object... args) {
sendInfo(context.getCommandSource(), msg, args);
}
public void sendError(BackupContext context, String msg, Object... args) {
sendError(context.getCommandSource(), msg, args);
}
}

View File

@ -0,0 +1,25 @@
/*
* 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 java.io.IOException;
public class DataLeftException extends IOException {
public DataLeftException(long n) { super("Input stream closed with " + n + " bytes left!"); }
}

View File

@ -1,5 +0,0 @@
package net.szum123321.textile_backup.core;
public interface LivingServer {
boolean isAlive();
}

View File

@ -0,0 +1,30 @@
/*
* 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 java.io.IOException;
/**
* Wrapper for specific IOException. Temporary way to get more info about issue #51
*/
public class NoSpaceLeftOnDeviceException extends IOException {
public NoSpaceLeftOnDeviceException(Throwable 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,185 +1,137 @@
/*
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.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.registry.Registry;
import net.minecraft.util.registry.RegistryKey;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.world.World;
import net.minecraft.world.dimension.DimensionType;
import net.szum123321.textile_backup.ConfigHandler;
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.mixin.MinecraftServerSessionAccessor;
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.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
public class Utilities {
private final static ConfigHelper config = ConfigHelper.INSTANCE;
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
//I'm keeping this wrapper function for easier backporting
public static boolean wasSentByPlayer(ServerCommandSource source) { return source.isExecutedByPlayer(); }
public static void notifyPlayers(@NotNull MinecraftServer server, String msg) {
MutableText message = log.getPrefixText();
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);
}
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;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
public static void disableWorldSaving(MinecraftServer server) {
for (ServerWorld serverWorld : server.getWorlds()) {
if (serverWorld != null && !serverWorld.savingDisabled) {
if (serverWorld != null && !serverWorld.savingDisabled)
serverWorld.savingDisabled = true;
}
}
}
public static void enableWorldSaving(MinecraftServer server) {
for (ServerWorld serverWorld : server.getWorlds()) {
if (serverWorld != null && serverWorld.savingDisabled) {
if (serverWorld != null && serverWorld.savingDisabled)
serverWorld.savingDisabled = false;
}
}
}
public static boolean isWindows() {
return System.getProperty("os.name").toLowerCase().contains("win");
}
public static boolean isBlacklisted(Path path) {
if(isWindows()) { //hotfix!
if (path.getFileName().toString().equals("session.lock")) {
Statics.LOGGER.trace("Skipping session.lock");
return true;
}
}
public static Path getBackupRootPath(String worldName) {
Path path = Path.of(config.get().backupDirectoryPath).toAbsolutePath();
for(String i : Statics.CONFIG.fileBlacklist) {
if(path.startsWith(i))
return true;
}
return false;
}
public static Optional<ConfigHandler.ArchiveFormat> getFileExtension(String fileName) {
String[] parts = fileName.split("\\.");
switch (parts[parts.length - 1]) {
case "zip":
return Optional.of(ConfigHandler.ArchiveFormat.ZIP);
case "bz2":
return Optional.of(ConfigHandler.ArchiveFormat.BZIP2);
case "gz":
return Optional.of(ConfigHandler.ArchiveFormat.GZIP);
case "xz":
return Optional.of(ConfigHandler.ArchiveFormat.LZMA);
default:
return Optional.empty();
}
}
public static Optional<ConfigHandler.ArchiveFormat> getFileExtension(File f) {
return getFileExtension(f.getName());
}
public static Optional<LocalDateTime> getFileCreationTime(File file) {
LocalDateTime creationTime = null;
if(getFileExtension(file).isPresent()) {
String fileExtension = getFileExtension(file).get().getString();
if (config.get().perWorldBackup) path = path.resolve(worldName);
if(Files.notExists(path)) {
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){}
Files.createDirectories(path);
} catch (IOException e) {
//I REALLY shouldn't be handling this here
}
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 File getBackupRootPath(String worldName) {
File path = new File(Statics.CONFIG.path).getAbsoluteFile();
if (Statics.CONFIG.perWorldBackup)
path = path.toPath().resolve(worldName).toFile();
if (!path.exists()) {
path.mkdirs();
}
return path;
}
public static boolean isValidBackup(File f) {
return getFileExtension(f).isPresent() && getFileCreationTime(f).isPresent() && isFileOk(f);
}
public static boolean isBlacklisted(Path path) {
if (path.getFileName().equals(Path.of("session.lock"))) return true;
public static boolean isFileOk(File f) {return f.exists() && f.isFile(); }
if(path.getFileName().equals(Path.of(CompressionStatus.DATA_FILENAME))) return true;
return config.get().fileBlacklist.stream().anyMatch(path::startsWith);
}
public static DateTimeFormatter getDateTimeFormatter() {
return DateTimeFormatter.ofPattern(Statics.CONFIG.dateTimeFormat);
}
public static DateTimeFormatter getBackupDateTimeFormatter() {
return Statics.defaultDateTimeFormatter;
return DateTimeFormatter.ofPattern(config.get().dateTimeFormat);
}
public static String formatDuration(Duration duration) {
DateTimeFormatter formatter;
if(duration.toHours() > 0)
formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
else if(duration.toMinutes() > 0)
formatter = DateTimeFormatter.ofPattern("mm:ss.SSS");
else
formatter = DateTimeFormatter.ofPattern("ss.SSS");
if(duration.toHours() > 0) formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
else if(duration.toMinutes() > 0) formatter = DateTimeFormatter.ofPattern("mm:ss.SSS");
else formatter = DateTimeFormatter.ofPattern("ss.SSS");
return LocalTime.ofNanoOfDay(duration.toNanos()).format(formatter);
}

View File

@ -1,155 +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.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import org.jetbrains.annotations.NotNull;
public class BackupContext {
private final MinecraftServer server;
private final ServerCommandSource commandSource;
private final BackupInitiator initiator;
private final boolean save;
private final String comment;
protected BackupContext(@NotNull MinecraftServer server, ServerCommandSource commandSource, @NotNull BackupInitiator initiator, boolean save, String comment) {
this.server = server;
this.commandSource = commandSource;
this.initiator = initiator;
this.save = save;
this.comment = comment;
}
public MinecraftServer getServer() {
return server;
}
public ServerCommandSource getCommandSource() {
return commandSource;
}
public BackupInitiator getInitiator() {
return initiator;
}
public boolean startedByPlayer() {
return initiator == BackupInitiator.Player;
}
public boolean shouldSave() {
return save;
}
public String getComment() {
return comment;
}
public static class Builder {
private MinecraftServer server;
private ServerCommandSource commandSource;
private BackupInitiator 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 Builder setCommandSource(ServerCommandSource commandSource) {
this.commandSource = commandSource;
return this;
}
public Builder setServer(MinecraftServer server) {
this.server = server;
return this;
}
public Builder setInitiator(BackupInitiator 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() == null ? BackupInitiator.ServerConsole : BackupInitiator.Player;
} else if(initiator == null) {
initiator = BackupInitiator.Null;
}
if(server == null) {
if(commandSource != null)
setServer(commandSource.getMinecraftServer());
else
throw new RuntimeException("Both MinecraftServer and ServerCommandSource weren't provided!");
}
return new BackupContext(server, commandSource, initiator, save, comment);
}
}
public enum BackupInitiator {
Player ("Player", "by"),
ServerConsole ("Server Console", "from"),
Timer ("Timer", "by"),
Shutdown ("Server Shutdown", "by"),
Restore ("Backup Restoration", "because of"),
Null ("Null (That shouldn't have happened)", "form");
private final String name;
private final String prefix;
BackupInitiator(String name, String prefix) {
this.name = name;
this.prefix = prefix;
}
public String getName() {
return name;
}
public String getPrefix() {
return prefix + ": ";
}
}
}

View File

@ -1,127 +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.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;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicInteger;
public class BackupHelper {
public static Runnable create(BackupContext ctx) {
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()));
Statics.LOGGER.info(builder.toString());
if (ctx.shouldSave()) {
Statics.LOGGER.sendInfo(ctx.getCommandSource(), "Saving server...");
Statics.LOGGER.info( "Saving server...");
ctx.getServer().save(true, true, true);
Utilities.disableWorldSaving(ctx.getServer());
}
return new MakeBackupRunnable(ctx);
}
public static int executeFileLimit(ServerCommandSource ctx, String worldName) {
File root = Utilities.getBackupRootPath(worldName);
AtomicInteger deletedFiles = new AtomicInteger();
if (root.isDirectory() && root.exists() && root.listFiles() != null) {
if (Statics.CONFIG.maxAge > 0) { // delete files older that configured
final LocalDateTime now = LocalDateTime.now();
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) > Statics.CONFIG.maxAge)
.forEach(f -> {
if(deleteFile(f, ctx))
deletedFiles.getAndIncrement();
});
}
if (Statics.CONFIG.backupsToKeep > 0 && root.listFiles().length > Statics.CONFIG.backupsToKeep) {
int i = root.listFiles().length;
Iterator<File> it = Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get()))
.iterator();
while(i > Statics.CONFIG.backupsToKeep && it.hasNext()) {
if(deleteFile(it.next(), ctx))
deletedFiles.getAndIncrement();
i--;
}
}
if (Statics.CONFIG.maxSize > 0 && FileUtils.sizeOfDirectory(root) / 1024 > Statics.CONFIG.maxSize) {
Iterator<File> it = Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)
.sorted(Comparator.comparing(f -> Utilities.getFileCreationTime(f).get()))
.iterator();
while(FileUtils.sizeOfDirectory(root) / 1024 > Statics.CONFIG.maxSize && it.hasNext()) {
if(deleteFile(it.next(), ctx))
deletedFiles.getAndIncrement();
}
}
}
return deletedFiles.get();
}
private static boolean deleteFile(File f, ServerCommandSource ctx) {
if(f != Statics.untouchableFile) {
if(f.delete()) {
Statics.LOGGER.sendInfo(ctx, "Deleting: {}", f.getName());
Statics.LOGGER.info("Deleting: {}", f.getName());
return true;
} else {
Statics.LOGGER.sendError(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,51 +19,69 @@
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 boolean scheduled;
private long nextBackup;
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public BackupScheduler() {
scheduled = false;
nextBackup = -1;
}
//Scheduled flag tells whether we have decided to run another backup
private static boolean scheduled = false;
private static long 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(Statics.CONFIG.doBackupsOnEmptyServer || server.getPlayerManager().getCurrentPlayerCount() > 0) {
if(config.get().doBackupsOnEmptyServer || server.getPlayerManager().getCurrentPlayerCount() > 0) {
//Either just run backup with no one playing or there's at least one player
if(scheduled) {
if(nextBackup <= now) {
Statics.executorService.submit(
BackupHelper.create(
new BackupContext.Builder()
//It's time to run
Globals.INSTANCE.getQueueExecutor().submit(
ExecutableBackup.Builder
.newBackupContextBuilder()
.setServer(server)
.setInitiator(BackupContext.BackupInitiator.Timer)
.setInitiator(ActionInitiator.Timer)
.saveServer()
.announce()
.build()
)
);
nextBackup = now + Statics.CONFIG.backupInterval;
nextBackup = now + config.get().backupInterval;
}
} else {
nextBackup = now + Statics.CONFIG.backupInterval;
//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(!Statics.CONFIG.doBackupsOnEmptyServer && server.getPlayerManager().getCurrentPlayerCount() == 0) {
} else if(!config.get().doBackupsOnEmptyServer && server.getPlayerManager().getCurrentPlayerCount() == 0) {
//Do the final backup. No one's on-line and doBackupsOnEmptyServer == false
if(scheduled && nextBackup <= now) {
Statics.executorService.submit(
BackupHelper.create(
new BackupContext.Builder()
//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(BackupContext.BackupInitiator.Timer)
.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,116 +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.core.create.compressors.*;
import net.szum123321.textile_backup.core.Utilities;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
public class MakeBackupRunnable implements Runnable {
private final BackupContext context;
public MakeBackupRunnable(BackupContext context){
this.context = context;
}
@Override
public void run() {
try {
Statics.LOGGER.sendInfo(context.getCommandSource(), "Starting backup");
Statics.LOGGER.info("Starting backup");
File world = Utilities.getWorldFolder(context.getServer());
Statics.LOGGER.trace("Minecraft world is: {}", world);
File outFile = Utilities
.getBackupRootPath(Utilities.getLevelName(context.getServer()))
.toPath()
.resolve(getFileName())
.toFile();
Statics.LOGGER.trace("Outfile is: {}", outFile);
outFile.getParentFile().mkdirs();
try {
outFile.createNewFile();
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred when trying to create new backup file!", e);
Statics.LOGGER.sendError(context.getCommandSource(), "An exception occurred when trying to create new backup file!");
return;
}
int coreCount;
if(Statics.CONFIG.compressionCoreCountLimit <= 0) {
coreCount = Runtime.getRuntime().availableProcessors();
} else {
coreCount = Math.min(Statics.CONFIG.compressionCoreCountLimit, Runtime.getRuntime().availableProcessors());
}
Statics.LOGGER.trace("Running compression on {} threads. Available cores = {}", coreCount, Runtime.getRuntime().availableProcessors());
switch (Statics.CONFIG.format) {
case ZIP:
ParallelZipCompressor.createArchive(world, outFile, context, coreCount);
break;
case BZIP2:
ParallelBZip2Compressor.getInstance().createArchive(world, outFile, context, coreCount);
break;
case GZIP:
ParallelGzipCompressor.getInstance().createArchive(world, outFile, context, coreCount);
break;
case LZMA:
LZMACompressor.getInstance().createArchive(world, outFile, context, coreCount);
break;
default:
Statics.LOGGER.warn("Specified compressor ({}) is not supported! Zip will be used instead!", Statics.CONFIG.format);
Statics.LOGGER.sendError(context.getCommandSource(), "Error! No correct compression format specified! Using default compressor!");
ParallelZipCompressor.createArchive(world, outFile, context, coreCount);
break;
}
BackupHelper.executeFileLimit(context.getCommandSource(), Utilities.getLevelName(context.getServer()));
Statics.LOGGER.sendInfo(context, "Done!");
Statics.LOGGER.info("Done!");
} finally {
Utilities.enableWorldSaving(context.getServer());
}
}
private String getFileName(){
LocalDateTime now = LocalDateTime.now();
return Utilities.getDateTimeFormatter().format(now) +
(context.getComment() != null ? "#" + context.getComment().replace("#", "") : "") +
Statics.CONFIG.format.getString();
}
}

View File

@ -0,0 +1,131 @@
/*
* 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.compressors;
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.*;
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(Path inputFile, Path outputFile, ExecutableBackup ctx, int coreLimit) throws IOException, ExecutionException, InterruptedException {
Instant start = Instant.now();
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);
Stream<Path> fileStream = Files.walk(inputFile)) {
var fileList = fileStream
.filter(path -> !Utilities.isBlacklisted(inputFile.relativize(path)))
.filter(Files::isRegularFile)
.toList();
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);
} finally {
close();
}
log.sendInfoAL(ctx, "压缩耗时:{}秒. ", Utilities.formatDuration(Duration.between(start, Instant.now())));
}
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 {
//This function is only needed for the ParallelZipCompressor to write out ParallelScatterZipCreator
}
protected void close() {
//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,73 +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.compressors;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.create.BackupContext;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
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 abstract class AbstractTarCompressor {
protected abstract OutputStream openCompressorStream(OutputStream outputStream, int coreCountLimit) throws IOException;
public void createArchive(File inputFile, File outputFile, BackupContext ctx, int coreLimit) {
Statics.LOGGER.sendInfo(ctx, "Starting compression...");
Instant start = Instant.now();
try (FileOutputStream outStream = new FileOutputStream(outputFile);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outStream);
OutputStream compressorOutputStream = openCompressorStream(bufferedOutputStream, coreLimit);
TarArchiveOutputStream arc = new TarArchiveOutputStream(compressorOutputStream)) {
arc.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
arc.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
Files.walk(inputFile.toPath())
.filter(path -> !Utilities.isBlacklisted(inputFile.toPath().relativize(path)))
.map(Path::toFile)
.filter(File::isFile)
.forEach(file -> {
try (FileInputStream fileInputStream = new FileInputStream(file)){
ArchiveEntry entry = arc.createArchiveEntry(file, inputFile.toPath().relativize(file.toPath()).toString());
arc.putArchiveEntry(entry);
IOUtils.copy(fileInputStream, arc);
arc.closeArchiveEntry();
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred while trying to compress: {}", file.getName(), e);
Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!");
}
});
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred!", e);
Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!");
}
Statics.LOGGER.sendInfo(ctx, "Compression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
}
}

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,18 +18,17 @@
package net.szum123321.textile_backup.core.create.compressors;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.core.Utilities;
import net.szum123321.textile_backup.core.create.BackupContext;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.core.NoSpaceLeftOnDeviceException;
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.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.zip.ZipEntry;
@ -39,66 +38,90 @@ import java.util.zip.ZipEntry;
answer by:
https://stackoverflow.com/users/2987755/dkb
*/
public class ParallelZipCompressor {
public static void createArchive(File inputFile, File outputFile, BackupContext ctx, int coreLimit) {
Statics.LOGGER.sendInfo(ctx, "Starting compression...");
public class ParallelZipCompressor extends ZipCompressor {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
Instant start = Instant.now();
//These fields are used to discriminate against the issue #51
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),
new SimpleStackTraceElement("sun.nio.ch.IOUtil", "write", false),
new SimpleStackTraceElement("sun.nio.ch.FileChannelImpl", "write", false),
new SimpleStackTraceElement("java.nio.channels.Channels", "writeFullyImpl", false),
new SimpleStackTraceElement("java.nio.channels.Channels", "writeFully", false),
new SimpleStackTraceElement("java.nio.channels.Channels$1", "write", false),
new SimpleStackTraceElement("org.apache.commons.compress.parallel.FileBasedScatterGatherBackingStore", "writeOut", false)
};
Path rootPath = inputFile.toPath();
private ParallelScatterZipCreator scatterZipCreator;
try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
ZipArchiveOutputStream arc = new ZipArchiveOutputStream(bufferedOutputStream)) {
ParallelScatterZipCreator scatterZipCreator = new ParallelScatterZipCreator(Executors.newFixedThreadPool(coreLimit));
arc.setMethod(ZipArchiveOutputStream.DEFLATED);
arc.setUseZip64(Zip64Mode.AsNeeded);
arc.setLevel(Statics.CONFIG.compression);
arc.setComment("Created on: " + Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
Files.walk(inputFile.toPath())
.filter(path -> !Utilities.isBlacklisted(inputFile.toPath().relativize(path)))
.map(Path::toFile)
.filter(File::isFile)
.forEach(file -> {
try { //IOException gets thrown only when arc is closed
ZipArchiveEntry entry = (ZipArchiveEntry)arc.createArchiveEntry(file, rootPath.relativize(file.toPath()).toString());
entry.setMethod(ZipEntry.DEFLATED);
scatterZipCreator.addArchiveEntry(entry, new FileInputStreamSupplier(file));
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred while trying to compress: {}", file.getName(), e);
Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!");
}
});
scatterZipCreator.writeTo(arc);
} catch (IOException | InterruptedException | ExecutionException e) {
Statics.LOGGER.error("An exception occurred!", e);
Statics.LOGGER.sendError(ctx, "Something went wrong while compressing files!");
public static ParallelZipCompressor getInstance() {
return new ParallelZipCompressor();
}
Statics.LOGGER.sendInfo(ctx, "Compression took: {} seconds.", Utilities.formatDuration(Duration.between(start, Instant.now())));
@Override
protected OutputStream createArchiveOutputStream(OutputStream stream, ExecutableBackup ctx, int coreLimit) {
scatterZipCreator = new ParallelScatterZipCreator(Executors.newFixedThreadPool(coreLimit));
return super.createArchiveOutputStream(stream, ctx, coreLimit);
}
static class FileInputStreamSupplier implements InputStreamSupplier {
private final File sourceFile;
private InputStream stream;
FileInputStreamSupplier(File sourceFile) {
this.sourceFile = sourceFile;
@Override
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);
}
public InputStream get() {
entry.setTime(System.currentTimeMillis());
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 (#20 and #80)
*/
try {
stream = new BufferedInputStream(new FileInputStream(sourceFile));
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred while trying to create input stream!", e);
scatterZipCreator.writeTo((ZipArchiveOutputStream) arc);
} catch (ExecutionException e) {
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_NO_SPACE_ON_LEFT_ON_DEVICE.length);
if(match) {
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'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);
}
}
return stream;
throw e;
}
}
private record SimpleStackTraceElement (
String className,
String methodName,
boolean isNative
) {
public boolean matches(StackTraceElement o) {
return (isNative == o.isNativeMethod()) && Objects.equals(className, o.getClassName()) && Objects.equals(methodName, o.getMethodName());
}
}
}

View File

@ -0,0 +1,104 @@
/*
* 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.compressors;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.Utilities;
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;
public static ZipCompressor getInstance() {
return new ZipCompressor();
}
@Override
protected OutputStream createArchiveOutputStream(OutputStream stream, ExecutableBackup ctx, int coreLimit) {
ZipArchiveOutputStream arc = new ZipArchiveOutputStream(stream);
arc.setMethod(ZipArchiveOutputStream.DEFLATED);
arc.setUseZip64(Zip64Mode.AsNeeded);
arc.setLevel(config.get().compression);
arc.setComment("Created on: " + Utilities.getDateTimeFormatter().format(LocalDateTime.now()));
return arc;
}
@Override
protected void addEntry(InputSupplier input, OutputStream arc) throws IOException {
try (InputStream fileInputStream = input.getInputStream()) {
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 (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);
IOUtils.copy(fileInputStream, arc);
((ZipArchiveOutputStream)arc).closeArchiveEntry();
}
}
//*.dat files are already compressed with gzip which uses the same algorithm as zip so there's no point in compressing it again
protected static boolean isDotDat(String filename) {
String[] arr = filename.split("\\.");
return arr[arr.length - 1].contains("dat"); //includes dat_old
}
protected static long getCRC(Path file) throws IOException {
Checksum sum = new CRC32();
byte[] buffer = new byte[8192];
int len;
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.toAbsolutePath(), e);
}
return sum.getValue();
}
}

View File

@ -0,0 +1,61 @@
/*
* 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.compressors.tar;
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.*;
public class AbstractTarArchiver extends AbstractCompressor {
protected OutputStream getCompressorOutputStream(OutputStream stream, ExecutableBackup ctx, int coreLimit) throws IOException {
return stream;
}
@Override
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);
return tar;
}
@Override
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);
((TarArchiveOutputStream)arc).closeArchiveEntry();
}
}
}

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
@ -16,22 +16,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.create.compressors;
package net.szum123321.textile_backup.core.create.compressors.tar;
import net.szum123321.textile_backup.core.create.ExecutableBackup;
import org.at4j.comp.bzip2.BZip2OutputStream;
import org.at4j.comp.bzip2.BZip2OutputStreamSettings;
import java.io.*;
public class ParallelBZip2Compressor extends AbstractTarCompressor {
private static final ParallelBZip2Compressor INSTANCE = new ParallelBZip2Compressor();
public class ParallelBZip2Compressor extends AbstractTarArchiver {
public static ParallelBZip2Compressor getInstance() {
return INSTANCE;
return new ParallelBZip2Compressor();
}
@Override
protected OutputStream openCompressorStream(OutputStream outputStream, int coreCountLimit) throws IOException {
return new BZip2OutputStream(outputStream, new BZip2OutputStreamSettings().setNumberOfEncoderThreads(coreCountLimit));
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
@ -16,22 +16,32 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.szum123321.textile_backup.core.create.compressors;
package net.szum123321.textile_backup.core.create.compressors.tar;
import net.szum123321.textile_backup.core.create.ExecutableBackup;
import org.anarres.parallelgzip.ParallelGZIPOutputStream;
import java.io.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ParallelGzipCompressor extends AbstractTarCompressor {
private static final ParallelGzipCompressor INSTANCE = new ParallelGzipCompressor();
public class ParallelGzipCompressor extends AbstractTarArchiver {
private ExecutorService executorService;
public static ParallelGzipCompressor getInstance() {
return INSTANCE;
return new ParallelGzipCompressor();
}
@Override
protected OutputStream openCompressorStream(OutputStream outputStream, int coreCountLimit) throws IOException {
return new ParallelGZIPOutputStream(outputStream, Executors.newFixedThreadPool(coreCountLimit));
protected OutputStream getCompressorOutputStream(OutputStream stream, ExecutableBackup ctx, int coreLimit) throws IOException {
executorService = Executors.newFixedThreadPool(coreLimit);
return new ParallelGZIPOutputStream(stream, executorService);
}
@Override
protected void close() {
//it seems like ParallelGZIPOutputStream doesn't shut down its ExecutorService, so to not leave garbage I shut it down
executorService.shutdown();
}
}

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
@ -18,29 +18,37 @@
package net.szum123321.textile_backup.core.restore;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import java.util.concurrent.atomic.AtomicInteger;
/*
This thread waits some amount of time and then starts a new, independent thread
*/
public class AwaitThread extends Thread {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static AtomicInteger threadCounter = new AtomicInteger(0);
private final int delay;
private final int thisThreadId = threadCounter.getAndIncrement();
private final Runnable taskRunnable;
public AwaitThread(int delay, Runnable taskRunnable) {
this.setName("Textile Backup await thread nr. " + thisThreadId);
this.delay = delay;
this.taskRunnable = taskRunnable;
}
@Override
public void run() {
Statics.LOGGER.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 * 1000);
Thread.sleep(delay * 1000L);
} catch (InterruptedException e) {
Statics.LOGGER.info("Backup restoration cancelled.");
log.info("备份恢复已取消.");
return;
}
@ -49,6 +57,6 @@ public class AwaitThread extends Thread {
But still it's farewell
And maybe we'll come back
*/
new Thread(taskRunnable).start();
new Thread(taskRunnable, "Textile Backup restore thread nr. " + thisThreadId).start();
}
}

View File

@ -1,107 +1,158 @@
/*
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.ConfigHandler;
import net.szum123321.textile_backup.core.LivingServer;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.config.ConfigPOJO;
import net.szum123321.textile_backup.core.ActionInitiator;
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.util.NoSuchElementException;
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 MinecraftServer server;
private final File backupFile;
private final String finalBackupComment;
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
public RestoreBackupRunnable(MinecraftServer server, File backupFile, String finalBackupComment) {
this.server = server;
this.backupFile = backupFile;
this.finalBackupComment = finalBackupComment;
private final RestoreContext ctx;
public RestoreBackupRunnable(RestoreContext ctx) {
this.ctx = ctx;
}
@Override
public void run() {
Statics.LOGGER.info("Shutting down server...");
server.stop(false);
awaitServerShutdown();
Globals.INSTANCE.globalShutdownBackupFlag.set(false);
if(Statics.CONFIG.backupOldWorlds) {
BackupHelper.create(
new BackupContext.Builder()
.setServer(server)
.setInitiator(BackupContext.BackupInitiator.Restore)
.setComment("Old_World" + (finalBackupComment != null ? "_" + finalBackupComment : ""))
.build()
).run();
}
log.info("关闭服务器...");
File worldFile = Utilities.getWorldFolder(server);
ctx.server().stop(false);
Statics.LOGGER.info("Deleting old world...");
if(!deleteDirectory(worldFile))
Statics.LOGGER.error("Something went wrong while deleting old world!");
Path worldFile = Utilities.getWorldFolder(ctx.server()),
tmp;
worldFile.mkdirs();
Statics.LOGGER.info("Starting decompression...");
if(Utilities.getFileExtension(backupFile).orElseThrow(() -> new NoSuchElementException("Couldn't get file extension!")) == ConfigHandler.ArchiveFormat.ZIP) {
ZipDecompressor.decompress(backupFile, worldFile);
} else {
GenericTarDecompressor.decompress(backupFile, worldFile);
}
if(Statics.CONFIG.deleteOldBackupAfterRestore) {
Statics.LOGGER.info("Deleting old backup");
if(!backupFile.delete())
Statics.LOGGER.info("Something went wrong while deleting old backup");
}
Statics.LOGGER.info("Done!");
}
private void awaitServerShutdown() {
while(((LivingServer)server).isAlive()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Statics.LOGGER.error("Exception occurred!", e);
tmp = Files.createTempDirectory(
ctx.server().getRunDirectory().toPath(),
ctx.restoreableFile().getFile().getFileName().toString()
);
} catch (IOException e) {
log.error("在解压备份时发生了异常.", e);
return;
}
//By making a separate thread we can start unpacking an old backup instantly
//Let the server shut down gracefully, and wait for the old world backup to complete
FutureTask<Void> waitForShutdown = new FutureTask<>(() -> {
ctx.server().getThread().join(); //wait for server thread to die and save all its state
if(config.get().backupOldWorlds) {
return ExecutableBackup.Builder
.newBackupContextBuilder()
.setServer(ctx.server())
.setInitiator(ActionInitiator.Restore)
.noCleanup()
.setComment("Old_World" + (ctx.comment() != null ? "_" + ctx.comment() : ""))
.announce()
.build().call();
}
return null;
});
//run the thread.
new Thread(waitForShutdown, "Server shutdown wait thread").start();
try {
log.info("开始解压...");
long hash;
if (ctx.restoreableFile().getArchiveFormat() == ConfigPOJO.ArchiveFormat.ZIP)
hash = ZipDecompressor.decompress(ctx.restoreableFile().getFile(), tmp);
else
hash = GenericTarDecompressor.decompress(ctx.restoreableFile().getFile(), tmp);
log.info("等待服务器完全终止...");
//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) {}
}
}
private static boolean deleteDirectory(File f) {
boolean state = true;
//in case we're playing on client
Globals.INSTANCE.globalShutdownBackupFlag.set(true);
if(f.isDirectory()) {
for(File f2 : f.listFiles())
state &= deleteDirectory(f2);
}
return f.delete() && state;
log.info("完成!");
}
}

View File

@ -0,0 +1,75 @@
/*
* 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.entity.player.PlayerEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.ServerCommandSource;
import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.RestoreableFile;
import javax.annotation.Nullable;
public record RestoreContext(RestoreableFile restoreableFile,
MinecraftServer server,
@Nullable String comment,
ActionInitiator initiator,
ServerCommandSource commandSource) {
public static final class Builder {
private RestoreableFile file;
private MinecraftServer server;
private String comment;
private ServerCommandSource serverCommandSource;
private Builder() {
}
public static Builder newRestoreContextBuilder() {
return new Builder();
}
public Builder setFile(RestoreableFile file) {
this.file = file;
return this;
}
public Builder setServer(MinecraftServer server) {
this.server = server;
return this;
}
public Builder setComment(@Nullable String comment) {
this.comment = comment;
return this;
}
public Builder setCommandSource(ServerCommandSource commandSource) {
this.serverCommandSource = commandSource;
return this;
}
public RestoreContext build() {
if (server == null) server = serverCommandSource.getServer();
ActionInitiator initiator = serverCommandSource.getEntity() instanceof PlayerEntity ? ActionInitiator.Player : ActionInitiator.ServerConsole;
return new RestoreContext(file, server, comment, initiator, serverCommandSource);
}
}
}

View File

@ -1,105 +1,87 @@
/*
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.network.MessageType;
import net.minecraft.server.MinecraftServer;
import net.minecraft.text.LiteralText;
import net.minecraft.text.MutableText;
import net.minecraft.util.Formatting;
import net.minecraft.util.Util;
import net.szum123321.textile_backup.Statics;
import net.szum123321.textile_backup.Globals;
import net.szum123321.textile_backup.TextileBackup;
import net.szum123321.textile_backup.TextileLogger;
import net.szum123321.textile_backup.config.ConfigHelper;
import net.szum123321.textile_backup.core.ActionInitiator;
import net.szum123321.textile_backup.core.RestoreableFile;
import net.szum123321.textile_backup.core.Utilities;
import java.io.File;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
public class RestoreHelper {
public static Optional<File> findFileAndLockIfPresent(LocalDateTime backupTime, MinecraftServer server) {
File root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
private final static ConfigHelper config = ConfigHelper.INSTANCE;
Optional<File> optionalFile = Arrays.stream(root.listFiles())
.filter(Utilities::isValidBackup)
.filter(file -> Utilities.getFileCreationTime(file).get().equals(backupTime))
.findFirst();
public static Optional<RestoreableFile> findFileAndLockIfPresent(LocalDateTime backupTime, MinecraftServer server) {
Path root = Utilities.getBackupRootPath(Utilities.getLevelName(server));
optionalFile.ifPresent(file -> Statics.untouchableFile = file);
Optional<RestoreableFile> optionalFile =
RestoreableFile.applyOnFiles(root, Optional.empty(),
e -> log.error("在尝试锁定文件时发生了异常!", e),
s -> s.filter(rf -> rf.getCreationTime().equals(backupTime))
.findFirst());
optionalFile.ifPresent(r -> Globals.INSTANCE.setLockedFile(r.getFile()));
return optionalFile;
}
public static AwaitThread create(File backupFile, MinecraftServer server, String comment) {
MutableText msg = new LiteralText("Warning! The server is going to shut down in " + Statics.CONFIG.restoreDelay + " seconds!");
msg.formatted(Formatting.WHITE);
msg = Statics.LOGGER.getPrefixText().append(msg);
public static Optional<RestoreableFile> getLatestAndLockIfPresent( MinecraftServer server) {
var available = RestoreHelper.getAvailableBackups(server);
server.getPlayerManager().broadcastChatMessage(msg, MessageType.SYSTEM, Util.NIL_UUID);
if(available.isEmpty()) return Optional.empty();
else {
var latest = available.getLast();
Globals.INSTANCE.setLockedFile(latest.getFile());
return Optional.of(latest);
}
}
Statics.globalShutdownBackupFlag.set(false);
public static AwaitThread create(RestoreContext ctx) {
if(ctx.initiator() == ActionInitiator.Player)
log.info("备份恢复由以下玩家发起:{}", ctx.commandSource().getName());
else
log.info("备份恢复由服务器控制台发起");
Utilities.notifyPlayers(
ctx.server(),
"警告!服务器将在" + config.get().restoreDelay + "秒后关闭!"
);
return new AwaitThread(
Statics.CONFIG.restoreDelay,
new RestoreBackupRunnable(server, backupFile, comment)
config.get().restoreDelay,
new RestoreBackupRunnable(ctx)
);
}
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::new)
.collect(Collectors.toList());
}
public static class RestoreableFile {
private final LocalDateTime creationTime;
private final String comment;
protected RestoreableFile(File file) {
String extension = Utilities.getFileExtension(file).orElseThrow(() -> new NoSuchElementException("Couldn't get file extention")).getString();
this.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 LocalDateTime getCreationTime() {
return creationTime;
}
public String getComment() {
return comment;
}
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,76 +1,101 @@
/*
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.Statics;
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;
import org.apache.commons.compress.compressors.CompressorInputStream;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
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 {
public static void decompress(File input, File target) {
Instant start = Instant.now();
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
try (FileInputStream fileInputStream = new FileInputStream(input);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
CompressorInputStream compressorInputStream = new CompressorStreamFactory().createCompressorInputStream(bufferedInputStream);
public static long decompress(Path input, Path target) throws IOException {
Instant start = Instant.now();
FileTreeHashBuilder treeBuilder = new FileTreeHashBuilder(0);
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)) {
Statics.LOGGER.warn("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()) {
Statics.LOGGER.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) {
Statics.LOGGER.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) {
Statics.LOGGER.error("An exception occurred! ", e);
} catch (CompressorException e) {
throw new IOException(e);
}
Statics.LOGGER.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 {
try {
return new CompressorStreamFactory().createCompressorInputStream(inputStream);
} catch (CompressorException e) {
final byte[] tarHeader = new byte[512];
int signatureLength;
inputStream.mark(tarHeader.length);
try {
signatureLength = IOUtils.readFully(inputStream, tarHeader);
inputStream.reset();
} catch (IOException e1) {
throw new CompressorException("IOException while reading tar signature", e1);
}
if(TarArchiveInputStream.matches(tarHeader, signatureLength)) return inputStream;
throw e;
}
}
}

View File

@ -1,71 +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.Statics;
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.ZipArchiveInputStream;
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 {
public static void decompress(File inputFile, File target) {
private final static TextileLogger log = new TextileLogger(TextileBackup.MOD_NAME);
public static long decompress(Path inputFile, Path target) throws IOException {
Instant start = Instant.now();
try (FileInputStream fileInputStream = new FileInputStream(inputFile);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
ZipArchiveInputStream zipInputStream = new ZipArchiveInputStream((bufferedInputStream))) {
ZipArchiveEntry entry;
FileTreeHashBuilder hashBuilder = new FileTreeHashBuilder(0);
while ((entry = zipInputStream.getNextZipEntry()) != null) {
if(!zipInputStream.canReadEntryData(entry)){
Statics.LOGGER.warn("Something when wrong while trying to decompress {}", entry.getName());
continue;
}
File file = target.toPath().resolve(entry.getName()).toFile();
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())
throw new IOException("Failed to create directory " + parent);
try (OutputStream outputStream = Files.newOutputStream(file.toPath());
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
IOUtils.copy(zipInputStream, bufferedOutputStream);
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred while trying to decompress file: {}", file.getName(), e);
IOUtils.copy(in, out);
}
}
}
} catch (IOException e) {
Statics.LOGGER.error("An exception occurred! ", e);
}
Statics.LOGGER.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

@ -0,0 +1,39 @@
/*
* 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.mixin;
import net.minecraft.server.dedicated.DedicatedServerWatchdog;
import net.minecraft.util.Util;
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 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,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.mixin;
import net.minecraft.server.MinecraftServer;

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

@ -0,0 +1,63 @@
{
"text.autoconfig.textile_backup.title": "Textile Backup Configuration",
"text.autoconfig.textile_backup.category.default": "General",
"text.autoconfig.textile_backup.category.Create": "Backup settings",
"text.autoconfig.textile_backup.category.Restore": "Restore",
"text.autoconfig.textile_backup.category.Manage": "Management",
"text.autoconfig.textile_backup.option.backupInterval": "Backup Interval",
"text.autoconfig.textile_backup.option.backupInterval.@Tooltip": "Time between each automatic backup (in seconds)",
"text.autoconfig.textile_backup.option.restoreDelay": "Restore Delay",
"text.autoconfig.textile_backup.option.restoreDelay.@Tooltip": "In seconds",
"text.autoconfig.textile_backup.option.broadcastBackupStart": "Announce backup start",
"text.autoconfig.textile_backup.option.broadcastBackupDone": "Announce backup done",
"text.autoconfig.textile_backup.option.doBackupsOnEmptyServer": "Make automatic backups on empty server",
"text.autoconfig.textile_backup.option.shutdownBackup": "Make a backup on shutdown",
"text.autoconfig.textile_backup.option.backupOldWorlds": "Backup old worlds",
"text.autoconfig.textile_backup.option.perWorldBackup": "Use separate folders for different worlds",
"text.autoconfig.textile_backup.option.backupDirectoryPath": "Path to backup folder",
"text.autoconfig.textile_backup.option.fileBlacklist": "Blacklisted files",
"text.autoconfig.textile_backup.option.deleteOldBackupAfterRestore": "Delete restored backup",
"text.autoconfig.textile_backup.option.backupsToKeep": "Number of backups to keep",
"text.autoconfig.textile_backup.option.maxAge": "Max age of backup",
"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 KiBytes",
"text.autoconfig.textile_backup.option.compression": "Compression level",
"text.autoconfig.textile_backup.option.compression.@Tooltip": "Only affects zip",
"text.autoconfig.textile_backup.option.compressionCoreCountLimit": "Max number of cores used for compression",
"text.autoconfig.textile_backup.option.compressionCoreCountLimit.@Tooltip": "Set to 0 to use all available cores",
"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",
"text.autoconfig.textile_backup.option.playerWhitelist": "Admin Whitelist",
"text.autoconfig.textile_backup.option.playerBlacklist": "Admin Blacklist",
"text.autoconfig.textile_backup.option.dateTimeFormat": "Date&Time format",
"text.autoconfig.textile_backup.option.dateTimeFormat.@Tooltip": "See: https://github.com/Szum123321/textile_backup/wiki/Date-time-format"
}

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

@ -8,6 +8,13 @@
"authors": [
"Szum123321"
],
"contributors": [
"1a2s3d4f1",
"pm709",
"Harveykang",
"66Leo66",
"IzzyBizzy45"
],
"contact": {
"homepage": "https://www.curseforge.com/minecraft/mc-mods/textile-backup",
"issues": "https://github.com/Szum123321/textile_backup/issues",
@ -21,6 +28,9 @@
"entrypoints": {
"main": [
"net.szum123321.textile_backup.TextileBackup"
],
"modmenu": [
"net.szum123321.textile_backup.client.ModMenuEntry"
]
},
"mixins": [
@ -28,9 +38,14 @@
],
"depends": {
"fabricloader": ">=0.8.8",
"fabricloader": ">=0.14.0",
"fabric": "*",
"minecraft": "1.17.*"
"minecraft": "^1.20-",
"cloth-config2": "*",
"java": ">=16"
},
"recommends": {
"modmenu": "*"
},
"custom": {

View File

@ -1,9 +1,9 @@
{
"required": true,
"package": "net.szum123321.textile_backup.mixin",
"compatibilityLevel": "JAVA_8",
"compatibilityLevel": "JAVA_16",
"mixins": [
"MinecraftServerMixin",
"DedicatedServerWatchdogMixin",
"MinecraftServerSessionAccessor"
],
"client": [