From a087eb62b5a79e3f5b05676ce9cbd70c89c8ba9e Mon Sep 17 00:00:00 2001 From: Aaron <51387595+AzureAaron@users.noreply.github.com> Date: Tue, 27 Feb 2024 15:44:55 -0500 Subject: Image Repo Loader --- .../de/hysky/skyblocker/config/ConfigUtils.java | 21 +++ .../hysky/skyblocker/config/ImageRepoLoader.java | 144 +++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 src/main/java/de/hysky/skyblocker/config/ImageRepoLoader.java (limited to 'src/main/java/de/hysky/skyblocker/config') diff --git a/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java b/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java index 8b0f27a7..781f7f15 100644 --- a/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java +++ b/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java @@ -1,16 +1,26 @@ package de.hysky.skyblocker.config; import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.OptionDescription; import dev.isxander.yacl3.api.controller.*; import net.minecraft.text.Text; import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.FileUtils; +import java.nio.file.Path; import java.util.function.Function; public class ConfigUtils { public static final ValueFormatter FORMATTING_FORMATTER = formatting -> Text.literal(StringUtils.capitalize(formatting.getName().replaceAll("_", " "))); public static final ValueFormatter FLOAT_TWO_FORMATTER = value -> Text.literal(String.format("%,.2f", value).replaceAll("[\u00a0\u202F]", " ")); + private static final Path IMAGE_DIRECTORY = ImageRepoLoader.REPO_DIRECTORY.resolve("Skyblocker-Assets-images"); public static BooleanControllerBuilder createBooleanController(Option opt) { return BooleanControllerBuilder.create(opt).yesNoFormatter().coloured(true); @@ -34,4 +44,15 @@ public class ConfigUtils { public static > Function, ControllerBuilder> getEnumDropdownControllerFactory(ValueFormatter formatter) { return opt -> EnumDropdownControllerBuilder.create(opt).formatValue(formatter); } + + /** + * Creates an {@link OptionDescription} with an image and text. + */ + @SafeVarargs + public static OptionDescription withImage(Path imagePath, @Nullable Text... texts) { + return OptionDescription.createBuilder() + .text(ArrayUtils.isNotEmpty(texts) ? texts : new Text[] {}) + .image(IMAGE_DIRECTORY.resolve(imagePath), new Identifier(SkyblockerMod.NAMESPACE, "config_image_" + FileUtils.normalizePath(imagePath))) + .build(); + } } diff --git a/src/main/java/de/hysky/skyblocker/config/ImageRepoLoader.java b/src/main/java/de/hysky/skyblocker/config/ImageRepoLoader.java new file mode 100644 index 00000000..0591cd96 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/config/ImageRepoLoader.java @@ -0,0 +1,144 @@ +package de.hysky.skyblocker.config; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.CompletableFuture; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.slf4j.Logger; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.FileUtils; +import de.hysky.skyblocker.utils.Http; + +public class ImageRepoLoader { + private static final Logger LOGGER = LogUtils.getLogger(); + static final Path REPO_DIRECTORY = SkyblockerMod.CONFIG_DIR.resolve("image-repo"); + private static final String BRANCH_INFO = "https://api.github.com/repos/SkyblockerMod/Skyblocker-Assets/branches/images"; + private static final String REPO_DOWNLOAD = "https://github.com/SkyblockerMod/Skyblocker-Assets/archive/refs/heads/images.zip"; + private static final String PLACEHOLDER_HASH = "None!"; + + public static void init() { + update(0); + } + + /** + * Attempts to update/load the image repository, if any errors are encountered it will try 3 times. + */ + private static void update(int retries) { + CompletableFuture.runAsync(() -> { + if (retries < 3) { + try { + long start = System.currentTimeMillis(); + //Retrieve the saved commit hash + String savedCommitHash = checkSavedCommitData(); + + //Fetch the latest commit data + JsonObject response = JsonParser.parseString(Http.sendGetRequest(BRANCH_INFO)).getAsJsonObject(); + String latestCommitHash = response.getAsJsonObject("commit").get("sha").getAsString(); + + //Download the repository if there was a new commit + if (!savedCommitHash.equals(latestCommitHash)) { + InputStream in = Http.downloadContent(REPO_DOWNLOAD); + + //Delete all directories to clear potentially now unused/old files + //TODO change this to only delete periodically? + deleteDirectories(); + + try (ZipInputStream zis = new ZipInputStream(in)) { + ZipEntry entry; + + while ((entry = zis.getNextEntry()) != null) { + Path outputFile = REPO_DIRECTORY.resolve(entry.getName()); + + if (entry.isDirectory()) { + Files.createDirectories(outputFile); + } else { + Files.createDirectories(outputFile.getParent()); + Files.copy(zis, outputFile, StandardCopyOption.REPLACE_EXISTING); + } + } + } + + writeCommitData(latestCommitHash); + + long end = System.currentTimeMillis(); + LOGGER.info("[Skyblocker] Successfully updated the Image Respository in {} ms! {} → {}", end - start, savedCommitHash, latestCommitHash); + } else { + LOGGER.info("[Skyblocker] The Image Respository is up to date!"); + } + } catch (Exception e) { + LOGGER.error("[Skyblocker] Error while downloading image repo on attempt {}!", retries, e); + update(retries + 1); + } + } + }); + } + + /** + * @return The stored hash or the {@link #PLACEHOLDER_HASH}. + */ + private static String checkSavedCommitData() throws IOException { + Path file = REPO_DIRECTORY.resolve("image_repo.json"); + + if (Files.exists(file)) { + try (BufferedReader reader = Files.newBufferedReader(file)) { + CommitData commitData = CommitData.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).result().orElseThrow(); + + return commitData.commit(); + } + } + + return PLACEHOLDER_HASH; + } + + /** + * Writes the {@code newHash} into a file to be used to check for repo updates. + * + * @implNote Checking whether the directory exists or not isn't needed as this is called after all files are written successfully. + */ + private static void writeCommitData(String newHash) throws IOException { + Path file = REPO_DIRECTORY.resolve("image_repo.json"); + CommitData commitData = new CommitData(newHash, System.currentTimeMillis()); + + try (BufferedWriter writer = Files.newBufferedWriter(file)) { + SkyblockerMod.GSON.toJson(CommitData.CODEC.encodeStart(JsonOps.INSTANCE, commitData).result().orElseThrow(), writer); + } + } + + /** + * Deletes all directories (not files) inside of the {@link #REPO_DIRECTORY} + * @throws IOException + */ + private static void deleteDirectories() throws IOException { + Files.list(REPO_DIRECTORY) + .filter(Files::isDirectory) + .forEach(dir -> { + try { + FileUtils.recursiveDelete(dir); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Encountered an exception while deleting a directory! Path: {}", dir.toAbsolutePath(), e); + } + }); + } + + record CommitData(String commit, long lastUpdated) { + static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.STRING.fieldOf("commit").forGetter(CommitData::commit), + Codec.LONG.fieldOf("lastUpdated").forGetter(CommitData::lastUpdated)) + .apply(instance, CommitData::new)); + } +} -- cgit