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 {
|
||||
id 'fabric-loom' version "${loom_version}"
|
||||
id 'maven-publish'
|
||||
id("io.freefair.lombok") version "9.0.0-rc2"
|
||||
|
||||
}
|
||||
|
||||
version = project.mod_version
|
||||
@@ -38,7 +40,8 @@ dependencies {
|
||||
|
||||
// Fabric API. This is technically optional, but you probably want it anyway.
|
||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
||||
|
||||
implementation("io.minio:minio:8.5.17")
|
||||
|
||||
}
|
||||
|
||||
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,
|
||||
"id": "modid",
|
||||
"id": "justbackup",
|
||||
"version": "${version}",
|
||||
"name": "Example mod",
|
||||
"description": "This is an example description! Tell everyone what your mod is about!",
|
||||
"name": "JustBackup",
|
||||
"description": "Just backup",
|
||||
"authors": [
|
||||
"Me!"
|
||||
"iceBear67"
|
||||
],
|
||||
"contact": {
|
||||
"homepage": "https://fabricmc.net/",
|
||||
"sources": "https://github.com/FabricMC/fabric-example-mod"
|
||||
},
|
||||
"license": "CC0-1.0",
|
||||
"icon": "assets/modid/icon.png",
|
||||
"environment": "*",
|
||||
"entrypoints": {
|
||||
"main": [
|
||||
"com.example.ExampleMod"
|
||||
],
|
||||
"client": [
|
||||
"com.example.ExampleModClient"
|
||||
"io.ib67.sfcraft.JustBackupMod"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
"modid.mixins.json",
|
||||
{
|
||||
"config": "modid.client.mixins.json",
|
||||
"environment": "client"
|
||||
}
|
||||
"justbackup.mixins.json"
|
||||
],
|
||||
"depends": {
|
||||
"fabricloader": ">=0.16.14",
|
||||
"minecraft": "~1.21.8",
|
||||
"java": ">=21",
|
||||
"fabric-api": "*"
|
||||
},
|
||||
"suggests": {
|
||||
"another-mod": "*"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "com.example.mixin",
|
||||
"package": "io.ib67.sfcraft.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"mixins": [
|
||||
"ExampleMixin"
|
||||
"LevelStorageAccessor",
|
||||
"MixinMinecraftServer"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
Reference in new issue
Block a user