From 1024cbf9b2310b605a2eb8878be5dcd8eb68c184 Mon Sep 17 00:00:00 2001 From: Vinrobot Date: Sun, 19 Nov 2023 22:18:41 +0100 Subject: [PATCH] Implement file http cache storage Does not limit cache size --- build.gradle | 3 + gradle.properties | 1 + .../mcemote/client/imageio/BufferedFrame.java | 8 +- .../mcemote/client/imageio/NativeImageIO.java | 13 ++- .../mcemote/client/providers/BTTVEmote.java | 4 +- .../mcemote/client/providers/FFZEmote.java | 4 +- .../client/providers/SevenTVEmote.java | 6 +- .../net/vinrobot/mcemote/MinecraftEmote.java | 20 +++++ .../mcemote/http/FileHttpCacheStorage.java | 89 +++++++++++++++++++ 9 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 src/main/java/net/vinrobot/mcemote/http/FileHttpCacheStorage.java diff --git a/build.gradle b/build.gradle index c9639d6..ef811b9 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,9 @@ dependencies { // WebP support for animated WebP using official Google's native library modClientImplementation "com.github.Vinrobot.WebPDecoderJN:lib:${project.webpdecoderjn_version}" include "com.github.Vinrobot.WebPDecoderJN:lib:${rootProject.webpdecoderjn_version}" + + modImplementation "org.apache.httpcomponents:httpclient-cache:${project.httpcomponents_version}" + include "org.apache.httpcomponents:httpclient-cache:${project.httpcomponents_version}" } processResources { diff --git a/gradle.properties b/gradle.properties index 0302ec2..71afb0a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,3 +19,4 @@ fabric_api_version=0.83.0+1.20 # Dependencies versions modmenu_version=7.2.1 webpdecoderjn_version=1.4 +httpcomponents_version=4.5.14 diff --git a/src/client/java/net/vinrobot/mcemote/client/imageio/BufferedFrame.java b/src/client/java/net/vinrobot/mcemote/client/imageio/BufferedFrame.java index 9ab383b..f93dd07 100644 --- a/src/client/java/net/vinrobot/mcemote/client/imageio/BufferedFrame.java +++ b/src/client/java/net/vinrobot/mcemote/client/imageio/BufferedFrame.java @@ -2,12 +2,18 @@ import java.awt.image.BufferedImage; import java.time.Duration; +import java.util.Objects; public record BufferedFrame( BufferedImage image, Duration duration ) { - NativeFrame toNativeFrame() { + public BufferedFrame { + Objects.requireNonNull(image); + Objects.requireNonNull(duration); + } + + public NativeFrame toNativeFrame() { return new NativeFrame(NativeImageHelper.fromBufferedImage(image), duration); } } diff --git a/src/client/java/net/vinrobot/mcemote/client/imageio/NativeImageIO.java b/src/client/java/net/vinrobot/mcemote/client/imageio/NativeImageIO.java index b69e16b..ad36eec 100644 --- a/src/client/java/net/vinrobot/mcemote/client/imageio/NativeImageIO.java +++ b/src/client/java/net/vinrobot/mcemote/client/imageio/NativeImageIO.java @@ -1,7 +1,11 @@ package net.vinrobot.mcemote.client.imageio; +import net.vinrobot.mcemote.MinecraftEmote; import net.vinrobot.mcemote.client.imageio.plugins.gif.GifReader; import net.vinrobot.mcemote.client.imageio.plugins.webp.WebPReader; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; import javax.imageio.IIOException; import javax.imageio.ImageIO; @@ -9,13 +13,16 @@ import javax.imageio.stream.ImageInputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URL; +import java.net.URI; import java.time.Duration; import java.util.List; public class NativeImageIO { - public static NativeFrame[] readAll(final URL url) throws IOException { - try (final InputStream input = url.openStream()) { + public static NativeFrame[] readAll(final URI uri) throws IOException { + final HttpGet httpGet = new HttpGet(uri); + final CloseableHttpClient client = MinecraftEmote.getInstance().getHttpClient(); + try (final CloseableHttpResponse response = client.execute(httpGet); + final InputStream input = response.getEntity().getContent()) { return readAll(input); } } diff --git a/src/client/java/net/vinrobot/mcemote/client/providers/BTTVEmote.java b/src/client/java/net/vinrobot/mcemote/client/providers/BTTVEmote.java index d94d47b..33a29ed 100644 --- a/src/client/java/net/vinrobot/mcemote/client/providers/BTTVEmote.java +++ b/src/client/java/net/vinrobot/mcemote/client/providers/BTTVEmote.java @@ -7,7 +7,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import java.net.URL; +import java.net.URI; public class BTTVEmote implements net.vinrobot.mcemote.client.font.Emote { private static final Logger LOGGER = LoggerFactory.getLogger(BTTVEmote.class); @@ -38,6 +38,6 @@ public int getHeight() { @Override public NativeFrame[] loadFrames() throws IOException { - return NativeImageIO.readAll(new URL("https://cdn.betterttv.net/emote/" + this.emote.id() + "/1x")); + return NativeImageIO.readAll(URI.create("https://cdn.betterttv.net/emote/" + this.emote.id() + "/1x")); } } diff --git a/src/client/java/net/vinrobot/mcemote/client/providers/FFZEmote.java b/src/client/java/net/vinrobot/mcemote/client/providers/FFZEmote.java index 4a6d69a..d1be757 100644 --- a/src/client/java/net/vinrobot/mcemote/client/providers/FFZEmote.java +++ b/src/client/java/net/vinrobot/mcemote/client/providers/FFZEmote.java @@ -8,7 +8,7 @@ import net.vinrobot.mcemote.client.imageio.NativeImageIO; import java.io.IOException; -import java.net.URL; +import java.net.URI; import java.util.Map; @Environment(EnvType.CLIENT) @@ -38,6 +38,6 @@ public int getHeight() { public NativeFrame[] loadFrames() throws IOException { final Map urls = this.emoticon.urls(); final String url = urls.containsKey("1") ? urls.get("1") : urls.values().iterator().next(); - return NativeImageIO.readAll(new URL(url)); + return NativeImageIO.readAll(URI.create(url)); } } diff --git a/src/client/java/net/vinrobot/mcemote/client/providers/SevenTVEmote.java b/src/client/java/net/vinrobot/mcemote/client/providers/SevenTVEmote.java index d5df5f4..d2271d2 100644 --- a/src/client/java/net/vinrobot/mcemote/client/providers/SevenTVEmote.java +++ b/src/client/java/net/vinrobot/mcemote/client/providers/SevenTVEmote.java @@ -10,7 +10,7 @@ import net.vinrobot.mcemote.client.imageio.NativeImageIO; import java.io.IOException; -import java.net.URL; +import java.net.URI; import java.util.Comparator; @Environment(EnvType.CLIENT) @@ -25,7 +25,7 @@ private EmoteFile getFile() { return this.emote.data().host().files().stream() .filter(f -> "WEBP".equalsIgnoreCase(f.format())) .sorted(Comparator.comparingInt(EmoteFile::size)) - .findFirst().get(); + .findFirst().orElseThrow(); } @Override @@ -51,6 +51,6 @@ public NativeFrame[] loadFrames() throws IOException { final EmoteHost host = data.host(); final EmoteFile file = getFile(); final String url = "https:" + host.url() + "/" + file.name(); - return NativeImageIO.readAll(new URL(url)); + return NativeImageIO.readAll(URI.create(url)); } } diff --git a/src/main/java/net/vinrobot/mcemote/MinecraftEmote.java b/src/main/java/net/vinrobot/mcemote/MinecraftEmote.java index a98d2bb..3128ad4 100644 --- a/src/main/java/net/vinrobot/mcemote/MinecraftEmote.java +++ b/src/main/java/net/vinrobot/mcemote/MinecraftEmote.java @@ -4,6 +4,10 @@ import net.vinrobot.mcemote.config.ConfigurationManager; import net.vinrobot.mcemote.config.ConfigurationService; import net.vinrobot.mcemote.config.file.FileConfigurationService; +import net.vinrobot.mcemote.http.FileHttpCacheStorage; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.cache.CacheConfig; +import org.apache.http.impl.client.cache.CachingHttpClientBuilder; import java.nio.file.Path; @@ -13,15 +17,31 @@ public static MinecraftEmote getInstance() { } private final ConfigurationManager configManager; + private final CloseableHttpClient httpClient; protected MinecraftEmote() { final Path configDir = FabricLoader.getInstance().getConfigDir(); final Path configFile = configDir.resolve("mcemote.json"); final ConfigurationService configService = new FileConfigurationService(configFile); this.configManager = new ConfigurationManager(configService); + + final Path cacheDir = FabricLoader.getInstance().getGameDir() + .resolve("cache") + .resolve("mcemote.httpcache"); + final CacheConfig cacheConfig = CacheConfig.custom() + .setMaxObjectSize(1 << 26) // 64 MiB + .build(); + this.httpClient = CachingHttpClientBuilder.create() + .setCacheConfig(cacheConfig) + .setHttpCacheStorage(new FileHttpCacheStorage(cacheDir)) + .build(); } public ConfigurationManager getConfigManager() { return this.configManager; } + + public CloseableHttpClient getHttpClient() { + return this.httpClient; + } } diff --git a/src/main/java/net/vinrobot/mcemote/http/FileHttpCacheStorage.java b/src/main/java/net/vinrobot/mcemote/http/FileHttpCacheStorage.java new file mode 100644 index 0000000..6912899 --- /dev/null +++ b/src/main/java/net/vinrobot/mcemote/http/FileHttpCacheStorage.java @@ -0,0 +1,89 @@ +package net.vinrobot.mcemote.http; + +import org.apache.http.client.cache.HttpCacheEntry; +import org.apache.http.client.cache.HttpCacheEntrySerializer; +import org.apache.http.client.cache.HttpCacheStorage; +import org.apache.http.client.cache.HttpCacheUpdateCallback; +import org.apache.http.impl.client.cache.DefaultHttpCacheEntrySerializer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Objects; + +import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; + +public class FileHttpCacheStorage implements HttpCacheStorage { + private final HttpCacheEntrySerializer serializer = new DefaultHttpCacheEntrySerializer(); + private final Path cacheDir; + + public FileHttpCacheStorage(final Path cacheDir) { + this.cacheDir = Objects.requireNonNull(cacheDir); + } + + @Override + public void putEntry(final String key, final HttpCacheEntry entry) throws IOException { + final Path file = this.computePath(key); + synchronized (this) { + this.writeCacheEntry(entry, file); + } + } + + @Override + public HttpCacheEntry getEntry(final String key) throws IOException { + final Path file = this.computePath(key); + try { + synchronized (this) { + return this.readCacheEntry(file); + } + } catch (final NoSuchFileException e) { + return null; + } + } + + @Override + public void removeEntry(final String key) throws IOException { + final Path file = this.computePath(key); + synchronized (this) { + Files.deleteIfExists(file); + } + } + + @Override + public void updateEntry(final String key, final HttpCacheUpdateCallback callback) throws IOException { + final Path file = this.computePath(key); + synchronized (this) { + final HttpCacheEntry entry = Files.exists(file) ? this.readCacheEntry(file) : null; + final HttpCacheEntry updated = callback.update(entry); + if (updated == null) { + Files.deleteIfExists(file); + } else { + this.writeCacheEntry(updated, file); + } + } + } + + private void writeCacheEntry(final HttpCacheEntry entry, final Path path) throws IOException { + final Path parentPath = path.getParent(); + if (!Files.exists(parentPath)) { + Files.createDirectories(parentPath); + } + try (final OutputStream os = Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { + this.serializer.writeTo(entry, os); + } + } + + private HttpCacheEntry readCacheEntry(final Path path) throws IOException { + try (final InputStream is = Files.newInputStream(path, StandardOpenOption.READ)) { + return this.serializer.readFrom(is); + } + } + + private Path computePath(final String key) { + return this.cacheDir.resolve(sha256Hex(key)); + } +}