From ae61be3c17d049ba4f8b89f84505ee65a2af634b Mon Sep 17 00:00:00 2001 From: SkyDynamic Date: Sun, 20 Jul 2025 18:09:44 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E5=A4=87=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 + .../quickbakcupmulti/ModContainer.java | 10 ++ .../QuickbakcupmultiReforged.java | 10 ++ .../config/DatabaseConfig.java | 26 ++++ .../quickbakcupmulti/config/ModConfig.java | 15 ++- .../event/OnLoadedWorldHandler.java | 23 ++++ .../event/OnServerStoppedHandler.java | 2 + .../mixin/MixinMinecraftServer.java | 3 + .../client/MixinClientPacketListener.java | 4 +- .../quickbakcupmulti/schedule/CronUtils.java | 76 +++++++++++ .../schedule/IModSchedule.java | 14 ++ .../schedule/ScheduleManager.java | 48 +++++++ .../schedule/impl/ModSchedule.java | 126 ++++++++++++++++++ .../quartz/DisableQuartzInfoLogger.java | 10 ++ .../schedule/quartz/ModJobFactory.java | 19 +++ .../runnables/DatabaseBackupRunnable.java | 30 +++++ .../quickbakcupmulti/utils/JitterUtils.java | 38 ++++++ fabric/build.gradle | 2 + gradle.properties | 8 +- neoforge/build.gradle | 4 + 20 files changed, 464 insertions(+), 8 deletions(-) create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/DatabaseConfig.java create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnLoadedWorldHandler.java create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/CronUtils.java create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/IModSchedule.java create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/ScheduleManager.java create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/impl/ModSchedule.java create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/quartz/DisableQuartzInfoLogger.java create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/quartz/ModJobFactory.java create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DatabaseBackupRunnable.java create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/utils/JitterUtils.java diff --git a/build.gradle b/build.gradle index f314381..bde07ea 100644 --- a/build.gradle +++ b/build.gradle @@ -41,11 +41,15 @@ subprojects { minecraft "net.minecraft:minecraft:$rootProject.minecraft_version" mappings loom.officialMojangMappings() + // incremental storage implementation("io.github.skydynamic:incremental-storage-lib:$rootProject.incremental_storage_lib_version") implementation("org.jetbrains.exposed:exposed-core:0.57.0") implementation("org.jetbrains.exposed:exposed-jdbc:0.57.0") implementation("com.h2database:h2:2.2.224") + // https://mvnrepository.com/artifact/org.quartz-scheduler/quartz + implementation("org.quartz-scheduler:quartz:2.5.0") + // lombok compileOnly("org.projectlombok:lombok:1.18.30") annotationProcessor("org.projectlombok:lombok:1.18.30") diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/ModContainer.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/ModContainer.java index c9b19bd..98c51f7 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/ModContainer.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/ModContainer.java @@ -1,12 +1,16 @@ package io.github.skydynamic.quickbakcupmulti; import com.mojang.brigadier.CommandDispatcher; +import io.github.skydynamic.quickbakcupmulti.schedule.IModSchedule; import io.github.skydynamic.quickbakcupmulti.utils.permission.PermissionManager; import lombok.Getter; import lombok.Setter; import net.minecraft.commands.CommandSourceStack; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; @Setter @Getter @@ -19,6 +23,8 @@ public class ModContainer { private boolean isRestoringBackup; private String currentSelectionBackup; + private List schedules = new ArrayList<>(); + public ModContainer( CommandDispatcher dispatcher ) { @@ -26,4 +32,8 @@ public ModContainer( } public ModContainer() {} + + public Optional getSchedule(String name) { + return schedules.stream().filter(it -> it.getName().equals(name)).findFirst(); + } } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/QuickbakcupmultiReforged.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/QuickbakcupmultiReforged.java index a90a236..ca50261 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/QuickbakcupmultiReforged.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/QuickbakcupmultiReforged.java @@ -4,6 +4,7 @@ import io.github.skydynamic.increment.storage.lib.utils.StorageManager; import io.github.skydynamic.quickbakcupmulti.command.ModCommand; import io.github.skydynamic.quickbakcupmulti.config.ModConfig; +import io.github.skydynamic.quickbakcupmulti.schedule.quartz.DisableQuartzInfoLogger; import io.github.skydynamic.quickbakcupmulti.translate.Translate; import io.github.skydynamic.quickbakcupmulti.utils.permission.PermissionManager; import lombok.Getter; @@ -12,6 +13,7 @@ import org.slf4j.LoggerFactory; import java.io.File; +import java.text.SimpleDateFormat; public final class QuickbakcupmultiReforged { public static final String MOD_ID = "quickbakcupmulti_reforged"; @@ -45,10 +47,18 @@ public static void init(ModContainer container) { if (!storagePath.exists()) { storagePath.mkdirs(); } + + // Disable Quartz Info Logger + DisableQuartzInfoLogger.disable(); } public static void registerCommand() { if (modContainer.getDispatcher() == null) return; ModCommand.register(modContainer.getDispatcher()); } + + public static String formatTimestamp(long timestamp) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return sdf.format(timestamp); + } } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/DatabaseConfig.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/DatabaseConfig.java new file mode 100644 index 0000000..24917c0 --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/DatabaseConfig.java @@ -0,0 +1,26 @@ +package io.github.skydynamic.quickbakcupmulti.config; + +import lombok.Getter; + +@SuppressWarnings("FieldMayBeFinal") +@Getter +public class DatabaseConfig { + private BackupConfig backupConfig = new BackupConfig(); + + public static class BackupConfig { + public boolean enabled; + public Integer interval = 7200; + public String crontab = null; + public String jitter = "1m"; + + @Override + public String toString() { + return "DatabaseBackupConfig{" + + "enabled=" + enabled + + ", interval=" + interval + "s" + + ", crontab='" + crontab + '\'' + + ", jitter='" + jitter + '\'' + + '}'; + } + } +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ModConfig.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ModConfig.java index 8b7d0cf..43df8a9 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ModConfig.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ModConfig.java @@ -5,6 +5,7 @@ import io.github.skydynamic.increment.storage.lib.manager.IConfig; import lombok.Getter; import lombok.Setter; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,7 +19,11 @@ public class ModConfig implements IConfig { private static final Logger logger = LoggerFactory.getLogger("Qbm-Config"); - private static final Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + private static final Gson gson = new GsonBuilder() + .setPrettyPrinting() + .disableHtmlEscaping() + .serializeNulls() + .create(); @Setter @Getter private ConfigStorage config = new ConfigStorage(); @@ -101,8 +106,12 @@ public boolean isCacheDatabase() { return config.cacheDatabase; } + public DatabaseConfig getDatabaseConfig() { + return config.database; + } + @Override - public String getStoragePath() { + public @NotNull String getStoragePath() { return config.storagePath; } @@ -127,6 +136,8 @@ public static class ConfigStorage { private String storagePath = "./QuickBackupMulti"; private boolean cacheDatabase = false; + private DatabaseConfig database = new DatabaseConfig(); + @Override public String toString() { return "ConfigStorage [checkUpdate=" + checkUpdate + ", ignoredFiles=" + ignoredFiles diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnLoadedWorldHandler.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnLoadedWorldHandler.java new file mode 100644 index 0000000..8da5b6c --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnLoadedWorldHandler.java @@ -0,0 +1,23 @@ +package io.github.skydynamic.quickbakcupmulti.event; + +import io.github.skydynamic.quickbakcupmulti.QuickbakcupmultiReforged; +import io.github.skydynamic.quickbakcupmulti.config.DatabaseConfig; +import io.github.skydynamic.quickbakcupmulti.schedule.ScheduleManager; +import io.github.skydynamic.quickbakcupmulti.schedule.runnables.DatabaseBackupRunnable; + +public class OnLoadedWorldHandler { + public static void handler() { + // Database Schedule backup + DatabaseConfig.BackupConfig databaseBackupConfig = QuickbakcupmultiReforged.getModConfig().getDatabaseConfig().getBackupConfig(); + Runnable databaseBackupRunnable = new DatabaseBackupRunnable(); + if (databaseBackupConfig.enabled) { + if (databaseBackupConfig.interval != null) { + ScheduleManager.registerSchedule("databaseSchedule", databaseBackupConfig.interval, databaseBackupConfig.jitter, databaseBackupRunnable); + } else if (databaseBackupConfig.crontab != null) { + ScheduleManager.registerSchedule("databaseSchedule", databaseBackupConfig.crontab, databaseBackupConfig.jitter, databaseBackupRunnable); + } + } + + ScheduleManager.startAllSchedule(); + } +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnServerStoppedHandler.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnServerStoppedHandler.java index 6131b3f..31e4159 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnServerStoppedHandler.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnServerStoppedHandler.java @@ -1,6 +1,7 @@ package io.github.skydynamic.quickbakcupmulti.event; import io.github.skydynamic.quickbakcupmulti.QuickbakcupmultiReforged; +import io.github.skydynamic.quickbakcupmulti.schedule.ScheduleManager; import io.github.skydynamic.quickbakcupmulti.utils.BackupManager; public class OnServerStoppedHandler { @@ -16,6 +17,7 @@ public static void handle() { } } else { QuickbakcupmultiReforged.getDatabase().closeDatabase(); + ScheduleManager.clearAllSchedule(); } } } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/mixin/MixinMinecraftServer.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/mixin/MixinMinecraftServer.java index 8bc0874..8e9a34e 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/mixin/MixinMinecraftServer.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/mixin/MixinMinecraftServer.java @@ -6,6 +6,7 @@ import io.github.skydynamic.quickbakcupmulti.DatabaseCache; import io.github.skydynamic.quickbakcupmulti.QuickbakcupmultiReforged; import io.github.skydynamic.quickbakcupmulti.database.DatabaseManager; +import io.github.skydynamic.quickbakcupmulti.event.OnLoadedWorldHandler; import net.minecraft.server.MinecraftServer; import net.minecraft.server.Services; import net.minecraft.server.WorldStem; @@ -53,5 +54,7 @@ private void onLoadLevel(CallbackInfoReturnable cir) { if (QuickbakcupmultiReforged.getModConfig().isCacheDatabase()) { DatabaseCache.updateStorageInfoCaches(); } + + OnLoadedWorldHandler.handler(); } } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/mixin/client/MixinClientPacketListener.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/mixin/client/MixinClientPacketListener.java index 17935e1..5d9ba4f 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/mixin/client/MixinClientPacketListener.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/mixin/client/MixinClientPacketListener.java @@ -20,9 +20,9 @@ public class MixinClientPacketListener { private void showWarning(ClientboundLoginPacket packet, CallbackInfo ci) { LocalPlayer player = Minecraft.getInstance().player; if (player != null) { - player.sendSystemMessage( + player.displayClientMessage( Component.literal("QuickbackupmultiReforged mod is not supporting clients now!") - .withStyle(ChatFormatting.RED) + .withStyle(ChatFormatting.RED), false ); } } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/CronUtils.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/CronUtils.java new file mode 100644 index 0000000..984d9cc --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/CronUtils.java @@ -0,0 +1,76 @@ +package io.github.skydynamic.quickbakcupmulti.schedule; + +import io.github.skydynamic.quickbakcupmulti.QuickbakcupmultiReforged; +import io.github.skydynamic.quickbakcupmulti.utils.JitterUtils; +import org.quartz.*; + +import java.text.ParseException; +import java.util.Date; + +public class CronUtils { + public enum ScheduleMode { + INTERVAL(Integer.class), + CRONTAB(String.class); + + private final Class type; + + ScheduleMode(Class type) { + this.type = type; + } + } + + public static Trigger buildTrigger(String name, ScheduleMode mode, T value, String jitter) { + IllegalArgumentException buildException = new IllegalArgumentException("Schedule mode %s requires value of type %s, but got %s (value: %s)" + .formatted(mode.name(), mode.type.getName(), value.getClass().getName(), value)); + int jitterSeconds = JitterUtils.parseAndRandom(jitter); + switch (mode) { + case INTERVAL -> { + if (value instanceof Integer v) { + return TriggerBuilder.newTrigger() + .withIdentity(name) + .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(v).repeatForever()) + .startAt(new Date(System.currentTimeMillis() + v * 1000L + jitterSeconds * 1000L)) + .usingJobData("jitter", jitterSeconds) + .build(); + } else { + QuickbakcupmultiReforged.logger.error("Failed to build schedule trigger", buildException); + } + } + case CRONTAB -> { + if (value instanceof String v) { + if (!cronIsValid(v)) { + QuickbakcupmultiReforged.logger.error("Failed to build schedule trigger, CronExpression {} is invalid", v); + return null; + } + return TriggerBuilder.newTrigger() + .withIdentity(name) + .withSchedule(CronScheduleBuilder.cronSchedule(v)) + .startAt(new Date(getNextExecutionTime(v).getTime() + jitterSeconds * 1000L)) + .usingJobData("jitter", jitterSeconds) + .build(); + } else { + QuickbakcupmultiReforged.logger.error("Failed to build schedule trigger", buildException); + } + } + } + return null; + } + + public static Date getNextExecutionTime(String cronExpress) { + try { + CronExpression cronExpression = new CronExpression(cronExpress); + return cronExpression.getNextValidTimeAfter(new Date()); + } catch (ParseException e) { + return new Date(); + } + } + + public static boolean cronIsValid(String cronExpression) { + try { + new CronExpression(cronExpression); + return true; + } catch (Exception e) { + return false; + } + } +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/IModSchedule.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/IModSchedule.java new file mode 100644 index 0000000..07712c5 --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/IModSchedule.java @@ -0,0 +1,14 @@ +package io.github.skydynamic.quickbakcupmulti.schedule; + +public interface IModSchedule { + String getName(); + + boolean startSchedule(); + void stopSchedule(); + + boolean isRunning(); + + long getNextExecuteTime(); + + IModSchedule setExcutor(Runnable executor); +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/ScheduleManager.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/ScheduleManager.java new file mode 100644 index 0000000..7676c0c --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/ScheduleManager.java @@ -0,0 +1,48 @@ +package io.github.skydynamic.quickbakcupmulti.schedule; + +import io.github.skydynamic.quickbakcupmulti.QuickbakcupmultiReforged; +import io.github.skydynamic.quickbakcupmulti.schedule.impl.ModSchedule; + +public class ScheduleManager { + private static void registerSchedule(ModSchedule schedule) { + QuickbakcupmultiReforged.getModContainer().getSchedules().add(schedule); + QuickbakcupmultiReforged.logger.info("Register schedule: {}", schedule.getName()); + } + + public static void registerSchedule(String name, String crontab, String jitter, Runnable executor) { + ModSchedule schedule = new ModSchedule(name, crontab, jitter).setExcutor(executor); + registerSchedule(schedule); + } + + public static void registerSchedule(String name, int interval, String jitter, Runnable executor) { + ModSchedule schedule = new ModSchedule(name, interval, jitter).setExcutor(executor); + registerSchedule(schedule); + } + + public static void startAllSchedule() { + for (IModSchedule schedule : QuickbakcupmultiReforged.getModContainer().getSchedules()) { + if (!schedule.startSchedule()) { + QuickbakcupmultiReforged.logger.warn("Failed to start schedule: {}", schedule.getName()); + } else { + QuickbakcupmultiReforged.logger.info("Start schedule: {}, next execute time: {}", + schedule.getName(), + QuickbakcupmultiReforged.formatTimestamp(schedule.getNextExecuteTime()) + ); + } + } + } + + public static void stopAllSchedule() { + for (IModSchedule schedule : QuickbakcupmultiReforged.getModContainer().getSchedules()) { + if (schedule.isRunning()) { + schedule.stopSchedule(); + QuickbakcupmultiReforged.logger.info("Stop schedule: {}", schedule.getName()); + } + } + } + + public static void clearAllSchedule() { + stopAllSchedule(); + QuickbakcupmultiReforged.getModContainer().getSchedules().clear(); + } +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/impl/ModSchedule.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/impl/ModSchedule.java new file mode 100644 index 0000000..89f00c9 --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/impl/ModSchedule.java @@ -0,0 +1,126 @@ +package io.github.skydynamic.quickbakcupmulti.schedule.impl; + +import io.github.skydynamic.quickbakcupmulti.QuickbakcupmultiReforged; +import io.github.skydynamic.quickbakcupmulti.schedule.CronUtils; +import io.github.skydynamic.quickbakcupmulti.schedule.IModSchedule; +import io.github.skydynamic.quickbakcupmulti.schedule.quartz.ModJobFactory; +import org.quartz.*; +import org.quartz.impl.StdSchedulerFactory; + +import static io.github.skydynamic.quickbakcupmulti.schedule.CronUtils.buildTrigger; + +public class ModSchedule implements IModSchedule, Job { + private String identity; + + private String crontab; + private Integer interval; + private String jitter; + + private Runnable executor; + + protected JobDetail jobDetail; + protected Trigger trigger; + protected Scheduler scheduler; + + // Quartz + @SuppressWarnings("unused") + public ModSchedule() { + } + + public ModSchedule(String identity, Integer interval, String jitter) { + this.identity = identity; + this.interval = interval; + this.jitter = jitter; + } + + public ModSchedule(String identity, String crontab, String jitter) { + this.identity = identity; + this.crontab = crontab; + this.jitter = jitter; + } + + @Override + public String getName() { + return identity; + } + + @Override + public boolean startSchedule() { + jobDetail = JobBuilder.newJob(this.getClass()).withIdentity(identity).build(); + StdSchedulerFactory sf = new StdSchedulerFactory(); + + if (crontab != null && !crontab.isEmpty() && interval == null) { + trigger = buildTrigger(identity, CronUtils.ScheduleMode.CRONTAB, crontab, jitter); + } else if (interval != null && interval > 0 && crontab == null) { + trigger = buildTrigger(identity, CronUtils.ScheduleMode.INTERVAL, interval, jitter); + } else { + return false; + } + + if (trigger == null) { + return false; + } + + try { + scheduler = sf.getScheduler(); + scheduler.scheduleJob(jobDetail, trigger); + scheduler.setJobFactory(new ModJobFactory(this)); + scheduler.start(); + return true; + } catch (SchedulerException e) { + QuickbakcupmultiReforged.logger.error("Failed to get scheduler", e); + return false; + } + } + + @Override + public void stopSchedule() { + try { + scheduler.shutdown(true); + } catch (SchedulerException e) { + QuickbakcupmultiReforged.logger.error("Failed to stop scheduler", e); + } + } + + @Override + public ModSchedule setExcutor(Runnable executor) { + this.executor = executor; + return this; + } + + @Override + public boolean isRunning() { + try { + return scheduler.isStarted(); + } catch (SchedulerException e) { + return false; + } + } + + @Override + public long getNextExecuteTime() { + JobDataMap dataMap = trigger.getJobDataMap(); + int jitter = dataMap.getInt("jitter"); + return trigger.getNextFireTime().getTime() + jitter * 1000L; + } + + @Override + public void execute(JobExecutionContext jobExecutionContext) { + JobDataMap dataMap = jobExecutionContext.getMergedJobDataMap(); + int jitter = dataMap.getInt("jitter"); + + try { + Thread.sleep(jitter * 1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + QuickbakcupmultiReforged.logger.info("Schedule {} execute in {}", identity, QuickbakcupmultiReforged.formatTimestamp(System.currentTimeMillis())); + executor.run(); + QuickbakcupmultiReforged.logger.info( + "Schedule {} execute done, next execute time: {}", + identity, + QuickbakcupmultiReforged.formatTimestamp(jobExecutionContext.getTrigger().getNextFireTime().getTime() + jitter * 1000L) + ); + } +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/quartz/DisableQuartzInfoLogger.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/quartz/DisableQuartzInfoLogger.java new file mode 100644 index 0000000..92f6aad --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/quartz/DisableQuartzInfoLogger.java @@ -0,0 +1,10 @@ +package io.github.skydynamic.quickbakcupmulti.schedule.quartz; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.config.Configurator; + +public class DisableQuartzInfoLogger { + public static void disable() { + Configurator.setLevel("org.quartz", Level.ERROR); + } +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/quartz/ModJobFactory.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/quartz/ModJobFactory.java new file mode 100644 index 0000000..a242081 --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/quartz/ModJobFactory.java @@ -0,0 +1,19 @@ +package io.github.skydynamic.quickbakcupmulti.schedule.quartz; + +import org.quartz.Job; +import org.quartz.Scheduler; +import org.quartz.simpl.SimpleJobFactory; +import org.quartz.spi.TriggerFiredBundle; + +public class ModJobFactory extends SimpleJobFactory { + private final Job jobInstance; + + public ModJobFactory(Job jobInstance) { + this.jobInstance = jobInstance; + } + + @Override + public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler){ + return jobInstance; + } +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DatabaseBackupRunnable.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DatabaseBackupRunnable.java new file mode 100644 index 0000000..c8cb940 --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DatabaseBackupRunnable.java @@ -0,0 +1,30 @@ +package io.github.skydynamic.quickbakcupmulti.schedule.runnables; + +import io.github.skydynamic.quickbakcupmulti.QuickbakcupmultiReforged; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +public class DatabaseBackupRunnable implements Runnable { + @Override + public void run() { + Path storagePath = Path.of(QuickbakcupmultiReforged.getModConfig().getStoragePath()); + File databaseFile = storagePath.resolve("QuickBakcupMulti.mv.db").toFile(); + File databaseBackupPath = storagePath.resolve("databaseBackup").toFile(); + if (!databaseBackupPath.exists()) { + databaseBackupPath.mkdirs(); + } + + if (databaseFile.exists()) { + try { + FileUtils.copyFile(databaseFile, FileUtils.getFile(databaseBackupPath, "database-" + System.currentTimeMillis() + ".mv.db")); + } catch (IOException e) { + QuickbakcupmultiReforged.logger.error("Database Backup Failed", e); + } + } else { + QuickbakcupmultiReforged.logger.error("Database File Not Found"); + } + } +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/utils/JitterUtils.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/utils/JitterUtils.java new file mode 100644 index 0000000..db0ff9a --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/utils/JitterUtils.java @@ -0,0 +1,38 @@ +package io.github.skydynamic.quickbakcupmulti.utils; + +import java.util.Random; + +public class JitterUtils { + public static long parseJitterToSeconds(String input) { + var pattern = java.util.regex.Pattern.compile("(\\d+)([a-zA-Z]+)"); + var matcher = pattern.matcher(input.trim()); + + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid jitter format: " + input); + } + + long value = Long.parseLong(matcher.group(1)); + String unit = matcher.group(2).toLowerCase(); + + return switch (unit) { + case "ms", "milli", "millis" -> value / 1000; + case "s", "sec", "second", "seconds" -> value; + case "m", "min", "minute", "minutes" -> value * 60; + case "h", "hr", "hour", "hours" -> value * 3600; + case "d", "day", "days" -> value * 86400; + default -> throw new IllegalArgumentException("Unknown time unit: " + unit); + }; + } + + public static int getRandomJitterInSeconds(long maxJitterSeconds) { + if (maxJitterSeconds <= 0) { + return 0; + } + return new Random().nextInt((int) maxJitterSeconds + 1); + } + + public static int parseAndRandom(String input) { + long parse = parseJitterToSeconds(input); + return getRandomJitterInSeconds((int) parse); + } +} diff --git a/fabric/build.gradle b/fabric/build.gradle index a3f6ea8..b71dae8 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -37,6 +37,8 @@ dependencies { common(project(path: ":${mod_id}-common:", configuration: "namedElements")) { transitive false } shadowBundle project(path: ":${mod_id}-common", configuration: "transformProductionFabric") + include("org.quartz-scheduler:quartz:2.5.0") + include("io.github.skydynamic:incremental-storage-lib:$rootProject.incremental_storage_lib_version") include("org.jetbrains.exposed:exposed-core:0.57.0") include("org.jetbrains.exposed:exposed-jdbc:0.57.0") diff --git a/gradle.properties b/gradle.properties index cb79c5a..264dd8f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,11 +8,11 @@ mod_id = quickbakcupmulti_reforged archives_name = QuickBakcupMulti-Reforged enabled_platforms = fabric,neoforge # Minecraft properties -minecraft_version = 1.21 -minecraft_supported_versions = >=1.21 <1.21.5 +minecraft_version = 1.21.4 +minecraft_supported_versions = 1.21.4 # Dependencies fabric_loader_version = 0.16.10 -fabric_api_version = 0.102.0+1.21 -neoforge_version = 21.0.167 +fabric_api_version = 0.119.3+1.21.4 +neoforge_version = 21.4.147 incremental_storage_lib_version=1.2.0+build.25062418.20 diff --git a/neoforge/build.gradle b/neoforge/build.gradle index 9866daa..5e51754 100644 --- a/neoforge/build.gradle +++ b/neoforge/build.gradle @@ -38,11 +38,15 @@ dependencies { common(project(path: ":${mod_id}-common", configuration: "namedElements")) { transitive false } shadowBundle project(path: ":${mod_id}-common", configuration: "transformProductionNeoForge") + forgeRuntimeLibrary("org.quartz-scheduler:quartz:2.5.0") + forgeRuntimeLibrary("io.github.skydynamic:incremental-storage-lib:$rootProject.incremental_storage_lib_version") forgeRuntimeLibrary("org.jetbrains.exposed:exposed-core:0.57.0") forgeRuntimeLibrary("org.jetbrains.exposed:exposed-jdbc:0.57.0") forgeRuntimeLibrary("com.h2database:h2:2.2.224") + include("org.quartz-scheduler:quartz:2.5.0") + include("io.github.skydynamic:incremental-storage-lib:$rootProject.incremental_storage_lib_version") include("org.jetbrains.exposed:exposed-core:0.57.0") include("org.jetbrains.exposed:exposed-jdbc:0.57.0") From 1dd6a0a2967930026f444754d2e8751e38631460 Mon Sep 17 00:00:00 2001 From: SkyDynamic Date: Sun, 20 Jul 2025 22:16:41 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E4=B8=96=E7=95=8C=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E5=A4=87=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../quickbakcupmulti/ServerManager.java | 12 +++ .../quickbakcupmulti/command/MakeCommand.java | 5 ++ .../config/DatabaseConfig.java | 18 +--- .../quickbakcupmulti/config/ModConfig.java | 22 ++++- .../config/ScheduleBackupConfig.java | 14 ++++ .../config/ScheduleConfig.java | 8 ++ .../event/OnLoadedWorldHandler.java | 22 ++++- .../schedule/IModSchedule.java | 2 + .../schedule/ScheduleManager.java | 10 +++ .../schedule/impl/ModSchedule.java | 9 ++ ...ava => DefaultDatabaseBackupRunnable.java} | 2 +- .../DefaultScheduleBackupRunnable.java | 82 +++++++++++++++++++ 12 files changed, 180 insertions(+), 26 deletions(-) create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ScheduleBackupConfig.java create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ScheduleConfig.java rename common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/{DatabaseBackupRunnable.java => DefaultDatabaseBackupRunnable.java} (94%) create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DefaultScheduleBackupRunnable.java diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/ServerManager.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/ServerManager.java index cd9bd6e..ca72361 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/ServerManager.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/ServerManager.java @@ -1,6 +1,10 @@ package io.github.skydynamic.quickbakcupmulti; +import net.minecraft.commands.CommandSourceStack; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; public class ServerManager { private final MinecraftServer server; @@ -18,4 +22,12 @@ public void startServer() { public void stopServer() { this.server.halt(false); } + + public List getPlayers() { + return this.server.getPlayerList().getPlayers(); + } + + public CommandSourceStack getCommandSource() { + return this.server.createCommandSourceStack(); + } } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/command/MakeCommand.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/command/MakeCommand.java index eb006b7..5d89fb4 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/command/MakeCommand.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/command/MakeCommand.java @@ -4,6 +4,7 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; import io.github.skydynamic.quickbakcupmulti.DatabaseCache; import io.github.skydynamic.quickbakcupmulti.QuickbakcupmultiReforged; +import io.github.skydynamic.quickbakcupmulti.schedule.ScheduleManager; import io.github.skydynamic.quickbakcupmulti.utils.BackupManager; import io.github.skydynamic.quickbakcupmulti.utils.permission.PermissionManager; import io.github.skydynamic.quickbakcupmulti.utils.permission.PermissionType; @@ -32,6 +33,10 @@ public void run() { if (QuickbakcupmultiReforged.getModConfig().isCacheDatabase()) { DatabaseCache.updateStorageInfoCaches(); } + + if (QuickbakcupmultiReforged.getModConfig().getScheduleBackupConfig().isResetTimerOnBackup() && ScheduleManager.resetTimer("scheduleBackup")) { + QuickbakcupmultiReforged.logger.info("Reset timer for scheduleBackup"); + } QuickbakcupmultiReforged.logger.info("Make Backup thread close => {}ms", System.currentTimeMillis() - l); } } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/DatabaseConfig.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/DatabaseConfig.java index 24917c0..145ff5e 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/DatabaseConfig.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/DatabaseConfig.java @@ -5,22 +5,6 @@ @SuppressWarnings("FieldMayBeFinal") @Getter public class DatabaseConfig { - private BackupConfig backupConfig = new BackupConfig(); + private ScheduleConfig backup = new ScheduleConfig(); - public static class BackupConfig { - public boolean enabled; - public Integer interval = 7200; - public String crontab = null; - public String jitter = "1m"; - - @Override - public String toString() { - return "DatabaseBackupConfig{" + - "enabled=" + enabled + - ", interval=" + interval + "s" + - ", crontab='" + crontab + '\'' + - ", jitter='" + jitter + '\'' + - '}'; - } - } } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ModConfig.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ModConfig.java index 43df8a9..112c19d 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ModConfig.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ModConfig.java @@ -106,6 +106,10 @@ public boolean isCacheDatabase() { return config.cacheDatabase; } + public ScheduleBackupConfig getScheduleBackupConfig() { + return config.scheduleBackup; + } + public DatabaseConfig getDatabaseConfig() { return config.database; } @@ -136,14 +140,24 @@ public static class ConfigStorage { private String storagePath = "./QuickBackupMulti"; private boolean cacheDatabase = false; + private ScheduleBackupConfig scheduleBackup = new ScheduleBackupConfig(); + private DatabaseConfig database = new DatabaseConfig(); @Override public String toString() { - return "ConfigStorage [checkUpdate=" + checkUpdate + ", ignoredFiles=" + ignoredFiles - + ", ignoredFolders=" + ignoredFolders + ", lang=" + lang - + ", autoRestartMode=" + autoRestartMode + ", storagePath=" + storagePath - + ", cacheDatabase=" + cacheDatabase + "]"; + return "ConfigStorage{" + + "checkUpdate=" + checkUpdate + + ", ignoredFiles=" + ignoredFiles + + ", ignoredFolders=" + ignoredFolders + + ", lang='" + lang + '\'' + + ", maxScheduleBackup=" + maxScheduleBackup + + ", autoRestartMode=" + autoRestartMode + + ", storagePath='" + storagePath + '\'' + + ", cacheDatabase=" + cacheDatabase + + ", scheduleBackup=" + scheduleBackup + + ", database=" + database + + '}'; } } } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ScheduleBackupConfig.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ScheduleBackupConfig.java new file mode 100644 index 0000000..d8d7cb0 --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ScheduleBackupConfig.java @@ -0,0 +1,14 @@ +package io.github.skydynamic.quickbakcupmulti.config; + +import lombok.Getter; + +import java.util.List; + +@SuppressWarnings("FieldMayBeFinal") +@Getter +public class ScheduleBackupConfig extends ScheduleConfig { + private boolean resetTimerOnBackup = true; + private boolean requireOnlinePlayers = false; + private boolean requireOnlinePlayersIgnoreCarpetFakePlayer = true; + private List requireOnlinePlayersBlacklist = List.of(); +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ScheduleConfig.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ScheduleConfig.java new file mode 100644 index 0000000..d4cf0d7 --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ScheduleConfig.java @@ -0,0 +1,8 @@ +package io.github.skydynamic.quickbakcupmulti.config; + +public class ScheduleConfig { + public boolean enabled; + public Integer interval = 7200; + public String crontab = null; + public String jitter = "1m"; +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnLoadedWorldHandler.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnLoadedWorldHandler.java index 8da5b6c..814c0cb 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnLoadedWorldHandler.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnLoadedWorldHandler.java @@ -1,15 +1,17 @@ package io.github.skydynamic.quickbakcupmulti.event; import io.github.skydynamic.quickbakcupmulti.QuickbakcupmultiReforged; -import io.github.skydynamic.quickbakcupmulti.config.DatabaseConfig; +import io.github.skydynamic.quickbakcupmulti.config.ScheduleBackupConfig; +import io.github.skydynamic.quickbakcupmulti.config.ScheduleConfig; import io.github.skydynamic.quickbakcupmulti.schedule.ScheduleManager; -import io.github.skydynamic.quickbakcupmulti.schedule.runnables.DatabaseBackupRunnable; +import io.github.skydynamic.quickbakcupmulti.schedule.runnables.DefaultDatabaseBackupRunnable; +import io.github.skydynamic.quickbakcupmulti.schedule.runnables.DefaultScheduleBackupRunnable; public class OnLoadedWorldHandler { public static void handler() { // Database Schedule backup - DatabaseConfig.BackupConfig databaseBackupConfig = QuickbakcupmultiReforged.getModConfig().getDatabaseConfig().getBackupConfig(); - Runnable databaseBackupRunnable = new DatabaseBackupRunnable(); + ScheduleConfig databaseBackupConfig = QuickbakcupmultiReforged.getModConfig().getDatabaseConfig().getBackup(); + Runnable databaseBackupRunnable = new DefaultDatabaseBackupRunnable(); if (databaseBackupConfig.enabled) { if (databaseBackupConfig.interval != null) { ScheduleManager.registerSchedule("databaseSchedule", databaseBackupConfig.interval, databaseBackupConfig.jitter, databaseBackupRunnable); @@ -18,6 +20,18 @@ public static void handler() { } } + // Schedule backup + ScheduleBackupConfig scheduleBackupConfig = QuickbakcupmultiReforged.getModConfig().getScheduleBackupConfig(); + Runnable scheduleBackupRunnable = new DefaultScheduleBackupRunnable(); + if (scheduleBackupConfig.enabled) { + if (scheduleBackupConfig.interval != null) { + ScheduleManager.registerSchedule("scheduleBackup", scheduleBackupConfig.interval, scheduleBackupConfig.jitter, scheduleBackupRunnable); + } else if (scheduleBackupConfig.crontab != null) { + ScheduleManager.registerSchedule("scheduleBackup", scheduleBackupConfig.crontab, scheduleBackupConfig.jitter, scheduleBackupRunnable); + } + } + + ScheduleManager.startAllSchedule(); } } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/IModSchedule.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/IModSchedule.java index 07712c5..f7a54c8 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/IModSchedule.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/IModSchedule.java @@ -10,5 +10,7 @@ public interface IModSchedule { long getNextExecuteTime(); + boolean resetTimer(); + IModSchedule setExcutor(Runnable executor); } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/ScheduleManager.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/ScheduleManager.java index 7676c0c..f4af7a2 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/ScheduleManager.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/ScheduleManager.java @@ -45,4 +45,14 @@ public static void clearAllSchedule() { stopAllSchedule(); QuickbakcupmultiReforged.getModContainer().getSchedules().clear(); } + + public static boolean resetTimer(String name) { + for (IModSchedule schedule : QuickbakcupmultiReforged.getModContainer().getSchedules()) { + if (schedule.getName().equals(name) && schedule.resetTimer()) { + QuickbakcupmultiReforged.logger.info("Reset timer: {}", name); + return true; + } + } + return false; + } } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/impl/ModSchedule.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/impl/ModSchedule.java index 89f00c9..b23c918 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/impl/ModSchedule.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/impl/ModSchedule.java @@ -104,6 +104,15 @@ public long getNextExecuteTime() { return trigger.getNextFireTime().getTime() + jitter * 1000L; } + @Override + public boolean resetTimer() { + if (scheduler != null) { + stopSchedule(); + return startSchedule(); + } + return false; + } + @Override public void execute(JobExecutionContext jobExecutionContext) { JobDataMap dataMap = jobExecutionContext.getMergedJobDataMap(); diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DatabaseBackupRunnable.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DefaultDatabaseBackupRunnable.java similarity index 94% rename from common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DatabaseBackupRunnable.java rename to common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DefaultDatabaseBackupRunnable.java index c8cb940..a8b8156 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DatabaseBackupRunnable.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DefaultDatabaseBackupRunnable.java @@ -7,7 +7,7 @@ import java.io.IOException; import java.nio.file.Path; -public class DatabaseBackupRunnable implements Runnable { +public class DefaultDatabaseBackupRunnable implements Runnable { @Override public void run() { Path storagePath = Path.of(QuickbakcupmultiReforged.getModConfig().getStoragePath()); diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DefaultScheduleBackupRunnable.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DefaultScheduleBackupRunnable.java new file mode 100644 index 0000000..0788d00 --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DefaultScheduleBackupRunnable.java @@ -0,0 +1,82 @@ +package io.github.skydynamic.quickbakcupmulti.schedule.runnables; + +import io.github.skydynamic.quickbakcupmulti.DatabaseCache; +import io.github.skydynamic.quickbakcupmulti.QuickbakcupmultiReforged; +import io.github.skydynamic.quickbakcupmulti.config.ScheduleBackupConfig; +import io.github.skydynamic.quickbakcupmulti.utils.BackupManager; +import net.minecraft.server.level.ServerPlayer; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +public class DefaultScheduleBackupRunnable implements Runnable { + public static final Class CARPET_PLAYER_CLASS; + + @Override + public void run() { + ScheduleBackupConfig scheduleBackupConfig = QuickbakcupmultiReforged.getModConfig().getScheduleBackupConfig(); + List players = new ArrayList<>(QuickbakcupmultiReforged.getServerManager().getPlayers()); + + if (scheduleBackupConfig.isRequireOnlinePlayers()) { + if (scheduleBackupConfig.isRequireOnlinePlayersIgnoreCarpetFakePlayer()) { + players.removeIf(DefaultScheduleBackupRunnable::isCarpetPlayer); + } + + if (!scheduleBackupConfig.getRequireOnlinePlayersBlacklist().isEmpty()) { + players = filterBlacklistedPlayers(players, scheduleBackupConfig.getRequireOnlinePlayersBlacklist()); + } + + if (players.isEmpty()) { + QuickbakcupmultiReforged.logger.warn("No online player meets the requirements, skip schedule backup"); + return; + } + } + + String scheduleName = "ScheduleBackup-" + QuickbakcupmultiReforged.formatTimestamp(System.currentTimeMillis()); + + if (QuickbakcupmultiReforged.getDatabase().storageExists(scheduleName)) { + QuickbakcupmultiReforged.logger.warn("Schedule backup name already exists: {}", scheduleName); + return; + } + + BackupManager.makeBackup(QuickbakcupmultiReforged.getServerManager().getCommandSource(), scheduleName, ""); + + if (QuickbakcupmultiReforged.getModConfig().isCacheDatabase()) { + DatabaseCache.updateStorageInfoCaches(); + } + + QuickbakcupmultiReforged.logger.info("Schedule backup complete: {}", scheduleName); + } + + private List filterBlacklistedPlayers(List players, List blacklist) { + List patterns = new ArrayList<>(); + for (String patternStr : blacklist) { + try { + patterns.add(Pattern.compile(patternStr)); + } catch (PatternSyntaxException e) { + QuickbakcupmultiReforged.logger.warn("Invalid pattern: {}, skip filter", patternStr); + } + } + + return players.stream() + .filter(player -> patterns.stream().noneMatch(p -> p.matcher(player.getName().getString()).matches())) + .toList(); + } + + + private static boolean isCarpetPlayer(ServerPlayer player) { + return CARPET_PLAYER_CLASS != null && CARPET_PLAYER_CLASS.isInstance(player); + } + + static { + Class clazz; + try { + clazz = Class.forName("carpet.patches.EntityPlayerMPFake"); + } catch (ClassNotFoundException e) { + clazz = null; + } + CARPET_PLAYER_CLASS = clazz; + } +} From 67dd93078129f3c7d4305708aee971eb23a55187 Mon Sep 17 00:00:00 2001 From: SkyDynamic Date: Sun, 20 Jul 2025 23:00:42 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E5=9F=BA=E4=BA=8EPBS=E4=BF=9D?= =?UTF-8?q?=E7=95=99=E7=AD=96=E7=95=A5=E7=9A=84=E5=AE=9A=E6=97=B6=E6=B8=85?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../quickbakcupmulti/config/ModConfig.java | 7 ++ .../quickbakcupmulti/config/PbsConfig.java | 17 ++++ .../config/PruneScheduleConfig.java | 10 ++ .../event/OnLoadedWorldHandler.java | 16 +++- .../quickbakcupmulti/schedule/CronUtils.java | 4 +- .../runnables/DefaultPruneRunnable.java | 91 +++++++++++++++++++ .../quickbakcupmulti/utils/DurationUtils.java | 57 ++++++++++++ .../quickbakcupmulti/utils/JitterUtils.java | 38 -------- 8 files changed, 198 insertions(+), 42 deletions(-) create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/PbsConfig.java create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/PruneScheduleConfig.java create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DefaultPruneRunnable.java create mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/utils/DurationUtils.java delete mode 100644 common/src/main/java/io/github/skydynamic/quickbakcupmulti/utils/JitterUtils.java diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ModConfig.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ModConfig.java index 112c19d..f3d3bec 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ModConfig.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/ModConfig.java @@ -110,6 +110,10 @@ public ScheduleBackupConfig getScheduleBackupConfig() { return config.scheduleBackup; } + public PruneScheduleConfig getPruneScheduleConfig() { + return config.prune; + } + public DatabaseConfig getDatabaseConfig() { return config.database; } @@ -142,6 +146,8 @@ public static class ConfigStorage { private ScheduleBackupConfig scheduleBackup = new ScheduleBackupConfig(); + private PruneScheduleConfig prune = new PruneScheduleConfig(); + private DatabaseConfig database = new DatabaseConfig(); @Override @@ -156,6 +162,7 @@ public String toString() { ", storagePath='" + storagePath + '\'' + ", cacheDatabase=" + cacheDatabase + ", scheduleBackup=" + scheduleBackup + + ", prune=" + prune + ", database=" + database + '}'; } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/PbsConfig.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/PbsConfig.java new file mode 100644 index 0000000..ac92fbc --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/PbsConfig.java @@ -0,0 +1,17 @@ +package io.github.skydynamic.quickbakcupmulti.config; + +import lombok.Getter; + +@SuppressWarnings("FieldMayBeFinal") +@Getter +public class PbsConfig { + private boolean enabled = false; + private Integer maxAmount = 10; + private String maxLifeTime = "0s"; + private int last = -1; + private int hour = 0; + private int day = 0; + private int week = 0; + private int month = 1; + private int year = 0; +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/PruneScheduleConfig.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/PruneScheduleConfig.java new file mode 100644 index 0000000..29d9625 --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/config/PruneScheduleConfig.java @@ -0,0 +1,10 @@ +package io.github.skydynamic.quickbakcupmulti.config; + +import lombok.Getter; + +@SuppressWarnings("FieldMayBeFinal") +@Getter +public class PruneScheduleConfig extends ScheduleConfig { + private String timezoneOverride = null; + private PbsConfig regularBackup = new PbsConfig(); +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnLoadedWorldHandler.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnLoadedWorldHandler.java index 814c0cb..abe69f5 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnLoadedWorldHandler.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/event/OnLoadedWorldHandler.java @@ -1,18 +1,20 @@ package io.github.skydynamic.quickbakcupmulti.event; import io.github.skydynamic.quickbakcupmulti.QuickbakcupmultiReforged; +import io.github.skydynamic.quickbakcupmulti.config.PruneScheduleConfig; import io.github.skydynamic.quickbakcupmulti.config.ScheduleBackupConfig; import io.github.skydynamic.quickbakcupmulti.config.ScheduleConfig; import io.github.skydynamic.quickbakcupmulti.schedule.ScheduleManager; import io.github.skydynamic.quickbakcupmulti.schedule.runnables.DefaultDatabaseBackupRunnable; +import io.github.skydynamic.quickbakcupmulti.schedule.runnables.DefaultPruneRunnable; import io.github.skydynamic.quickbakcupmulti.schedule.runnables.DefaultScheduleBackupRunnable; public class OnLoadedWorldHandler { public static void handler() { // Database Schedule backup ScheduleConfig databaseBackupConfig = QuickbakcupmultiReforged.getModConfig().getDatabaseConfig().getBackup(); - Runnable databaseBackupRunnable = new DefaultDatabaseBackupRunnable(); if (databaseBackupConfig.enabled) { + Runnable databaseBackupRunnable = new DefaultDatabaseBackupRunnable(); if (databaseBackupConfig.interval != null) { ScheduleManager.registerSchedule("databaseSchedule", databaseBackupConfig.interval, databaseBackupConfig.jitter, databaseBackupRunnable); } else if (databaseBackupConfig.crontab != null) { @@ -22,8 +24,8 @@ public static void handler() { // Schedule backup ScheduleBackupConfig scheduleBackupConfig = QuickbakcupmultiReforged.getModConfig().getScheduleBackupConfig(); - Runnable scheduleBackupRunnable = new DefaultScheduleBackupRunnable(); if (scheduleBackupConfig.enabled) { + Runnable scheduleBackupRunnable = new DefaultScheduleBackupRunnable(); if (scheduleBackupConfig.interval != null) { ScheduleManager.registerSchedule("scheduleBackup", scheduleBackupConfig.interval, scheduleBackupConfig.jitter, scheduleBackupRunnable); } else if (scheduleBackupConfig.crontab != null) { @@ -31,6 +33,16 @@ public static void handler() { } } + // Prune schedule + PruneScheduleConfig pruneScheduleConfig = QuickbakcupmultiReforged.getModConfig().getPruneScheduleConfig(); + if (pruneScheduleConfig.enabled) { + Runnable pruneRunnable = new DefaultPruneRunnable(); + if (pruneScheduleConfig.interval != null) { + ScheduleManager.registerSchedule("pruneSchedule", pruneScheduleConfig.interval, pruneScheduleConfig.jitter, pruneRunnable); + } else if (pruneScheduleConfig.crontab != null) { + ScheduleManager.registerSchedule("pruneSchedule", pruneScheduleConfig.crontab, pruneScheduleConfig.jitter, pruneRunnable); + } + } ScheduleManager.startAllSchedule(); } diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/CronUtils.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/CronUtils.java index 984d9cc..0a215b7 100644 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/CronUtils.java +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/CronUtils.java @@ -1,7 +1,7 @@ package io.github.skydynamic.quickbakcupmulti.schedule; import io.github.skydynamic.quickbakcupmulti.QuickbakcupmultiReforged; -import io.github.skydynamic.quickbakcupmulti.utils.JitterUtils; +import io.github.skydynamic.quickbakcupmulti.utils.DurationUtils; import org.quartz.*; import java.text.ParseException; @@ -22,7 +22,7 @@ public enum ScheduleMode { public static Trigger buildTrigger(String name, ScheduleMode mode, T value, String jitter) { IllegalArgumentException buildException = new IllegalArgumentException("Schedule mode %s requires value of type %s, but got %s (value: %s)" .formatted(mode.name(), mode.type.getName(), value.getClass().getName(), value)); - int jitterSeconds = JitterUtils.parseAndRandom(jitter); + int jitterSeconds = DurationUtils.parseAndRandom(jitter); switch (mode) { case INTERVAL -> { if (value instanceof Integer v) { diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DefaultPruneRunnable.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DefaultPruneRunnable.java new file mode 100644 index 0000000..11f7d17 --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/schedule/runnables/DefaultPruneRunnable.java @@ -0,0 +1,91 @@ +package io.github.skydynamic.quickbakcupmulti.schedule.runnables; + +import io.github.skydynamic.increment.storage.lib.database.StorageInfo; +import io.github.skydynamic.quickbakcupmulti.QuickbakcupmultiReforged; +import io.github.skydynamic.quickbakcupmulti.config.PbsConfig; +import io.github.skydynamic.quickbakcupmulti.config.PruneScheduleConfig; +import io.github.skydynamic.quickbakcupmulti.utils.BackupManager; +import io.github.skydynamic.quickbakcupmulti.utils.DurationUtils; +import net.minecraft.commands.CommandSourceStack; + +import java.time.ZoneId; +import java.util.*; + +public class DefaultPruneRunnable implements Runnable { + @Override + public void run() { + PruneScheduleConfig pruneScheduleConfig = QuickbakcupmultiReforged.getModConfig().getPruneScheduleConfig(); + CommandSourceStack commandSourceStack = QuickbakcupmultiReforged.getServerManager().getCommandSource(); + + List backupList = QuickbakcupmultiReforged.getDatabase().getAllStorageInfo(); + List toDelete = filterBackupWithPbs(pruneScheduleConfig.getRegularBackup(), backupList, pruneScheduleConfig.getTimezoneOverride()); + toDelete.forEach(backup -> BackupManager.deleteBackup(commandSourceStack, backup.getName())); + + QuickbakcupmultiReforged.logger.info("Prune backup: {}", toDelete.size()); + } + + private List filterBackupWithPbs(PbsConfig pbsConfig, List backupList, String timezoneOverride) { + if (pbsConfig == null || !pbsConfig.isEnabled() || backupList == null || backupList.isEmpty()) { + return new ArrayList<>(); + } + + ZoneId zoneId = timezoneOverride != null ? ZoneId.of(timezoneOverride) : ZoneId.systemDefault(); + + List filteredList = new ArrayList<>(backupList); + filteredList.sort(Comparator.comparingLong(StorageInfo::getTimestamp)); + + List toDelete = new ArrayList<>(); + + Map timeUnits = Map.of( + "hour", pbsConfig.getHour(), + "day", pbsConfig.getDay(), + "week", pbsConfig.getWeek(), + "month", pbsConfig.getMonth(), + "year", pbsConfig.getYear() + ); + + for (Map.Entry entry : timeUnits.entrySet()) { + String unit = entry.getKey(); + int count = entry.getValue(); + if (count <= 0) continue; + + Map latestPerUnit = new HashMap<>(); + for (StorageInfo backup : filteredList) { + String key = DurationUtils.formatByUnit(backup.getTimestamp(), unit, zoneId); + if (!latestPerUnit.containsKey(key)) { + latestPerUnit.put(key, backup); + } else if (backup.getTimestamp() > latestPerUnit.get(key).getTimestamp()) { + toDelete.add(latestPerUnit.get(key)); + latestPerUnit.put(key, backup); + } else { + toDelete.add(backup); + } + } + filteredList.removeAll(latestPerUnit.values()); + } + + if (pbsConfig.getLast() > 0 && filteredList.size() > pbsConfig.getLast()) { + int keepCount = pbsConfig.getLast(); + List toKeep = filteredList.subList(filteredList.size() - keepCount, filteredList.size()); + Set toKeepSet = new HashSet<>(toKeep); + toDelete.addAll(filteredList.stream().filter(b -> !toKeepSet.contains(b)).toList()); + } + + if (pbsConfig.getMaxLifeTime() != null && !pbsConfig.getMaxLifeTime().equals("0s")) { + long maxLifeTimeMillis = DurationUtils.parseDurationToSeconds(pbsConfig.getMaxLifeTime()) * 1000; + long now = System.currentTimeMillis(); + List expired = filteredList.stream() + .filter(b -> now - b.getTimestamp() > maxLifeTimeMillis) + .toList(); + toDelete.addAll(expired); + filteredList.removeAll(expired); + } + + if (pbsConfig.getMaxAmount() > 0 && filteredList.size() > pbsConfig.getMaxAmount()) { + int removeCount = filteredList.size() - pbsConfig.getMaxAmount(); + toDelete.addAll(filteredList.subList(0, removeCount)); + } + + return toDelete; + } +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/utils/DurationUtils.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/utils/DurationUtils.java new file mode 100644 index 0000000..b596937 --- /dev/null +++ b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/utils/DurationUtils.java @@ -0,0 +1,57 @@ +package io.github.skydynamic.quickbakcupmulti.utils; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Random; + +public class DurationUtils { + public static long parseDurationToSeconds(String input) { + var pattern = java.util.regex.Pattern.compile("(\\d+)([a-zA-Z]+)"); + var matcher = pattern.matcher(input.trim()); + + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid jitter format: " + input); + } + + long value = Long.parseLong(matcher.group(1)); + String unit = matcher.group(2).toLowerCase(); + + return switch (unit) { + case "ms", "milli", "millis" -> value / 1000; + case "s", "sec", "second", "seconds" -> value; + case "m", "min", "minute", "minutes" -> value * 60; + case "h", "hr", "hour", "hours" -> value * 60 * 60; + case "d", "day", "days" -> value * 24 * 60 * 60; + case "w", "week", "weeks" -> value * 7 * 24 * 60 * 60; + case "mo", "mon", "month", "months" -> value * 30 * 24 * 60 * 60; + case "y", "yr", "year", "years" -> value * 365 * 24 * 60 * 60; + default -> 0; + }; + } + + public static String formatByUnit(long timestamp, String unit, ZoneId zoneId) { + LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), zoneId); + return switch (unit) { + case "hour" -> dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH")); + case "day" -> dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE); + case "week" -> String.format("%d-%02d", dateTime.getYear(), dateTime.getYear() / 52 + 1); + case "month" -> dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM")); + case "year" -> String.valueOf(dateTime.getYear()); + default -> ""; + }; + } + + public static int getRandomDurationInSeconds(long maxJitterSeconds) { + if (maxJitterSeconds <= 0) { + return 0; + } + return new Random().nextInt((int) maxJitterSeconds + 1); + } + + public static int parseAndRandom(String input) { + long parse = parseDurationToSeconds(input); + return getRandomDurationInSeconds((int) parse); + } +} diff --git a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/utils/JitterUtils.java b/common/src/main/java/io/github/skydynamic/quickbakcupmulti/utils/JitterUtils.java deleted file mode 100644 index db0ff9a..0000000 --- a/common/src/main/java/io/github/skydynamic/quickbakcupmulti/utils/JitterUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.github.skydynamic.quickbakcupmulti.utils; - -import java.util.Random; - -public class JitterUtils { - public static long parseJitterToSeconds(String input) { - var pattern = java.util.regex.Pattern.compile("(\\d+)([a-zA-Z]+)"); - var matcher = pattern.matcher(input.trim()); - - if (!matcher.matches()) { - throw new IllegalArgumentException("Invalid jitter format: " + input); - } - - long value = Long.parseLong(matcher.group(1)); - String unit = matcher.group(2).toLowerCase(); - - return switch (unit) { - case "ms", "milli", "millis" -> value / 1000; - case "s", "sec", "second", "seconds" -> value; - case "m", "min", "minute", "minutes" -> value * 60; - case "h", "hr", "hour", "hours" -> value * 3600; - case "d", "day", "days" -> value * 86400; - default -> throw new IllegalArgumentException("Unknown time unit: " + unit); - }; - } - - public static int getRandomJitterInSeconds(long maxJitterSeconds) { - if (maxJitterSeconds <= 0) { - return 0; - } - return new Random().nextInt((int) maxJitterSeconds + 1); - } - - public static int parseAndRandom(String input) { - long parse = parseJitterToSeconds(input); - return getRandomJitterInSeconds((int) parse); - } -} From 5bbb807cf8249f3ee9e55c70c8a076dee7b520cd Mon Sep 17 00:00:00 2001 From: SkyDynamic Date: Sun, 20 Jul 2025 23:06:16 +0800 Subject: [PATCH 4/4] update mod version --- README.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a3fbf00..f26a5a9 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ _✨ MC备份 / 回档模组 ✨_ `/qb` 或 `/quickbackupmulti`均可触发mod ## 特性 -- [ ] 定时备份 +- [x] 定时备份 - [x] 无限槽位 - [x] Hash对比并仅备份差异文件 - [ ] 个性化设置 diff --git a/gradle.properties b/gradle.properties index 264dd8f..ae93169 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ org.gradle.jvmargs = -Xmx2G org.gradle.parallel = true # Mod properties -mod_version = 3.1.0 +mod_version = 3.1.1 maven_group = io.github.skydynamic mod_id = quickbakcupmulti_reforged archives_name = QuickBakcupMulti-Reforged