add imageuploader interface, server uploads expire
This commit is contained in:
parent
7ed8a3c7e3
commit
347b64cbc8
10 changed files with 275 additions and 111 deletions
|
@ -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)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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()));
|
||||
}
|
||||
}
|
||||
String upload(Path path) throws Throwable;
|
||||
}
|
||||
|
|
133
mod-fabric/src/eu/e99/pixelchat/fabric/image/ImageUploads.java
Normal file
133
mod-fabric/src/eu/e99/pixelchat/fabric/image/ImageUploads.java
Normal 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()));
|
||||
}*/
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ public abstract class ChatScreenMixin extends Screen {
|
|||
@Override
|
||||
public void onFilesDropped(List<Path> paths) {
|
||||
for (Path path : paths) {
|
||||
PixelChat.UPLOADER.uploadImage(path);
|
||||
PixelChat.UPLOADS.uploadImage(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
0
server/Dockerfile
Normal file
0
server/Dockerfile
Normal file
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, byte[]> storage;
|
||||
private final Map<String, Entry> 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<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
|
||||
) {}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue