From bfefcd071801e6c73bd5e5ba0dbc2febed50c148 Mon Sep 17 00:00:00 2001 From: Daniel <59575049+lumijiez@users.noreply.github.com> Date: Sun, 3 Nov 2024 23:30:04 +0200 Subject: [PATCH] MULTIPART UPLOAD WORKS --- .../lumijiez/core/http/HttpFileItem.java | 35 ++++++ .../lumijiez/core/http/HttpMultipartData.java | 35 ++++++ .../core/http/HttpMultipartParser.java | 115 ++++++++++++++---- .../lumijiez/core/http/HttpRequest.java | 56 +++++---- .../github/lumijiez/core/logging/Logger.java | 2 +- .../java/io/github/lumijiez/example/Main.java | 41 +++++++ 6 files changed, 235 insertions(+), 49 deletions(-) create mode 100644 src/main/java/io/github/lumijiez/core/http/HttpFileItem.java create mode 100644 src/main/java/io/github/lumijiez/core/http/HttpMultipartData.java diff --git a/src/main/java/io/github/lumijiez/core/http/HttpFileItem.java b/src/main/java/io/github/lumijiez/core/http/HttpFileItem.java new file mode 100644 index 0000000..0df09ed --- /dev/null +++ b/src/main/java/io/github/lumijiez/core/http/HttpFileItem.java @@ -0,0 +1,35 @@ +package io.github.lumijiez.core.http; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +public class HttpFileItem { + private final String fileName; + private final String contentType; + private final byte[] content; + + public HttpFileItem(String fileName, String contentType, byte[] content) { + this.fileName = fileName; + this.contentType = contentType; + this.content = content; + } + + public String getFileName() { + return fileName; + } + + public String getContentType() { + return contentType; + } + + public byte[] getContent() { + return content; + } + + public void saveTo(File destination) throws IOException { + try (FileOutputStream fos = new FileOutputStream(destination)) { + fos.write(content); + } + } +} diff --git a/src/main/java/io/github/lumijiez/core/http/HttpMultipartData.java b/src/main/java/io/github/lumijiez/core/http/HttpMultipartData.java new file mode 100644 index 0000000..8463b8e --- /dev/null +++ b/src/main/java/io/github/lumijiez/core/http/HttpMultipartData.java @@ -0,0 +1,35 @@ +package io.github.lumijiez.core.http; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class HttpMultipartData { + private final Map fields = new HashMap<>(); + private final Map files = new HashMap<>(); + + public void addField(String name, String value) { + fields.put(name, value); + } + + public void addFile(String name, HttpFileItem file) { + files.put(name, file); + } + + public String getField(String name) { + return fields.get(name); + } + + public HttpFileItem getFile(String name) { + return files.get(name); + } + + public Map getFields() { + return Collections.unmodifiableMap(fields); + } + + public Map getFiles() { + return Collections.unmodifiableMap(files); + } +} + diff --git a/src/main/java/io/github/lumijiez/core/http/HttpMultipartParser.java b/src/main/java/io/github/lumijiez/core/http/HttpMultipartParser.java index 60c5dc8..52bc618 100644 --- a/src/main/java/io/github/lumijiez/core/http/HttpMultipartParser.java +++ b/src/main/java/io/github/lumijiez/core/http/HttpMultipartParser.java @@ -1,62 +1,127 @@ package io.github.lumijiez.core.http; +import io.github.lumijiez.core.logging.Logger; + import java.io.BufferedReader; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; public class HttpMultipartParser { - private final String boundary; private final BufferedReader reader; - private final Map parts = new HashMap<>(); + private final String boundary; + private final HttpMultipartData multipartData; + private static final int MAX_BUFFER_SIZE = 1024 * 1024; // 1MB public HttpMultipartParser(BufferedReader reader, String contentType) { this.reader = reader; - this.boundary = extractBoundary(contentType); + this.boundary = "--" + extractBoundary(contentType); + this.multipartData = new HttpMultipartData(); + Logger.debug("HTTP", "Initialized parser with boundary: " + this.boundary); } private String extractBoundary(String contentType) { String[] parts = contentType.split(";"); for (String part : parts) { - if (part.trim().startsWith("boundary=")) { - return "--" + part.split("=")[1].trim(); + part = part.trim(); + if (part.startsWith("boundary=")) { + String trim = part.substring(("boundary=".length())).trim(); + Logger.debug("HTTP", "Boundary: " + trim); + return trim; } } return null; } - public Map parse() throws IOException { + public HttpMultipartData parse() throws IOException { String line; - StringBuilder content = new StringBuilder(); - String currentName = null; + StringBuilder currentPart = new StringBuilder(); + Map currentHeaders = new HashMap<>(); + boolean isReadingHeaders = false; while ((line = reader.readLine()) != null) { - if (line.startsWith(boundary)) { - if (currentName != null) { - parts.put(currentName, content.toString().trim()); - content = new StringBuilder(); - } + Logger.debug("HTTP", "Reading line: '" + line + "'"); - while ((line = reader.readLine()) != null && !line.isEmpty()) { - if (line.toLowerCase().startsWith("content-disposition:")) { - currentName = extractFieldName(line); - } + line = line.trim(); + + if (line.startsWith(boundary + "--")) { + if (!currentHeaders.isEmpty() && !currentPart.isEmpty()) { + processContent(currentHeaders, currentPart.toString()); } - } else if (!line.equals(boundary + "--")) { - content.append(line).append("\n"); + Logger.debug("HTTP", "Found end boundary, finishing parse"); + break; + } + + if (line.startsWith(boundary)) { + if (!currentHeaders.isEmpty() && !currentPart.isEmpty()) { + processContent(currentHeaders, currentPart.toString()); + } + currentPart.setLength(0); + currentHeaders.clear(); + isReadingHeaders = true; + continue; + } + + if (isReadingHeaders) { + if (line.isEmpty()) { + isReadingHeaders = false; + continue; + } + int separator = line.indexOf(':'); + if (separator > 0) { + String headerName = line.substring(0, separator).trim().toLowerCase(); + String headerValue = line.substring(separator + 1).trim(); + currentHeaders.put(headerName, headerValue); + Logger.debug("HTTP", "Found header: " + headerName + " = " + headerValue); + } + } else { + currentPart.append(line).append("\r\n"); } } - return parts; + return multipartData; } - private String extractFieldName(String header) { - String[] parts = header.split(";"); + private void processContent(Map headers, String content) { + String contentDisposition = headers.get("content-disposition"); + if (contentDisposition == null) { + return; + } + + Map dispositionParams = parseContentDisposition(contentDisposition); + String name = dispositionParams.get("name"); + String fileName = dispositionParams.get("filename"); + + if (fileName != null) { + String contentType = headers.getOrDefault("content-type", "application/octet-stream"); + if (content.endsWith("\r\n")) { + content = content.substring(0, content.length() - 2); + } + byte[] fileContent = content.getBytes(StandardCharsets.UTF_8); + HttpFileItem fileItem = new HttpFileItem(fileName, contentType, fileContent); + multipartData.addFile(name, fileItem); + Logger.debug("HTTP", "Added file: " + name + ", filename: " + fileName); + } else { + multipartData.addField(name, content.trim()); + Logger.debug("HTTP", "Added field: " + name); + } + } + + private Map parseContentDisposition(String contentDisposition) { + Map params = new HashMap<>(); + String[] parts = contentDisposition.split(";"); + for (String part : parts) { - if (part.trim().startsWith("name=")) { - return part.split("=")[1].trim().replace("\"", ""); + part = part.trim(); + if (part.contains("=")) { + String[] keyValue = part.split("=", 2); + String key = keyValue[0].trim(); + String value = keyValue[1].trim().replace("\"", ""); + params.put(key, value); } } - return null; + + return params; } } diff --git a/src/main/java/io/github/lumijiez/core/http/HttpRequest.java b/src/main/java/io/github/lumijiez/core/http/HttpRequest.java index 18771eb..27196f3 100644 --- a/src/main/java/io/github/lumijiez/core/http/HttpRequest.java +++ b/src/main/java/io/github/lumijiez/core/http/HttpRequest.java @@ -21,6 +21,7 @@ public class HttpRequest { private UrlParser urlParser; private HttpRequestBody body; private Map formData; + private HttpMultipartData multipartData; private final Map cookies; public HttpRequest(BufferedReader in) throws IOException { @@ -115,32 +116,37 @@ public class HttpRequest { int contentLength = Integer.parseInt(headers.get("content-length")); String contentType = headers.getOrDefault("content-type", "text/plain"); - if ("chunked".equalsIgnoreCase(headers.get("transfer-encoding"))) { - parseChunkedBody(in); - return; - } - - char[] buffer = new char[contentLength]; - int totalRead = 0; - while (totalRead < contentLength) { - int read = in.read(buffer, totalRead, contentLength - totalRead); - if (read == -1) { - throw new IOException("Unexpected end of stream"); - } - totalRead += read; - } - String content = new String(buffer); - if (contentType.startsWith("multipart/form-data")) { + Logger.debug("CONTENT TYPE", contentType); HttpMultipartParser parser = new HttpMultipartParser(in, contentType); - this.formData = parser.parse(); - Logger.debug("HTTP", "Parsed multipart form data: " + formData.size() + " parts"); - } else if (contentType.startsWith("application/x-www-form-urlencoded")) { - this.formData = parseUrlEncodedForm(content); - Logger.debug("HTTP", "Parsed URL encoded form data: " + formData.size() + " fields"); + this.multipartData = parser.parse(); + Logger.debug("HTTP", "Parsed multipart data with " + + multipartData.getFiles().size() + " files and " + + multipartData.getFields().size() + " fields"); } else { - this.body = new HttpRequestBody(content, HttpContentType.fromString(contentType)); - Logger.debug("HTTP", "Parsed body with content type: " + contentType); + if ("chunked".equalsIgnoreCase(headers.get("transfer-encoding"))) { + parseChunkedBody(in); + return; + } + + char[] buffer = new char[contentLength]; + int totalRead = 0; + while (totalRead < contentLength) { + int read = in.read(buffer, totalRead, contentLength - totalRead); + if (read == -1) { + throw new IOException("Unexpected end of stream"); + } + totalRead += read; + } + String content = new String(buffer); + + if (contentType.startsWith("application/x-www-form-urlencoded")) { + this.formData = parseUrlEncodedForm(content); + Logger.debug("HTTP", "Parsed URL encoded form data: " + formData.size() + " fields"); + } else { + this.body = new HttpRequestBody(content, HttpContentType.fromString(contentType)); + Logger.debug("HTTP", "Parsed body with content type: " + contentType); + } } } @@ -220,6 +226,10 @@ public class HttpRequest { return new HashMap<>(headers); } + public HttpMultipartData getMultipartData() { + return multipartData; + } + public HttpRequestBody getBody() { return body; } diff --git a/src/main/java/io/github/lumijiez/core/logging/Logger.java b/src/main/java/io/github/lumijiez/core/logging/Logger.java index fef3807..e91bb1c 100644 --- a/src/main/java/io/github/lumijiez/core/logging/Logger.java +++ b/src/main/java/io/github/lumijiez/core/logging/Logger.java @@ -10,7 +10,7 @@ public class Logger { DEBUG, INFO, WARN, ERROR } - private static final Logger instance = new Logger(LogLevel.INFO); + private static final Logger instance = new Logger(LogLevel.DEBUG); private LogLevel currentLogLevel; diff --git a/src/main/java/io/github/lumijiez/example/Main.java b/src/main/java/io/github/lumijiez/example/Main.java index 0629a47..97a6f6a 100644 --- a/src/main/java/io/github/lumijiez/example/Main.java +++ b/src/main/java/io/github/lumijiez/example/Main.java @@ -1,6 +1,8 @@ package io.github.lumijiez.example; import io.github.lumijiez.core.config.ServerConfig; +import io.github.lumijiez.core.http.HttpFileItem; +import io.github.lumijiez.core.http.HttpMultipartData; import io.github.lumijiez.core.http.HttpServer; import io.github.lumijiez.core.http.HttpStatus; import io.github.lumijiez.core.ws.WebSocketConnection; @@ -10,6 +12,9 @@ import io.github.lumijiez.example.daos.ProductDao; import io.github.lumijiez.example.models.Product; import io.github.lumijiez.core.logging.Logger; +import java.io.File; +import java.util.Map; + public class Main { public static void main(String[] args) { ProductDao productDao = new ProductDao(); @@ -32,6 +37,42 @@ public class Main { res.sendResponse(HttpStatus.OK, "All good, lil bro"); }); + server.POST("/upload", (req, res) -> { + HttpMultipartData multipartData = req.getMultipartData(); + + String description = multipartData.getField("description"); + String category = multipartData.getField("category"); + + HttpFileItem uploadedFile = multipartData.getFile("file"); + if (uploadedFile != null) { + String fileName = uploadedFile.getFileName(); + String contentType = uploadedFile.getContentType(); + byte[] fileContent = uploadedFile.getContent(); + + File uploadDir = new File("uploads"); + if (!uploadDir.exists()) { + uploadDir.mkdirs(); + } + + Logger.info("START UPLOAD", fileName); + File destination = new File(uploadDir, fileName); + uploadedFile.saveTo(destination); + Logger.info("DONE UPLOAD", fileName); + res.sendResponse(HttpStatus.OK, "Uploaded: " + fileName); +// res.sendJson(HttpStatus.OK, Map.of( +// "message", "File uploaded successfully", +// "fileName", fileName, +// "size", fileContent.length, +// "description", description +// )); + Logger.info("START UPLOAD", fileName); + } else { + res.sendJson(HttpStatus.BAD_REQUEST, Map.of( + "error", "No file provided" + )); + } + }); + server.GET("/user", (req, res) -> { Product product = productDao.getProductById(5); res.sendJson(HttpStatus.OK, product);