add imageuploader interface, server uploads expire

This commit is contained in:
1e99 2024-12-16 15:52:05 +01:00
parent 7ed8a3c7e3
commit 347b64cbc8
10 changed files with 275 additions and 111 deletions

View file

@ -1,24 +1,27 @@
package eu.e99.pixelchat.fabric; package eu.e99.pixelchat.fabric;
import eu.e99.pixelchat.fabric.image.ImageUploader; 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.fabricmc.api.ClientModInitializer;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.net.URI;
public class PixelChat implements ClientModInitializer { public class PixelChat implements ClientModInitializer {
public static final String NAMESPACE = "pixelchat"; public static final String NAMESPACE = "pixelchat";
public static final Logger LOGGER = LoggerFactory.getLogger(NAMESPACE); public static final Logger LOGGER = LoggerFactory.getLogger(NAMESPACE);
public static ImageUploader UPLOADER; public static ImageUploads UPLOADS;
@Override @Override
public void onInitializeClient() { public void onInitializeClient() {
UPLOADER = new ImageUploader( UPLOADS = new ImageUploads(
MinecraftClient.getInstance(), MinecraftClient.getInstance(),
URI.create("http://localhost:3000/") new ImageUploader[]{
new PixelChatUploader("http", "localhost", 3001),
new PixelChatUploader("http", "localhost", 3000)
}
); );
} }
} }

View file

@ -1,95 +1,8 @@
package eu.e99.pixelchat.fabric.image; 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.nio.file.Path;
import java.util.UUID;
public class ImageUploader { public interface ImageUploader {
private final MinecraftClient minecraft; String upload(Path path) throws Throwable;
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<String> 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()));
}
}
} }

View file

@ -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<String> 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()));
}*/
}
}

View file

@ -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<String> 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);
}
}

View file

@ -19,7 +19,7 @@ public abstract class ChatScreenMixin extends Screen {
@Override @Override
public void onFilesDropped(List<Path> paths) { public void onFilesDropped(List<Path> paths) {
for (Path path : paths) { for (Path path : paths) {
PixelChat.UPLOADER.uploadImage(path); PixelChat.UPLOADS.uploadImage(path);
} }
} }
} }

0
server/Dockerfile Normal file
View file

View file

@ -3,24 +3,26 @@ package eu.e99.pixelchat.server;
import eu.e99.pixelchat.server.storage.Storage; import eu.e99.pixelchat.server.storage.Storage;
import io.javalin.http.Context; import io.javalin.http.Context;
import io.javalin.http.HttpStatus; 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.ImageFormats;
import org.apache.commons.imaging.Imaging; import org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.internal.ImageParserFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.time.Instant;
import java.time.temporal.TemporalAmount;
public class ImageHandler { public class ImageHandler {
private final Logger logger;
private final Storage storage; 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.storage = storage;
this.expireTime = expireTime;
} }
public void uploadImage(Context ctx) throws IOException { public void uploadImage(Context ctx) throws IOException {
@ -28,11 +30,12 @@ public class ImageHandler {
BufferedImage image = Imaging.getBufferedImage(body); BufferedImage image = Imaging.getBufferedImage(body);
byte[] png = Imaging.writeImageToBytes(image, ImageFormats.PNG); 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 { public void downloadImage(Context ctx) throws IOException {

View file

@ -3,16 +3,48 @@ package eu.e99.pixelchat.server;
import eu.e99.pixelchat.server.storage.MemoryStorage; import eu.e99.pixelchat.server.storage.MemoryStorage;
import eu.e99.pixelchat.server.storage.Storage; import eu.e99.pixelchat.server.storage.Storage;
import io.javalin.Javalin; 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 class Main {
public static void main(String[] args) { public static void main(String[] args) {
Storage storage = new MemoryStorage(); run(
ImageHandler handler = new ImageHandler(storage); 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() Javalin app = Javalin.create()
.post("/", handler::uploadImage) .post("/", handler::uploadImage)
.get("/{id}", handler::downloadImage) .get("/{id}", handler::downloadImage)
.start(3000); .start(port);
} }
} }

View file

@ -1,11 +1,13 @@
package eu.e99.pixelchat.server.storage; package eu.e99.pixelchat.server.storage;
import java.io.IOException;
import java.time.Instant;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
public class MemoryStorage implements Storage { public class MemoryStorage implements Storage {
private final Map<String, byte[]> storage; private final Map<String, Entry> storage;
private int counter; private int counter;
public MemoryStorage() { public MemoryStorage() {
@ -14,14 +16,42 @@ public class MemoryStorage implements Storage {
} }
@Override @Override
public synchronized String put(byte[] bytes) { public synchronized String put(byte[] bytes, Instant expiresAt) {
String id = Integer.toHexString(this.counter++); String id = Integer.toHexString(this.counter++);
this.storage.put(id, bytes); this.storage.put(id, new Entry(bytes, expiresAt));
return id; return id;
} }
@Override @Override
public synchronized byte[] get(String id) { 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<String, 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
) {}
}

View file

@ -1,10 +1,13 @@
package eu.e99.pixelchat.server.storage; package eu.e99.pixelchat.server.storage;
import java.io.IOException; import java.io.IOException;
import java.time.Instant;
public interface Storage { public interface Storage {
String put(byte[] bytes) throws IOException; String put(byte[] bytes, Instant expiresAt) throws IOException;
byte[] get(String id) throws IOException; byte[] get(String id) throws IOException;
void clearExpired() throws IOException;
} }