diff --git a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java index 6318e3de7..03a25a774 100644 --- a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java +++ b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java @@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.net.URL; +import java.nio.file.Path; import java.util.LinkedList; import java.util.List; @@ -75,6 +76,8 @@ public final class DiscoveredExtension { /** The original jar this is from. */ transient private File originalJar; + transient private Path dataDirectory; + /** The class loader that powers it. */ transient private MinestomExtensionClassLoader minestomExtensionClassLoader; @@ -130,6 +133,15 @@ public final class DiscoveredExtension { return originalJar; } + @NotNull + public Path getDataDirectory() { + return dataDirectory; + } + + public void setDataDirectory(@NotNull Path dataDirectory) { + this.dataDirectory = dataDirectory; + } + MinestomExtensionClassLoader removeMinestomExtensionClassLoader() { MinestomExtensionClassLoader oldClassLoader = getMinestomExtensionClassLoader(); setMinestomExtensionClassLoader(null); diff --git a/src/main/java/net/minestom/server/extensions/Extension.java b/src/main/java/net/minestom/server/extensions/Extension.java index cd9b52028..cd43cfe44 100644 --- a/src/main/java/net/minestom/server/extensions/Extension.java +++ b/src/main/java/net/minestom/server/extensions/Extension.java @@ -1,11 +1,19 @@ package net.minestom.server.extensions; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; +import java.io.IOException; +import java.io.InputStream; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; @@ -77,6 +85,122 @@ public abstract class Extension { return logger; } + @NotNull + public Path getDataDirectory() { + return getOrigin().getDataDirectory(); + } + + /** + * Gets a resource from the extension directory, or from inside the jar if it does not + * exist in the extension directory. + *

+ * If it does not exist in the extension directory, it will be copied from inside the jar. + *

+ * The caller is responsible for closing the returned {@link InputStream}. + * + * @param fileName The file to read + * @return The file contents, or null if there was an issue reading the file. + */ + @Nullable + public InputStream getResource(@NotNull String fileName) { + return getResource(Paths.get(fileName)); + } + + /** + * Gets a resource from the extension directory, or from inside the jar if it does not + * exist in the extension directory. + *

+ * If it does not exist in the extension directory, it will be copied from inside the jar. + *

+ * The caller is responsible for closing the returned {@link InputStream}. + * + * @param target The file to read + * @return The file contents, or null if there was an issue reading the file. + */ + @Nullable + public InputStream getResource(@NotNull Path target) { + final Path targetFile = getDataDirectory().resolve(target); + try { + // Copy from jar if the file does not exist in the extension data directory. + if (!Files.exists(targetFile)) { + savePackagedResource(target); + } + + return Files.newInputStream(targetFile); + } catch (IOException ex) { + getLogger().info("Failed to read resource {}.", target, ex); + return null; + } + } + + /** + * Gets a resource from inside the extension jar. + *

+ * The caller is responsible for closing the returned {@link InputStream}. + * + * @param fileName The file to read + * @return The file contents, or null if there was an issue reading the file. + */ + public InputStream getPackagedResource(@NotNull String fileName) { + return getPackagedResource(Paths.get(fileName)); + } + + /** + * Gets a resource from inside the extension jar. + *

+ * The caller is responsible for closing the returned {@link InputStream}. + * + * @param target The file to read + * @return The file contents, or null if there was an issue reading the file. + */ + @Nullable + public InputStream getPackagedResource(@NotNull Path target) { + try { + final URL url = getOrigin().getMinestomExtensionClassLoader().getResource(target.toString()); + if (url == null) { + getLogger().debug("Resource not found: {}", target); + return null; + } + + return url.openConnection().getInputStream(); + } catch (IOException ex) { + getLogger().debug("Failed to load resource {}.", target, ex); + return null; + } + } + + /** + * Copies a resource file to the extension directory, replacing any existing copy. + * + * @param fileName The resource to save + * @return True if the resource was saved successfully, null otherwise + */ + public boolean savePackagedResource(@NotNull String fileName) { + return savePackagedResource(Paths.get(fileName)); + } + + /** + * Copies a resource file to the extension directory, replacing any existing copy. + * + * @param target The resource to save + * @return True if the resource was saved successfully, null otherwise + */ + public boolean savePackagedResource(@NotNull Path target) { + final Path targetFile = getDataDirectory().resolve(target); + try (InputStream is = getPackagedResource(target)) { + if (is == null) { + return false; + } + + Files.createDirectories(targetFile.getParent()); + Files.copy(is, targetFile, StandardCopyOption.REPLACE_EXISTING); + return true; + } catch (IOException ex) { + getLogger().debug("Failed to save resource {}.", target, ex); + return false; + } + } + /** * Adds a new observer to this extension. * Will be kept as a WeakReference. diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index 2de27ece5..b9ad3b0b9 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -24,6 +24,8 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.*; import java.util.stream.Collectors; import java.util.zip.ZipFile; @@ -44,6 +46,7 @@ public class ExtensionManager { private final File extensionFolder = new File("extensions"); private final File dependenciesFolder = new File(extensionFolder, ".libs"); + private Path extensionDataRoot = extensionFolder.toPath(); private boolean loaded; // Option @@ -336,6 +339,7 @@ public class ExtensionManager { DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); extension.files.add(new File(extensionClasses).toURI().toURL()); extension.files.add(new File(extensionResources).toURI().toURL()); + extension.setDataDirectory(getExtensionDataRoot().resolve(extension.getName())); // Verify integrity and ensure defaults DiscoveredExtension.verifyIntegrity(extension); @@ -365,6 +369,7 @@ public class ExtensionManager { DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); extension.setOriginalJar(file); extension.files.add(file.toURI().toURL()); + extension.setDataDirectory(getExtensionDataRoot().resolve(extension.getName())); // Verify integrity and ensure defaults DiscoveredExtension.verifyIntegrity(extension); @@ -569,6 +574,15 @@ public class ExtensionManager { return extensionFolder; } + @NotNull + public Path getExtensionDataRoot() { + return extensionDataRoot; + } + + public void setExtensionDataRoot(@NotNull Path dataRoot) { + this.extensionDataRoot = dataRoot; + } + @NotNull public Collection getExtensions() { return immutableExtensions.values();