Implement some UI
This commit is contained in:
6 files changed
+296
-94
No files matched your search
@@ -65,12 +65,16 @@ dependencies {
|
|||||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
||||||
// https://mvnrepository.com/artifact/software.amazon.awssdk/s3
|
// https://mvnrepository.com/artifact/software.amazon.awssdk/s3
|
||||||
shadow project(":bundler")
|
shadow project(":bundler")
|
||||||
|
shadow 'software.amazon.awssdk:s3:2.34.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations.implementation.extendsFrom(configurations.shadow)
|
configurations.implementation.extendsFrom(configurations.shadow)
|
||||||
shadowJar {
|
shadowJar {
|
||||||
configurations = [project.configurations.shadow]
|
configurations = [project.configurations.shadow]
|
||||||
exclude("META-INF")
|
exclude("META-INF")
|
||||||
|
dependencies {
|
||||||
|
exclude(dependency("software.amazon.awssdk:aws-xml-protocol:.*"))
|
||||||
|
}
|
||||||
minimize()
|
minimize()
|
||||||
//relocate 'com.google', 'sfcraft.relocation.google'
|
//relocate 'com.google', 'sfcraft.relocation.google'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package io.ib67.sfcraft;
|
package io.ib67.sfcraft;
|
||||||
|
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
public record Backup(
|
public record Backup(
|
||||||
String name,
|
String name,
|
||||||
String backupKey,
|
String backupKey,
|
||||||
@@ -8,4 +10,9 @@ public record Backup(
|
|||||||
boolean incremental,
|
boolean incremental,
|
||||||
long sizeTotal
|
long sizeTotal
|
||||||
) {
|
) {
|
||||||
|
public Text toText(){
|
||||||
|
return Text.of("sizeTotal: "+((sizeTotal / 1024.0) / 1024.0) + "GiB, "+
|
||||||
|
(incremental ? "incremental" :"") + ", type: "+type+"."
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,12 @@ package io.ib67.sfcraft;
|
|||||||
|
|
||||||
import com.mojang.brigadier.Command;
|
import com.mojang.brigadier.Command;
|
||||||
import com.mojang.brigadier.CommandDispatcher;
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
|
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
import com.mojang.brigadier.context.CommandContext;
|
import com.mojang.brigadier.context.CommandContext;
|
||||||
import com.mojang.brigadier.suggestion.Suggestions;
|
import com.mojang.brigadier.suggestion.Suggestions;
|
||||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
import net.minecraft.command.CommandRegistryAccess;
|
import net.minecraft.command.CommandRegistryAccess;
|
||||||
import net.minecraft.server.command.CommandManager;
|
import net.minecraft.server.command.CommandManager;
|
||||||
import net.minecraft.server.command.ServerCommandSource;
|
import net.minecraft.server.command.ServerCommandSource;
|
||||||
@@ -13,107 +15,43 @@ import net.minecraft.text.ClickEvent;
|
|||||||
import net.minecraft.text.Text;
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.IOException;
|
import java.lang.ref.WeakReference;
|
||||||
import java.nio.file.Files;
|
import java.util.List;
|
||||||
import java.nio.file.Path;
|
import java.util.Map;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import static net.minecraft.server.command.CommandManager.argument;
|
import static net.minecraft.server.command.CommandManager.argument;
|
||||||
import static net.minecraft.server.command.CommandManager.literal;
|
import static net.minecraft.server.command.CommandManager.literal;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
public class BackupCommands {
|
public class BackupCommands {
|
||||||
protected final JustBackupMod mod;
|
protected final JustBackupMod mod;
|
||||||
protected Runnable restoreIssued;
|
protected Runnable restoreIssued;
|
||||||
|
protected final List<String> helpMessage = List.of(
|
||||||
|
"__ Help messages of JustBackup __",
|
||||||
|
" /backup help ",
|
||||||
|
" -- Show this message",
|
||||||
|
" /backup list ",
|
||||||
|
" -- List backups",
|
||||||
|
" /backup restore <backupName>",
|
||||||
|
" -- Restore the backup on next server restart.",
|
||||||
|
" Can be cancelled by /backup cancelrestore",
|
||||||
|
" /backup delete <backupName>",
|
||||||
|
" -- Delete a backup. This does not ask for a confirmation.",
|
||||||
|
" /backup create <incremental: true/false> <subject / \"@all\">",
|
||||||
|
" -- Create a backup.",
|
||||||
|
" Options:",
|
||||||
|
" incremental: Only bundle files detected changes",
|
||||||
|
" subject: specify a subject to backup. Or use \"@all\" to backup all subjects.",
|
||||||
|
" /backup suspend",
|
||||||
|
" -- Temporarily stops the auto-backup worker, until next restart or ",
|
||||||
|
" typing this command again."
|
||||||
|
);
|
||||||
|
|
||||||
public BackupCommands(JustBackupMod mod) {
|
public BackupCommands(JustBackupMod mod) {
|
||||||
this.mod = mod;
|
this.mod = mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int cmdList(CommandContext<ServerCommandSource> ctx) {
|
|
||||||
var s = ctx.getSource();
|
|
||||||
s.sendFeedback(() -> Text.of("Tracked backups:"), false);
|
|
||||||
for (Backup value : mod.tracker.getTrackedBackups().values()) {
|
|
||||||
var restoreClick = Text.literal("[RESTORE]").withColor(Color.GREEN.getRGB())
|
|
||||||
.styled(it -> it.withClickEvent(new ClickEvent.SuggestCommand("/backup restore " + value.backupKey())));
|
|
||||||
var deleteClick = Text.literal("[DELETE]").withColor(Color.RED.getRGB())
|
|
||||||
.styled(it -> it.withClickEvent(new ClickEvent.SuggestCommand("/backup delete " + value.backupKey())));
|
|
||||||
var message = Text.literal(" - " + value.name() + " (" + value.backupKey() + ") " +
|
|
||||||
(value.incremental() ? "incremental" : "full") + " ").append(restoreClick).append(" ").append(deleteClick);
|
|
||||||
s.sendMessage(message);
|
|
||||||
s.sendMessage(Text.of(" Size: " + Math.floorDiv(value.sizeTotal(), 1024 * 1024) + "MiB"));
|
|
||||||
}
|
|
||||||
return Command.SINGLE_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int cmdDelete(CommandContext<ServerCommandSource> ctx) {
|
|
||||||
var _backup = ctx.getArgument("backupName", String.class);
|
|
||||||
var backup = mod.tracker.getTrackedBackups().get(_backup);
|
|
||||||
mod.tracker.deleteBackup(backup);
|
|
||||||
ctx.getSource().sendMessage(Text.of("Backup " + _backup + " is deleted successfully."));
|
|
||||||
return Command.SINGLE_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int cmdSuspend(CommandContext<ServerCommandSource> ctx) {
|
|
||||||
mod.suspend = !mod.suspend;
|
|
||||||
ctx.getSource().sendMessage(Text.of("Backup disabled: " + mod.suspend));
|
|
||||||
return Command.SINGLE_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int cmdRestore(CommandContext<ServerCommandSource> ctx) {
|
|
||||||
var s = ctx.getSource();
|
|
||||||
var pm = s.getServer().getPlayerManager();
|
|
||||||
var _backup = ctx.getArgument("backupName", String.class);
|
|
||||||
var backup = mod.tracker.getTrackedBackups().get(_backup);
|
|
||||||
if (backup == null) {
|
|
||||||
s.sendMessage(Text.of("Invalid backup " + _backup));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (restoreIssued != null) {
|
|
||||||
s.sendMessage(Text.of("Another restoration is in progress. try /backup cancel to cancel it"));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
s.sendMessage(Text.literal("Please restart your server to take changes"));
|
|
||||||
restoreIssued = () -> {
|
|
||||||
System.out.println("Recovering backup... ");
|
|
||||||
try {
|
|
||||||
var toOverride = Path.of(backup.from());
|
|
||||||
var old = Path.of(backup.from() + "_old");
|
|
||||||
if (Files.exists(old)) {
|
|
||||||
try (var f = Files.walk(old)) {
|
|
||||||
f.sorted(Comparator.reverseOrder())
|
|
||||||
.forEach(it -> {
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(it);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Files.deleteIfExists(old);
|
|
||||||
Files.move(toOverride, old);
|
|
||||||
System.out.println("Moved your old save to " + old);
|
|
||||||
mod.tracker.recoverBackup(backup, toOverride);
|
|
||||||
System.out.println("Recover successfully!");
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return Command.SINGLE_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int cmdHelp(CommandContext<ServerCommandSource> serverCommandSourceCommandContext) {
|
|
||||||
// todo i18n
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int cmdCancelRestore(CommandContext<ServerCommandSource> serverCommandSourceCommandContext) {
|
|
||||||
restoreIssued = null;
|
|
||||||
serverCommandSourceCommandContext.getSource().sendMessage(Text.of("Operation cancelled."));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void registerCommand(
|
public void registerCommand(
|
||||||
CommandDispatcher<ServerCommandSource> dispatcher,
|
CommandDispatcher<ServerCommandSource> dispatcher,
|
||||||
CommandRegistryAccess registry, CommandManager.RegistrationEnvironment env) {
|
CommandRegistryAccess registry, CommandManager.RegistrationEnvironment env) {
|
||||||
@@ -130,10 +68,136 @@ public class BackupCommands {
|
|||||||
.then(argument("backupName", StringArgumentType.greedyString())
|
.then(argument("backupName", StringArgumentType.greedyString())
|
||||||
.suggests(this::suggestBackups)
|
.suggests(this::suggestBackups)
|
||||||
.executes(this::cmdRestore)))
|
.executes(this::cmdRestore)))
|
||||||
// .then(literal("create").executes(this::cmdIssueBackupFull)) todo
|
.then(literal("create")
|
||||||
|
.then(argument("incremental", BoolArgumentType.bool())
|
||||||
|
.then(argument("subject", StringArgumentType.greedyString())
|
||||||
|
.executes(this::createBackupBySubject))
|
||||||
|
.then(literal("@all").executes(this::fullBackup)))
|
||||||
.then(literal("suspend").executes(this::cmdSuspend))
|
.then(literal("suspend").executes(this::cmdSuspend))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int createBackupBySubject(CommandContext<ServerCommandSource> context) {
|
||||||
|
var incremental = BoolArgumentType.getBool(context, "incremental");
|
||||||
|
var subject = StringArgumentType.getString(context, "subject");
|
||||||
|
var s = context.getSource();
|
||||||
|
s.sendMessage(Text.literal("The issued backup has been scheduled.").withColor(Color.CYAN.getRGB()));
|
||||||
|
mod.issueBackup(subject, incremental);
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int fullBackup(CommandContext<ServerCommandSource> context) {
|
||||||
|
var incremental = BoolArgumentType.getBool(context, "incremental");
|
||||||
|
var s = context.getSource();
|
||||||
|
s.sendMessage(Text.literal("The issued backup has been scheduled.").withColor(Color.CYAN.getRGB()));
|
||||||
|
var weakRef = new WeakReference<>(s);
|
||||||
|
mod.backupAll(incremental).thenAccept(result -> {
|
||||||
|
var ref = weakRef.get();
|
||||||
|
if (ref == null) {
|
||||||
|
ref = mod.server.getCommandSource();
|
||||||
|
}
|
||||||
|
ref.sendMessage(Text.literal("-- BACKUP RESULT SUMMARY --"));
|
||||||
|
for (var entry : result.entrySet()) {
|
||||||
|
var k = entry.getKey();
|
||||||
|
var v = entry.getValue();
|
||||||
|
ref.sendMessage(Text.literal(" - [" + k + "]: ").append(v.toText()));
|
||||||
|
ref.sendMessage(Text.literal("BACKUP KEY: " + v.backupKey()).withColor(Color.GRAY.getRGB()));
|
||||||
|
ref.sendMessage(Text.literal("GENERATED FROM: " + v.from()).withColor(Color.GRAY.getRGB()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int cmdSuspend(CommandContext<ServerCommandSource> context) {
|
||||||
|
var s = context.getSource();
|
||||||
|
// this is safe as we are the only writer here.
|
||||||
|
var suspend = mod.suspend;
|
||||||
|
mod.suspend = !suspend;
|
||||||
|
if (suspend) {
|
||||||
|
s.sendMessage(Text.of("The automatic backup system is now re-enabled."));
|
||||||
|
} else {
|
||||||
|
s.sendMessage(Text.of("The automatic backup system is now disabled."));
|
||||||
|
}
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int cmdRestore(CommandContext<ServerCommandSource> context) {
|
||||||
|
var s = context.getSource();
|
||||||
|
if (restoreIssued != null) {
|
||||||
|
s.sendMessage(Text.literal("Another restore request is already present.").withColor(Color.RED.getRGB()));
|
||||||
|
s.sendMessage(Text.of("TIP: Use `/backup cancelrestore` to cancel that request."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var backupName = StringArgumentType.getString(context, "backupName");
|
||||||
|
restoreIssued = () -> {
|
||||||
|
//todo implementation
|
||||||
|
};
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int cmdDelete(CommandContext<ServerCommandSource> context) {
|
||||||
|
var backupName = StringArgumentType.getString(context, "backupName");
|
||||||
|
var backup = mod.tracker.getTrackedBackups().get(backupName);
|
||||||
|
var source = context.getSource();
|
||||||
|
if (backup == null) {
|
||||||
|
source.sendMessage(Text.of("Invalid backup. No backups are named '" + backupName + "'"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mod.tracker.deleteBackup(backup);
|
||||||
|
source.sendMessage(Text.of("Backup has been deleted."));
|
||||||
|
} catch (Exception e) {
|
||||||
|
source.sendMessage(Text.of("Backup has not been deleted successfully. ERROR: " + e));
|
||||||
|
log.error(e);
|
||||||
|
}
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int cmdList(CommandContext<ServerCommandSource> context) {
|
||||||
|
var source = context.getSource();
|
||||||
|
var trackedBackups = mod.tracker.getTrackedBackups();
|
||||||
|
if (trackedBackups.isEmpty()) {
|
||||||
|
source.sendMessage(Text.of("No backups are neither created nor tracked yet."));
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
}
|
||||||
|
source.sendMessage(Text.of("ALL TRACKED BACKUPS:"));
|
||||||
|
for (var entry : trackedBackups.entrySet()) {
|
||||||
|
var backupKey = entry.getKey();
|
||||||
|
var backup = entry.getValue();
|
||||||
|
source.sendMessage(Text.literal(" - [" + backup.name() + "] ").append(backup.toText()));
|
||||||
|
source.sendMessage(Text.literal(" ").append(
|
||||||
|
Text.literal("[DELETE]").withColor(Color.RED.getRGB()).styled(it -> it.withClickEvent(
|
||||||
|
new ClickEvent.SuggestCommand("/backup delete " + backupKey)
|
||||||
|
))).append(Text.literal(" ")).append(
|
||||||
|
Text.literal("[RESTORE]").withColor(Color.CYAN.getRGB()).styled(it -> it.withClickEvent(
|
||||||
|
new ClickEvent.SuggestCommand("/backup restore " + backupKey)
|
||||||
|
))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int cmdCancelRestore(CommandContext<ServerCommandSource> context) {
|
||||||
|
var source = context.getSource();
|
||||||
|
if (restoreIssued != null) {
|
||||||
|
restoreIssued = null;
|
||||||
|
source.sendMessage(Text.of("The backup restoration request has been cancelled."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
source.sendMessage(Text.of("You have no backup restoration request in flight."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int cmdHelp(CommandContext<ServerCommandSource> ctx) {
|
||||||
|
var source = ctx.getSource();
|
||||||
|
for (String s : helpMessage) {
|
||||||
|
if (s.startsWith(" /")) {
|
||||||
|
source.sendMessage(Text.literal(s).withColor(Color.CYAN.getRGB()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
private CompletableFuture<Suggestions> suggestBackups(CommandContext<ServerCommandSource> serverCommandSourceCommandContext, SuggestionsBuilder suggestionsBuilder) {
|
private CompletableFuture<Suggestions> suggestBackups(CommandContext<ServerCommandSource> serverCommandSourceCommandContext, SuggestionsBuilder suggestionsBuilder) {
|
||||||
for (Backup value : mod.tracker.getTrackedBackups().values()) {
|
for (Backup value : mod.tracker.getTrackedBackups().values()) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import io.ib67.sfcraft.config.StorageOption;
|
|||||||
import io.ib67.sfcraft.strategy.BackupStrategy;
|
import io.ib67.sfcraft.strategy.BackupStrategy;
|
||||||
import io.ib67.sfcraft.strategy.BackupTracker;
|
import io.ib67.sfcraft.strategy.BackupTracker;
|
||||||
import io.ib67.sfcraft.strategy.LocalBackupStrategy;
|
import io.ib67.sfcraft.strategy.LocalBackupStrategy;
|
||||||
|
import io.ib67.sfcraft.strategy.S3BackupStrategy;
|
||||||
import io.netty.buffer.ByteBufAllocator;
|
import io.netty.buffer.ByteBufAllocator;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import net.fabricmc.api.ModInitializer;
|
import net.fabricmc.api.ModInitializer;
|
||||||
@@ -19,6 +20,7 @@ import org.jetbrains.annotations.UnknownNullability;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -28,6 +30,7 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class JustBackupMod implements ModInitializer {
|
public class JustBackupMod implements ModInitializer {
|
||||||
@@ -104,7 +107,12 @@ public class JustBackupMod implements ModInitializer {
|
|||||||
if (config.bundleFullAtStartup()) {
|
if (config.bundleFullAtStartup()) {
|
||||||
backupAll(false);
|
backupAll(false);
|
||||||
}
|
}
|
||||||
scheduledBackupExecutor.scheduleAtFixedRate(() -> backupAll(config.incremental()),
|
scheduledBackupExecutor.scheduleAtFixedRate(() -> {
|
||||||
|
if (!suspend) backupAll(config.incremental()).thenAccept(it -> {
|
||||||
|
LOGGER.info("Backup success!");
|
||||||
|
it.forEach((k, v) -> LOGGER.info(k + ": " + v));
|
||||||
|
});
|
||||||
|
},
|
||||||
config.backupIntervalMinutes(), config.backupIntervalMinutes(), TimeUnit.MINUTES);
|
config.backupIntervalMinutes(), config.backupIntervalMinutes(), TimeUnit.MINUTES);
|
||||||
});
|
});
|
||||||
ServerLifecycleEvents.SERVER_STOPPED.register(server -> {
|
ServerLifecycleEvents.SERVER_STOPPED.register(server -> {
|
||||||
@@ -127,6 +135,11 @@ public class JustBackupMod implements ModInitializer {
|
|||||||
|
|
||||||
public CompletableFuture<Map<String, Backup>> backupAll(boolean incremental) {
|
public CompletableFuture<Map<String, Backup>> backupAll(boolean incremental) {
|
||||||
var map = new HashMap<String, CompletableFuture<Backup>>();
|
var map = new HashMap<String, CompletableFuture<Backup>>();
|
||||||
|
server.getPlayerManager().broadcast(
|
||||||
|
Text.literal(" BACKUP >> ").withColor(Color.RED.getRGB()).styled(it -> it.withBold(true))
|
||||||
|
.append(Text.of("The server is performing a backup, you may experience some lag.")),
|
||||||
|
false
|
||||||
|
);
|
||||||
for (var entry : backupSubjects.entrySet()) {
|
for (var entry : backupSubjects.entrySet()) {
|
||||||
map.put(entry.getKey(), issueBackup(entry.getKey(), incremental));
|
map.put(entry.getKey(), issueBackup(entry.getKey(), incremental));
|
||||||
}
|
}
|
||||||
@@ -139,9 +152,6 @@ public class JustBackupMod implements ModInitializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Backup> issueBackup(String subjectName, boolean incremental) {
|
public CompletableFuture<Backup> issueBackup(String subjectName, boolean incremental) {
|
||||||
if (suspend) {
|
|
||||||
return CompletableFuture.failedFuture(new IllegalStateException("Backup is temporarily disabled"));
|
|
||||||
}
|
|
||||||
var subject = backupSubjects.get(subjectName);
|
var subject = backupSubjects.get(subjectName);
|
||||||
if (subject == null)
|
if (subject == null)
|
||||||
return CompletableFuture.failedFuture(new IllegalArgumentException("Unknown backup subject: " + subjectName));
|
return CompletableFuture.failedFuture(new IllegalArgumentException("Unknown backup subject: " + subjectName));
|
||||||
@@ -179,7 +189,7 @@ public class JustBackupMod implements ModInitializer {
|
|||||||
private BackupStrategy createStrategy() {
|
private BackupStrategy createStrategy() {
|
||||||
return switch (config.option()) {
|
return switch (config.option()) {
|
||||||
case StorageOption.Local local -> new LocalBackupStrategy(local);
|
case StorageOption.Local local -> new LocalBackupStrategy(local);
|
||||||
case StorageOption.S3 s3 -> throw new IllegalStateException();
|
case StorageOption.S3 s3 -> new S3BackupStrategy(s3, Path.of(config.temporaryBackupDir()));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +197,7 @@ public class JustBackupMod implements ModInitializer {
|
|||||||
private void saveConfig(Path configPath) {
|
private void saveConfig(Path configPath) {
|
||||||
LOGGER.info("Saving default configuration");
|
LOGGER.info("Saving default configuration");
|
||||||
var cfg = new JustBackupConfig(
|
var cfg = new JustBackupConfig(
|
||||||
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
60,
|
60,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import java.util.Map;
|
|||||||
public record JustBackupConfig(
|
public record JustBackupConfig(
|
||||||
boolean bundleFullAtStartup,
|
boolean bundleFullAtStartup,
|
||||||
boolean incremental,
|
boolean incremental,
|
||||||
|
boolean broadcast,
|
||||||
long backupIntervalMinutes,
|
long backupIntervalMinutes,
|
||||||
int keepBackups,
|
int keepBackups,
|
||||||
int compressionLevel,
|
int compressionLevel,
|
||||||
@@ -35,5 +36,8 @@ public record JustBackupConfig(
|
|||||||
log.warn("Backup subjects are not configured. The mod will not work!");
|
log.warn("Backup subjects are not configured. The mod will not work!");
|
||||||
}
|
}
|
||||||
Validate.notNull(option, "option cannot be null");
|
Validate.notNull(option, "option cannot be null");
|
||||||
|
if(backupSubjects.containsKey("@all")){
|
||||||
|
throw new IllegalArgumentException("A ambiguous name \"@all\" is not allowed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package io.ib67.sfcraft.strategy;
|
||||||
|
|
||||||
|
import io.ib67.sfcraft.Backup;
|
||||||
|
import io.ib67.sfcraft.bundler.BundleReader;
|
||||||
|
import io.ib67.sfcraft.config.StorageOption;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||||
|
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||||
|
import software.amazon.awssdk.regions.Region;
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
public class S3BackupStrategy implements BackupStrategy {
|
||||||
|
protected final AtomicBoolean uploading = new AtomicBoolean(false);
|
||||||
|
protected final StorageOption.S3 option;
|
||||||
|
protected final S3Client s3;
|
||||||
|
protected final Path temporaryDownloadPath;
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public S3BackupStrategy(StorageOption.S3 option, Path tempDownloadPath) {
|
||||||
|
this.option = option;
|
||||||
|
var credential = AwsBasicCredentials.create(option.accessKey(), option.secretKey());
|
||||||
|
this.s3 = S3Client.builder()
|
||||||
|
.region(Region.of(option.region()))
|
||||||
|
.forcePathStyle(option.enforcePathStyle())
|
||||||
|
.endpointOverride(URI.create(option.endpoint()))
|
||||||
|
.credentialsProvider(StaticCredentialsProvider.create(credential))
|
||||||
|
.build();
|
||||||
|
log.info("Testing S3 credentials");
|
||||||
|
// throws exception if the bucket neither inaccessible nor not exist
|
||||||
|
s3.headBucket(b -> b.bucket(option.bucket()));
|
||||||
|
this.temporaryDownloadPath = Objects.requireNonNull(tempDownloadPath);
|
||||||
|
if (Files.notExists(temporaryDownloadPath))
|
||||||
|
Files.createDirectories(temporaryDownloadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
public Backup createBackup(String subject, Path from, Path pathToBundle) {
|
||||||
|
if (!uploading.compareAndSet(false, true)) {
|
||||||
|
throw new IllegalStateException("Last upload is not done yet! keep waiting...");
|
||||||
|
}
|
||||||
|
int i = 0;
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
var object = option.prefix() + "/" + pathToBundle.getFileName().toString();
|
||||||
|
s3.putObject(o -> o.bucket(option.bucket())
|
||||||
|
.contentType("application/octet-stream")
|
||||||
|
.ifNoneMatch("*") // to avoid content overriding
|
||||||
|
.key(object).build(), pathToBundle);
|
||||||
|
return new Backup(pathToBundle.getFileName().toString(),
|
||||||
|
object, "s3",
|
||||||
|
from.toString(),
|
||||||
|
false,
|
||||||
|
Files.size(pathToBundle));
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (i >= option.retryAmount()) {
|
||||||
|
throw new IllegalStateException("Cannot upload " + pathToBundle + " to S3");
|
||||||
|
}
|
||||||
|
var nextTry = Math.pow(2, i++);
|
||||||
|
log.error("Error uploading S3. trying in {}s later. ({}/{})", nextTry, i, option.retryAmount(), e);
|
||||||
|
Thread.sleep((long) (nextTry * 1000));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Unrecoverable exception occurred when uploading " + pathToBundle, e);
|
||||||
|
} finally {
|
||||||
|
uploading.set(false);
|
||||||
|
}
|
||||||
|
} while (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
public void recoverBackup(Backup backup, Path restorePath) {
|
||||||
|
var resp = s3.getObject(b -> b.bucket(option.bucket()).key(backup.backupKey()));
|
||||||
|
var target = temporaryDownloadPath.resolve("s3_" + System.currentTimeMillis());
|
||||||
|
try (var fs = Files.newOutputStream(target, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||||
|
resp.transferTo(fs);
|
||||||
|
BundleReader.builder().build().extract(target, restorePath);
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
public void deleteBackup(Backup backup) {
|
||||||
|
s3.deleteObject(b -> b.bucket(option.bucket()).key(backup.backupKey()).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
if (uploading.get()) {
|
||||||
|
log.warn("Last S3 Backup is still uploading!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
s3.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user