temp
This commit is contained in:
1 parent
32b61dedf9
commit
bc9fab8d22
15 files changed
+333
-98
No files matched your search
+4
-1
@@ -1,6 +1,8 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'fabric-loom' version "${loom_version}"
|
id 'fabric-loom' version "${loom_version}"
|
||||||
id 'maven-publish'
|
id 'maven-publish'
|
||||||
|
id("io.freefair.lombok") version "9.0.0-rc2"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
version = project.mod_version
|
version = project.mod_version
|
||||||
@@ -38,7 +40,8 @@ dependencies {
|
|||||||
|
|
||||||
// Fabric API. This is technically optional, but you probably want it anyway.
|
// Fabric API. This is technically optional, but you probably want it anyway.
|
||||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
||||||
|
implementation("io.minio:minio:8.5.17")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
processResources {
|
processResources {
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.example;
|
|
||||||
|
|
||||||
import net.fabricmc.api.ClientModInitializer;
|
|
||||||
|
|
||||||
public class ExampleModClient implements ClientModInitializer {
|
|
||||||
@Override
|
|
||||||
public void onInitializeClient() {
|
|
||||||
// This entrypoint is suitable for setting up client-specific logic, such as rendering.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.example.mixin.client;
|
|
||||||
|
|
||||||
import net.minecraft.client.MinecraftClient;
|
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
|
||||||
import org.spongepowered.asm.mixin.injection.At;
|
|
||||||
import org.spongepowered.asm.mixin.injection.Inject;
|
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
|
||||||
|
|
||||||
@Mixin(MinecraftClient.class)
|
|
||||||
public class ExampleClientMixin {
|
|
||||||
@Inject(at = @At("HEAD"), method = "run")
|
|
||||||
private void init(CallbackInfo info) {
|
|
||||||
// This code is injected into the start of MinecraftClient.run()V
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"required": true,
|
|
||||||
"package": "com.example.mixin.client",
|
|
||||||
"compatibilityLevel": "JAVA_21",
|
|
||||||
"client": [
|
|
||||||
"ExampleClientMixin"
|
|
||||||
],
|
|
||||||
"injectors": {
|
|
||||||
"defaultRequire": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.example;
|
|
||||||
|
|
||||||
import net.fabricmc.api.ModInitializer;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class ExampleMod implements ModInitializer {
|
|
||||||
public static final String MOD_ID = "modid";
|
|
||||||
|
|
||||||
// This logger is used to write text to the console and the log file.
|
|
||||||
// It is considered best practice to use your mod id as the logger's name.
|
|
||||||
// That way, it's clear which mod wrote info, warnings, and errors.
|
|
||||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onInitialize() {
|
|
||||||
// This code runs as soon as Minecraft is in a mod-load-ready state.
|
|
||||||
// However, some things (like resources) may still be uninitialized.
|
|
||||||
// Proceed with mild caution.
|
|
||||||
|
|
||||||
LOGGER.info("Hello Fabric world!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.example.mixin;
|
|
||||||
|
|
||||||
import net.minecraft.server.MinecraftServer;
|
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
|
||||||
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 ExampleMixin {
|
|
||||||
@Inject(at = @At("HEAD"), method = "loadWorld")
|
|
||||||
private void init(CallbackInfo info) {
|
|
||||||
// This code is injected into the start of MinecraftServer.loadWorld()V
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package io.ib67.sfcraft;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import io.ib67.sfcraft.config.JustBackupConfig;
|
||||||
|
import io.ib67.sfcraft.config.StorageOption;
|
||||||
|
import io.ib67.sfcraft.config.serializer.StorageOptionSerializer;
|
||||||
|
import io.ib67.sfcraft.mixin.LevelStorageAccessor;
|
||||||
|
import io.ib67.sfcraft.mixin.MixinMinecraftServer;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import net.fabricmc.api.ModInitializer;
|
||||||
|
|
||||||
|
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.world.ServerWorld;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class JustBackupMod implements ModInitializer {
|
||||||
|
public static final String MOD_ID = "justbackup";
|
||||||
|
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||||
|
public static final AtomicBoolean PERFORMING_BACKUP = new AtomicBoolean(false);
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting()
|
||||||
|
.registerTypeAdapter(StorageOption.class, new StorageOptionSerializer(
|
||||||
|
Map.of("s3", StorageOption.S3.class,
|
||||||
|
"local", StorageOption.Local.class)
|
||||||
|
))
|
||||||
|
.create();
|
||||||
|
protected ScheduledExecutorService scheduledBackupExecutor;
|
||||||
|
protected JustBackupConfig config;
|
||||||
|
protected volatile MinecraftServer server;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
public void onInitialize() {
|
||||||
|
LOGGER.info("Loading backup configuration");
|
||||||
|
var configPath = Path.of("config").resolve("just_backup.json");
|
||||||
|
if (Files.notExists(configPath.getParent())) {
|
||||||
|
Files.createDirectories(configPath.getParent());
|
||||||
|
}
|
||||||
|
if (Files.notExists(configPath)) {
|
||||||
|
saveConfig(configPath);
|
||||||
|
}
|
||||||
|
config = GSON.fromJson(Files.readString(configPath), JustBackupConfig.class);
|
||||||
|
LOGGER.info("Configuration has been loaded. Using storage option: {}", config.option().type());
|
||||||
|
scheduledBackupExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
|
||||||
|
LOGGER.info("Launching background threads...");
|
||||||
|
this.server = server;
|
||||||
|
scheduledBackupExecutor.scheduleAtFixedRate(this::performBackup,
|
||||||
|
config.backupIntervalMinutes(), config.backupIntervalMinutes(), TimeUnit.MINUTES);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private void performBackup() {
|
||||||
|
if (server == null) {
|
||||||
|
LOGGER.warn("MinecraftServer is not initialized yet, not doing backup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int i = 0;
|
||||||
|
while (!PERFORMING_BACKUP.compareAndSet(false, true)) {
|
||||||
|
if (i++ % 10 == 0) LOGGER.info("Server is saving world while performing backup! waiting...");
|
||||||
|
Thread.sleep(2000);
|
||||||
|
}
|
||||||
|
// performing backup is now true
|
||||||
|
try {
|
||||||
|
if (i == 0) {
|
||||||
|
// not saved yet.
|
||||||
|
server.saveAll(true, true, false);
|
||||||
|
}
|
||||||
|
var session = ((LevelStorageAccessor) server).getSession();
|
||||||
|
var path = session.getDirectory().getRootPath();
|
||||||
|
// !compress
|
||||||
|
if (!config.compress()) {
|
||||||
|
performBackupUncompressed(new WorldDir(Path.of(path)));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
PERFORMING_BACKUP.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private void performBackupUncompressed(WorldDir worldDir) {
|
||||||
|
var everything = worldDir.everything();
|
||||||
|
var opt = (StorageOption.Local) config.option();
|
||||||
|
var dst = Path.of(opt.saveDir()).resolve("Backup_" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()));
|
||||||
|
Files.createDirectories(dst);
|
||||||
|
try(var f = Files.walk(worldDir.region())){
|
||||||
|
bundleUncompressed(dst.resolve("regions.gz"), f.toList());
|
||||||
|
}
|
||||||
|
try(var f = Files.walk(worldDir.poi())) {
|
||||||
|
bundleUncompressed(dst.resolve("poi.gz"), f.toList());
|
||||||
|
}
|
||||||
|
var other = dst.resolve("other.zip", )
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private static void bundleUncompressed(Path dst, Collection<Path> toBundle){
|
||||||
|
try(var fileOut = new RandomAccessFile(dst.toFile(), "rw");){
|
||||||
|
var fileOutDst = fileOut.getChannel();
|
||||||
|
for (Path path : toBundle) {
|
||||||
|
var file = path.toFile();
|
||||||
|
try(var in = new FileInputStream(path.toFile())){
|
||||||
|
in.getChannel().transferTo(0, file.length(), fileOutDst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private void saveConfig(Path configPath) {
|
||||||
|
LOGGER.info("Saving default configuration");
|
||||||
|
var cfg = new JustBackupConfig(
|
||||||
|
60,
|
||||||
|
5,
|
||||||
|
true,
|
||||||
|
new StorageOption.Local("backups")
|
||||||
|
);
|
||||||
|
Files.writeString(configPath, GSON.toJson(cfg));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package io.ib67.sfcraft;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
|
||||||
|
import java.nio.file.FileVisitOption;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public record WorldDir(
|
||||||
|
Path root
|
||||||
|
) {
|
||||||
|
public Path region() {
|
||||||
|
return root.resolve("region");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path poi() {
|
||||||
|
return root.resolve("poi");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path entities() {
|
||||||
|
return root.resolve("entities");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path playerdata() {
|
||||||
|
return root.resolve("playerdata");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public Map<String, WorldDir> otherDimensions() {
|
||||||
|
var begin = root.resolve("dimensions").toAbsolutePath();
|
||||||
|
try (var w = Files.walk(begin, 3)) {
|
||||||
|
var _r = w.filter(Files::isDirectory)
|
||||||
|
.filter(it -> Files.exists(it.resolve("region")) && Files.exists(it.resolve("poi")))
|
||||||
|
.collect(Collectors.groupingBy(it -> it.relativize(root).toString(),
|
||||||
|
Collectors.mapping(WorldDir::new, Collectors.toList())));
|
||||||
|
var r = new HashMap<String, WorldDir>();
|
||||||
|
_r.forEach((k, v) -> {
|
||||||
|
assert v.size() <= 1;
|
||||||
|
r.put(k, v.getFirst());
|
||||||
|
});
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Files expect poi, region, entities, playerdata and things in dimension folder.
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
public List<Path> otherFiles() {
|
||||||
|
var exclude = List.of(playerdata(), entities(), region(), poi(), root.resolve("dimensions"));
|
||||||
|
try (var f = Files.walk(root)) {
|
||||||
|
return f.filter(it -> exclude.stream().noneMatch(it::startsWith)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public List<Path> everything() {
|
||||||
|
try (var f = Files.walk(root)) {
|
||||||
|
return f.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package io.ib67.sfcraft.config;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record JustBackupConfig(
|
||||||
|
long backupIntervalMinutes,
|
||||||
|
int keepBackups,
|
||||||
|
boolean compress,
|
||||||
|
StorageOption option
|
||||||
|
) {
|
||||||
|
public JustBackupConfig {
|
||||||
|
Validate.isTrue(backupIntervalMinutes > 0, "backup interval must be positive");
|
||||||
|
Validate.isTrue(keepBackups > 0, "keep backups must be positive");
|
||||||
|
Validate.notNull(option, "option cannot be null");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package io.ib67.sfcraft.config;
|
||||||
|
|
||||||
|
public sealed interface StorageOption {
|
||||||
|
String type();
|
||||||
|
|
||||||
|
record S3(
|
||||||
|
String region,
|
||||||
|
String accessKey,
|
||||||
|
String secretKey,
|
||||||
|
String endpoint,
|
||||||
|
String bucket
|
||||||
|
) implements StorageOption {
|
||||||
|
@Override
|
||||||
|
public String type() {
|
||||||
|
return "s3";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Local(
|
||||||
|
String saveDir
|
||||||
|
) implements StorageOption {
|
||||||
|
@Override
|
||||||
|
public String type() {
|
||||||
|
return "local";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package io.ib67.sfcraft.config.serializer;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import io.ib67.sfcraft.config.StorageOption;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class StorageOptionSerializer implements JsonDeserializer<StorageOption>, JsonSerializer<StorageOption> {
|
||||||
|
protected final Map<String, Class<? extends StorageOption>> subTypes;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StorageOption deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
|
if (!(json instanceof JsonObject jo)) throw new JsonParseException("Storage option must be a json object");
|
||||||
|
var type = jo.get("type").getAsString().toLowerCase();
|
||||||
|
return context.deserialize(json, subTypes.get(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(StorageOption src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
var jo = context.serialize(src).getAsJsonObject();
|
||||||
|
jo.addProperty("type", src.type());
|
||||||
|
return jo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package io.ib67.sfcraft.mixin;
|
||||||
|
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.world.level.storage.LevelStorage;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||||
|
|
||||||
|
@Mixin(MinecraftServer.class)
|
||||||
|
public interface LevelStorageAccessor {
|
||||||
|
@Accessor
|
||||||
|
LevelStorage.Session getSession();
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package io.ib67.sfcraft.mixin;
|
||||||
|
|
||||||
|
import io.ib67.sfcraft.JustBackupMod;
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.Shadow;
|
||||||
|
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;
|
||||||
|
|
||||||
|
import javax.security.auth.callback.Callback;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
@Mixin(MinecraftServer.class)
|
||||||
|
public abstract class MixinMinecraftServer {
|
||||||
|
|
||||||
|
@Shadow private int ticksUntilAutosave;
|
||||||
|
|
||||||
|
@Shadow protected abstract int getAutosaveInterval();
|
||||||
|
|
||||||
|
@Inject(method = "runAutosave", at = @At("HEAD"), cancellable = true)
|
||||||
|
private void backup$checkSaving(CallbackInfo ci) {
|
||||||
|
// is already backing up
|
||||||
|
this.ticksUntilAutosave = getAutosaveInterval();
|
||||||
|
if (!JustBackupMod.PERFORMING_BACKUP.compareAndSet(false, true)) ci.cancel();
|
||||||
|
}
|
||||||
|
@Inject(method="runAutosave", at=@At("RETURN"))
|
||||||
|
private void backup$disableProtect(CallbackInfo ci){
|
||||||
|
JustBackupMod.PERFORMING_BACKUP.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +1,27 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "modid",
|
"id": "justbackup",
|
||||||
"version": "${version}",
|
"version": "${version}",
|
||||||
"name": "Example mod",
|
"name": "JustBackup",
|
||||||
"description": "This is an example description! Tell everyone what your mod is about!",
|
"description": "Just backup",
|
||||||
"authors": [
|
"authors": [
|
||||||
"Me!"
|
"iceBear67"
|
||||||
],
|
],
|
||||||
"contact": {
|
|
||||||
"homepage": "https://fabricmc.net/",
|
|
||||||
"sources": "https://github.com/FabricMC/fabric-example-mod"
|
|
||||||
},
|
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"icon": "assets/modid/icon.png",
|
"icon": "assets/modid/icon.png",
|
||||||
"environment": "*",
|
"environment": "*",
|
||||||
"entrypoints": {
|
"entrypoints": {
|
||||||
"main": [
|
"main": [
|
||||||
"com.example.ExampleMod"
|
"io.ib67.sfcraft.JustBackupMod"
|
||||||
],
|
|
||||||
"client": [
|
|
||||||
"com.example.ExampleModClient"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"mixins": [
|
"mixins": [
|
||||||
"modid.mixins.json",
|
"justbackup.mixins.json"
|
||||||
{
|
|
||||||
"config": "modid.client.mixins.json",
|
|
||||||
"environment": "client"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"depends": {
|
"depends": {
|
||||||
"fabricloader": ">=0.16.14",
|
"fabricloader": ">=0.16.14",
|
||||||
"minecraft": "~1.21.8",
|
"minecraft": "~1.21.8",
|
||||||
"java": ">=21",
|
"java": ">=21",
|
||||||
"fabric-api": "*"
|
"fabric-api": "*"
|
||||||
},
|
|
||||||
"suggests": {
|
|
||||||
"another-mod": "*"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"required": true,
|
"required": true,
|
||||||
"package": "com.example.mixin",
|
"package": "io.ib67.sfcraft.mixin",
|
||||||
"compatibilityLevel": "JAVA_21",
|
"compatibilityLevel": "JAVA_21",
|
||||||
"mixins": [
|
"mixins": [
|
||||||
"ExampleMixin"
|
"LevelStorageAccessor",
|
||||||
|
"MixinMinecraftServer"
|
||||||
],
|
],
|
||||||
"injectors": {
|
"injectors": {
|
||||||
"defaultRequire": 1
|
"defaultRequire": 1
|
||||||
Reference in new issue
Block a user