From 347b64cbc85079b7c9feb3a95f9744ba178dca9e Mon Sep 17 00:00:00 2001 From: 1e99 Date: Mon, 16 Dec 2024 15:52:05 +0100 Subject: [PATCH] add imageuploader interface, server uploads expire --- .../eu/e99/pixelchat/fabric/PixelChat.java | 13 +- .../pixelchat/fabric/image/ImageUploader.java | 91 +----------- .../pixelchat/fabric/image/ImageUploads.java | 133 ++++++++++++++++++ .../fabric/image/PixelChatUploader.java | 47 +++++++ .../fabric/mixin/ChatScreenMixin.java | 2 +- server/Dockerfile | 0 .../eu/e99/pixelchat/server/ImageHandler.java | 19 +-- server/src/eu/e99/pixelchat/server/Main.java | 38 ++++- .../server/storage/MemoryStorage.java | 38 ++++- .../e99/pixelchat/server/storage/Storage.java | 5 +- 10 files changed, 275 insertions(+), 111 deletions(-) create mode 100644 mod-fabric/src/eu/e99/pixelchat/fabric/image/ImageUploads.java create mode 100644 mod-fabric/src/eu/e99/pixelchat/fabric/image/PixelChatUploader.java create mode 100644 server/Dockerfile diff --git a/mod-fabric/src/eu/e99/pixelchat/fabric/PixelChat.java b/mod-fabric/src/eu/e99/pixelchat/fabric/PixelChat.java index 3b3eda1..0a2f807 100644 --- a/mod-fabric/src/eu/e99/pixelchat/fabric/PixelChat.java +++ b/mod-fabric/src/eu/e99/pixelchat/fabric/PixelChat.java @@ -1,24 +1,27 @@ package eu.e99.pixelchat.fabric; import eu.e99.pixelchat.fabric.image.ImageUploader; +import eu.e99.pixelchat.fabric.image.ImageUploads; +import eu.e99.pixelchat.fabric.image.PixelChatUploader; import net.fabricmc.api.ClientModInitializer; import net.minecraft.client.MinecraftClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.net.URI; - public class PixelChat implements ClientModInitializer { public static final String NAMESPACE = "pixelchat"; public static final Logger LOGGER = LoggerFactory.getLogger(NAMESPACE); - public static ImageUploader UPLOADER; + public static ImageUploads UPLOADS; @Override public void onInitializeClient() { - UPLOADER = new ImageUploader( + UPLOADS = new ImageUploads( MinecraftClient.getInstance(), - URI.create("http://localhost:3000/") + new ImageUploader[]{ + new PixelChatUploader("http", "localhost", 3001), + new PixelChatUploader("http", "localhost", 3000) + } ); } } diff --git a/mod-fabric/src/eu/e99/pixelchat/fabric/image/ImageUploader.java b/mod-fabric/src/eu/e99/pixelchat/fabric/image/ImageUploader.java index f332bcd..a3e211c 100644 --- a/mod-fabric/src/eu/e99/pixelchat/fabric/image/ImageUploader.java +++ b/mod-fabric/src/eu/e99/pixelchat/fabric/image/ImageUploader.java @@ -1,95 +1,8 @@ package eu.e99.pixelchat.fabric.image; -import eu.e99.pixelchat.fabric.PixelChat; -import eu.e99.pixelchat.fabric.duck.BossBars; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.hud.ClientBossBar; -import net.minecraft.entity.boss.BossBar; -import net.minecraft.text.Text; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; import java.nio.file.Path; -import java.util.UUID; -public class ImageUploader { +public interface ImageUploader { - private final MinecraftClient minecraft; - private final URI endpoint; - private final HttpClient client; - - public ImageUploader(MinecraftClient minecraft, URI endpoint) { - this.minecraft = minecraft; - this.endpoint = endpoint; - this.client = HttpClient.newHttpClient(); - } - - public void uploadImage(Path path) { - Thread.ofVirtual().start(() -> this.innerUploadImage(path)); - } - - private void innerUploadImage(Path path) { - BossBars bossBars = (BossBars) minecraft.inGameHud.getBossBarHud(); - ClientBossBar bossBar = new ClientBossBar( - UUID.randomUUID(), - Text.translatable("pixelchat.uploading"), - 0f, - BossBar.Color.BLUE, - BossBar.Style.PROGRESS, - false, - false, - false - ); - - try { - this.minecraft.send(() -> bossBars.add(bossBar.getUuid(), bossBar)); - - // TODO: Report some sort of progress? - HttpRequest req = HttpRequest.newBuilder() - .POST(HttpRequest.BodyPublishers.ofFile(path)) - .uri(this.endpoint) - .build(); - - HttpResponse res = this.client.send( - req, - HttpResponse.BodyHandlers.ofString() - ); - if (res.statusCode() != 201) { - throw new RuntimeException(String.format("Failed to upload, expected status 201, got %d", res.statusCode())); - } - - String downloadUrl = res.body(); - - this.minecraft.send(() -> { - // TODO: Are we actually playing? - if (this.minecraft.player == null) { - return; - } - - bossBar.setName(Text.translatable("pixelchat.upload_completed")); - bossBar.setPercent(1.0f); - bossBar.setColor(BossBar.Color.GREEN); - - this.minecraft.inGameHud.getChatHud().addToMessageHistory(downloadUrl); - this.minecraft.player.networkHandler.sendChatMessage(downloadUrl); - }); - } catch (Throwable e) { - PixelChat.LOGGER.error("Upload failed", e); - - this.minecraft.send(() -> { - bossBar.setName(Text.translatable("pixelchat.upload_failed")); - bossBar.setPercent(1.0f); - bossBar.setColor(BossBar.Color.RED); - }); - } finally { - try { - Thread.sleep(5000); - } catch (InterruptedException ignored) { - } - - this.minecraft.send(() -> bossBars.remove(bossBar.getUuid())); - } - } + String upload(Path path) throws Throwable; } diff --git a/mod-fabric/src/eu/e99/pixelchat/fabric/image/ImageUploads.java b/mod-fabric/src/eu/e99/pixelchat/fabric/image/ImageUploads.java new file mode 100644 index 0000000..c3bf045 --- /dev/null +++ b/mod-fabric/src/eu/e99/pixelchat/fabric/image/ImageUploads.java @@ -0,0 +1,133 @@ +package eu.e99.pixelchat.fabric.image; + +import eu.e99.pixelchat.fabric.PixelChat; +import eu.e99.pixelchat.fabric.duck.BossBars; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.hud.ClientBossBar; +import net.minecraft.entity.boss.BossBar; +import net.minecraft.text.Text; + +import java.nio.file.Path; +import java.util.UUID; + +public class ImageUploads { + + private final MinecraftClient client; + private final ImageUploader[] uploaders; + + public ImageUploads(MinecraftClient client, ImageUploader[] uploaders) { + this.client = client; + this.uploaders = uploaders; + } + + public void uploadImage(Path path) { + Thread.ofVirtual().start(() -> this.innerUploadImage(path)); + } + + private void innerUploadImage(Path path) { + BossBars bossBars = (BossBars) client.inGameHud.getBossBarHud(); + ClientBossBar bossBar = new ClientBossBar( + UUID.randomUUID(), + Text.translatable("pixelchat.uploading"), + 0f, + BossBar.Color.BLUE, + BossBar.Style.PROGRESS, + false, + false, + false + ); + + this.client.send(() -> bossBars.add(bossBar.getUuid(), bossBar)); + + String url = null; + for (ImageUploader uploader : this.uploaders) { + try { + url = uploader.upload(path); + break; + } catch (Throwable e) { + PixelChat.LOGGER.warn("Failed to upload", e); + } + } + + // IntelliJ complains otherwise + String finalUrl = url; + this.client.send(() -> { + if (finalUrl == null) { + PixelChat.LOGGER.error("No uploader succeeded"); + bossBar.setName(Text.translatable("pixelchat.upload_failed")); + bossBar.setPercent(1.0f); + bossBar.setColor(BossBar.Color.RED); + return; + } + + bossBar.setName(Text.translatable("pixelchat.upload_completed")); + bossBar.setPercent(1.0f); + bossBar.setColor(BossBar.Color.GREEN); + + // TODO: Are we actually playing? + if (this.client.player == null) { + return; + } + + this.client.inGameHud.getChatHud().addToMessageHistory(finalUrl); + this.client.player.networkHandler.sendChatMessage(finalUrl); + }); + + try { + Thread.sleep(5000); + } catch (InterruptedException ignored) { + } + + this.client.send(() -> bossBars.remove(bossBar.getUuid())); + + /*try { + this.minecraft.send(() -> bossBars.add(bossBar.getUuid(), bossBar)); + + // TODO: Report some sort of progress? + HttpRequest req = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofFile(path)) + .uri(this.endpoint) + .build(); + + HttpResponse res = this.client.send( + req, + HttpResponse.BodyHandlers.ofString() + ); + if (res.statusCode() != 201) { + throw new RuntimeException(String.format("Failed to upload, expected status 201, got %d", res.statusCode())); + } + + this.minecraft.send(() -> { + // TODO: Are we actually playing? + if (this.minecraft.player == null) { + return; + } + + bossBar.setName(Text.translatable("pixelchat.upload_completed")); + bossBar.setPercent(1.0f); + bossBar.setColor(BossBar.Color.GREEN); + + String id = res.body(); + String url = String.format("%s", id); + + this.minecraft.inGameHud.getChatHud().addToMessageHistory(url); + this.minecraft.player.networkHandler.sendChatMessage(url); + }); + } catch (Throwable e) { + PixelChat.LOGGER.error("Upload failed", e); + + this.minecraft.send(() -> { + bossBar.setName(Text.translatable("pixelchat.upload_failed")); + bossBar.setPercent(1.0f); + bossBar.setColor(BossBar.Color.RED); + }); + } finally { + try { + Thread.sleep(5000); + } catch (InterruptedException ignored) { + } + + this.minecraft.send(() -> bossBars.remove(bossBar.getUuid())); + }*/ + } +} diff --git a/mod-fabric/src/eu/e99/pixelchat/fabric/image/PixelChatUploader.java b/mod-fabric/src/eu/e99/pixelchat/fabric/image/PixelChatUploader.java new file mode 100644 index 0000000..263ae07 --- /dev/null +++ b/mod-fabric/src/eu/e99/pixelchat/fabric/image/PixelChatUploader.java @@ -0,0 +1,47 @@ +package eu.e99.pixelchat.fabric.image; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Path; + +public class PixelChatUploader implements ImageUploader { + + private final HttpClient client; + private final String scheme; + private final String host; + private final int port; + + public PixelChatUploader(String scheme, String host, int port) { + this.client = HttpClient.newHttpClient(); + this.scheme = scheme; + this.host = host; + this.port = port; + } + + @Override + public String upload(Path path) throws Throwable { + String uri = this.url(""); + HttpRequest req = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofFile(path)) + .uri(URI.create(uri)) + .build(); + + HttpResponse res = this.client.send( + req, + HttpResponse.BodyHandlers.ofString() + ); + if (res.statusCode() != 201) { + throw new RuntimeException(String.format("Failed to upload, expected status 201, got %d", res.statusCode())); + } + + String id = res.body(); + return this.url(id); + } + + private String url(String path) { + return String.format("%s://%s:%d/%s", this.scheme, this.host, this.port, path); + } +} diff --git a/mod-fabric/src/eu/e99/pixelchat/fabric/mixin/ChatScreenMixin.java b/mod-fabric/src/eu/e99/pixelchat/fabric/mixin/ChatScreenMixin.java index a03fa01..dc78c86 100644 --- a/mod-fabric/src/eu/e99/pixelchat/fabric/mixin/ChatScreenMixin.java +++ b/mod-fabric/src/eu/e99/pixelchat/fabric/mixin/ChatScreenMixin.java @@ -19,7 +19,7 @@ public abstract class ChatScreenMixin extends Screen { @Override public void onFilesDropped(List paths) { for (Path path : paths) { - PixelChat.UPLOADER.uploadImage(path); + PixelChat.UPLOADS.uploadImage(path); } } } diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/server/src/eu/e99/pixelchat/server/ImageHandler.java b/server/src/eu/e99/pixelchat/server/ImageHandler.java index aa1be08..a01a7dc 100644 --- a/server/src/eu/e99/pixelchat/server/ImageHandler.java +++ b/server/src/eu/e99/pixelchat/server/ImageHandler.java @@ -3,24 +3,26 @@ package eu.e99.pixelchat.server; import eu.e99.pixelchat.server.storage.Storage; import io.javalin.http.Context; import io.javalin.http.HttpStatus; -import org.apache.commons.imaging.AbstractImageParser; -import org.apache.commons.imaging.ImageFormat; import org.apache.commons.imaging.ImageFormats; import org.apache.commons.imaging.Imaging; -import org.apache.commons.imaging.internal.ImageParserFactory; import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; +import java.time.Instant; +import java.time.temporal.TemporalAmount; public class ImageHandler { + private final Logger logger; private final Storage storage; + private final TemporalAmount expireTime; - public ImageHandler(Storage storage) { + public ImageHandler(Logger logger, Storage storage, TemporalAmount expireTime) { + this.logger = logger; this.storage = storage; + this.expireTime = expireTime; } public void uploadImage(Context ctx) throws IOException { @@ -28,11 +30,12 @@ public class ImageHandler { BufferedImage image = Imaging.getBufferedImage(body); byte[] png = Imaging.writeImageToBytes(image, ImageFormats.PNG); - String id = storage.put(png); + Instant expiresAt = Instant.now().plus(this.expireTime); - String url = String.format("http://localhost:3000/%s", id); + String id = storage.put(png, expiresAt); + logger.info("Stored {} bytes as {}", png.length, id); - ctx.status(HttpStatus.CREATED).result(url); + ctx.status(HttpStatus.CREATED).result(id); } public void downloadImage(Context ctx) throws IOException { diff --git a/server/src/eu/e99/pixelchat/server/Main.java b/server/src/eu/e99/pixelchat/server/Main.java index ccc177a..3b17717 100644 --- a/server/src/eu/e99/pixelchat/server/Main.java +++ b/server/src/eu/e99/pixelchat/server/Main.java @@ -3,16 +3,48 @@ package eu.e99.pixelchat.server; import eu.e99.pixelchat.server.storage.MemoryStorage; import eu.e99.pixelchat.server.storage.Storage; import io.javalin.Javalin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.time.temporal.TemporalAmount; public class Main { public static void main(String[] args) { - Storage storage = new MemoryStorage(); - ImageHandler handler = new ImageHandler(storage); + run( + LoggerFactory.getLogger("pixelchat"), + new MemoryStorage(), + Duration.ofSeconds(5), + Duration.ofMinutes(2), + 3000 + ); + } + private static void run(Logger logger, Storage storage, Duration clearTime, TemporalAmount expireTime, int port) { + ImageHandler handler = new ImageHandler( + logger, + storage, + expireTime + ); + + Thread.ofVirtual().start(() -> { + while (true) { + try { + Thread.sleep(clearTime); + storage.clearExpired(); + + logger.info("Cleared expired images"); + } catch (Throwable e) { + logger.error("Failed to clear expired images", e); + } + } + }); + + logger.info("PixelChat starting on port {}", port); Javalin app = Javalin.create() .post("/", handler::uploadImage) .get("/{id}", handler::downloadImage) - .start(3000); + .start(port); } } diff --git a/server/src/eu/e99/pixelchat/server/storage/MemoryStorage.java b/server/src/eu/e99/pixelchat/server/storage/MemoryStorage.java index 3925bba..96ffd6d 100644 --- a/server/src/eu/e99/pixelchat/server/storage/MemoryStorage.java +++ b/server/src/eu/e99/pixelchat/server/storage/MemoryStorage.java @@ -1,11 +1,13 @@ package eu.e99.pixelchat.server.storage; +import java.io.IOException; +import java.time.Instant; import java.util.HashMap; import java.util.Map; public class MemoryStorage implements Storage { - private final Map storage; + private final Map storage; private int counter; public MemoryStorage() { @@ -14,14 +16,42 @@ public class MemoryStorage implements Storage { } @Override - public synchronized String put(byte[] bytes) { + public synchronized String put(byte[] bytes, Instant expiresAt) { String id = Integer.toHexString(this.counter++); - this.storage.put(id, bytes); + this.storage.put(id, new Entry(bytes, expiresAt)); return id; } @Override public synchronized byte[] get(String id) { - return this.storage.get(id); + Entry entry = this.storage.get(id); + if (entry == null) { + return null; + } + + return entry.value; } + + @Override + public synchronized void clearExpired() throws IOException { + Instant instant = Instant.now(); + + for (Map.Entry mapEntry : this.storage.entrySet()) { + String id = mapEntry.getKey(); + Entry entry = mapEntry.getValue(); + + if (instant.isBefore(entry.expiresAt)) { + continue; + } + + System.out.printf("Cleared %s%n", id); + + this.storage.remove(id); + } + } + + private record Entry( + byte[] value, + Instant expiresAt + ) {} } diff --git a/server/src/eu/e99/pixelchat/server/storage/Storage.java b/server/src/eu/e99/pixelchat/server/storage/Storage.java index 3bb4dea..3b3b924 100644 --- a/server/src/eu/e99/pixelchat/server/storage/Storage.java +++ b/server/src/eu/e99/pixelchat/server/storage/Storage.java @@ -1,10 +1,13 @@ package eu.e99.pixelchat.server.storage; import java.io.IOException; +import java.time.Instant; public interface Storage { - String put(byte[] bytes) throws IOException; + String put(byte[] bytes, Instant expiresAt) throws IOException; byte[] get(String id) throws IOException; + + void clearExpired() throws IOException; }