diff --git a/src/main/java/io/ib67/sfcraft/BackupCommands.java b/src/main/java/io/ib67/sfcraft/BackupCommands.java index b151d64..7ac61bd 100644 --- a/src/main/java/io/ib67/sfcraft/BackupCommands.java +++ b/src/main/java/io/ib67/sfcraft/BackupCommands.java @@ -7,8 +7,10 @@ import net.minecraft.text.ClickEvent; import net.minecraft.text.Text; import java.awt.*; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Comparator; public class BackupCommands { protected final JustBackupMod mod; @@ -81,12 +83,24 @@ public class BackupCommands { s.sendMessage(Text.literal("Please restart your server to take changes")); restoreIssued = () -> { System.out.println("Recovering backup... "); - try{ + try { var toOverride = Path.of(backup.from()); - var old = Path.of(backup.from()+"_old"); + 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); + System.out.println("Moved your old save to " + old); mod.tracker.recoverBackup(backup, toOverride); System.out.println("Recover successfully!"); } catch (Exception e) { diff --git a/src/main/java/io/ib67/sfcraft/JustBackupMod.java b/src/main/java/io/ib67/sfcraft/JustBackupMod.java index 7b5a180..0aecb2f 100644 --- a/src/main/java/io/ib67/sfcraft/JustBackupMod.java +++ b/src/main/java/io/ib67/sfcraft/JustBackupMod.java @@ -145,12 +145,19 @@ public class JustBackupMod implements ModInitializer { .then(literal("create").executes(commandHandler::cmdIssueBackup)) .then(literal("suspend").executes(commandHandler::cmdSuspend)) ); + ServerLifecycleEvents.SERVER_STOPPED.register(server -> { + scheduledBackupExecutor.shutdown(); + if (commandHandler.restoreIssued != null) { + commandHandler.restoreIssued.run(); + } + tracker.close(); + }); } private BackupStrategy createStrategy() { return switch (config.option()) { case StorageOption.Local local -> new LocalBackupStrategy(local); - case StorageOption.S3 s3 -> new S3BackupStrategy(s3); + case StorageOption.S3 s3 -> new S3BackupStrategy(s3, Path.of(config.temporaryBackupDir())); }; } diff --git a/src/main/java/io/ib67/sfcraft/bundler/SerialBundlerReader.java b/src/main/java/io/ib67/sfcraft/bundler/SerialBundlerReader.java index cb25764..8728c82 100644 --- a/src/main/java/io/ib67/sfcraft/bundler/SerialBundlerReader.java +++ b/src/main/java/io/ib67/sfcraft/bundler/SerialBundlerReader.java @@ -18,6 +18,7 @@ public class SerialBundlerReader implements Closeable { protected final EntryInputStream metadataIn; protected final byte[] buffer = new byte[4096]; + protected final RandomAccessFile raf; protected boolean entryRead; protected int entries; @@ -28,7 +29,7 @@ public class SerialBundlerReader implements Closeable { // } public SerialBundlerReader(Path path) throws IOException { - var raf = new RandomAccessFile(path.toFile(), "r"); + raf = new RandomAccessFile(path.toFile(), "r"); var totalLen = Files.size(path); var magic = raf.readInt(); var compressed = magic == Bundle.COMPRESSED_MAGIC; @@ -96,5 +97,6 @@ public class SerialBundlerReader implements Closeable { public void close() throws IOException { in.close(); metadataIn.close(); + raf.close(); } } diff --git a/src/main/java/io/ib67/sfcraft/strategy/BackupStrategy.java b/src/main/java/io/ib67/sfcraft/strategy/BackupStrategy.java index 4745c8f..6c61740 100644 --- a/src/main/java/io/ib67/sfcraft/strategy/BackupStrategy.java +++ b/src/main/java/io/ib67/sfcraft/strategy/BackupStrategy.java @@ -18,4 +18,6 @@ public interface BackupStrategy { void deleteBackup(Backup backup); boolean isAvailable(); + + void close(); } diff --git a/src/main/java/io/ib67/sfcraft/strategy/BackupTracker.java b/src/main/java/io/ib67/sfcraft/strategy/BackupTracker.java index 8ab1885..b728360 100644 --- a/src/main/java/io/ib67/sfcraft/strategy/BackupTracker.java +++ b/src/main/java/io/ib67/sfcraft/strategy/BackupTracker.java @@ -41,4 +41,9 @@ public class BackupTracker implements BackupStrategy { public boolean isAvailable() { return underlying.isAvailable(); } + + @Override + public void close() { + underlying.close(); + } } diff --git a/src/main/java/io/ib67/sfcraft/strategy/LocalBackupStrategy.java b/src/main/java/io/ib67/sfcraft/strategy/LocalBackupStrategy.java index 8a39fc7..4298719 100644 --- a/src/main/java/io/ib67/sfcraft/strategy/LocalBackupStrategy.java +++ b/src/main/java/io/ib67/sfcraft/strategy/LocalBackupStrategy.java @@ -80,4 +80,9 @@ public class LocalBackupStrategy implements BackupStrategy { } return true; } + + @Override + public void close() { + + } } diff --git a/src/main/java/io/ib67/sfcraft/strategy/S3BackupStrategy.java b/src/main/java/io/ib67/sfcraft/strategy/S3BackupStrategy.java index e67f6fb..bf67bc6 100644 --- a/src/main/java/io/ib67/sfcraft/strategy/S3BackupStrategy.java +++ b/src/main/java/io/ib67/sfcraft/strategy/S3BackupStrategy.java @@ -1,48 +1,46 @@ package io.ib67.sfcraft.strategy; import io.ib67.sfcraft.Backup; -import io.ib67.sfcraft.WorldDir; import io.ib67.sfcraft.bundler.Bundle; import io.ib67.sfcraft.config.StorageOption; -import io.minio.BucketExistsArgs; -import io.minio.MinioClient; -import io.minio.RemoveObjectArgs; -import io.minio.UploadObjectArgs; -import lombok.RequiredArgsConstructor; 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.MalformedURLException; import java.net.URI; -import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.util.concurrent.TimeUnit; +import java.nio.file.StandardOpenOption; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; @Log4j2 public class S3BackupStrategy implements BackupStrategy { - protected final StorageOption.S3 option; - protected final MinioClient minio; 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) { + public S3BackupStrategy(StorageOption.S3 option, Path tempDownloadPath) { this.option = option; - this.minio = MinioClient.builder() - .endpoint(URI.create(option.endpoint()).toURL()) - .credentials(option.accessKey(), option.secretKey()) - .region(option.region()) + 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(); - if (option.enforcePathStyle()) { - minio.disableVirtualStyleEndpoint(); - } else { - minio.enableVirtualStyleEndpoint(); - } log.info("Testing S3 credentials"); - var exists = minio.bucketExists(BucketExistsArgs.builder().bucket(option.bucket()).build()); - if (!exists) throw new IllegalStateException("Bucket '" + option.bucket() + "' does not exist"); + // 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 @@ -55,11 +53,10 @@ public class S3BackupStrategy implements BackupStrategy { do { try { var object = option.prefix() + "/" + pathToBundle.getFileName().toString(); - minio.uploadObject(UploadObjectArgs.builder() + s3.putObject(o -> o.bucket(option.bucket()) .contentType("application/octet-stream") - .object(object) - .filename(pathToBundle.toString()) - .build()); + .ifNoneMatch("*") // to avoid content overriding + .key(object).build(), pathToBundle); //todo also calculate sha? return new Backup(pathToBundle.getFileName().toString(), object, "s3", @@ -82,15 +79,22 @@ public class S3BackupStrategy implements BackupStrategy { } @Override + @SneakyThrows public void recoverBackup(Backup backup, Path restorePath) { - //todo http range + 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); + Bundle.unbundleFile(target, restorePath); + } finally { + Files.deleteIfExists(target); + } } @Override @SneakyThrows public void deleteBackup(Backup backup) { - minio.removeObject(RemoveObjectArgs.builder() - .bucket(backup.backupKey()).build()); + s3.deleteObject(b -> b.bucket(option.bucket()).key(backup.backupKey()).build()); } @Override @@ -101,4 +105,9 @@ public class S3BackupStrategy implements BackupStrategy { } return true; } + + @Override + public void close() { + s3.close(); + } }