MULTIPART UPLOAD WORKS

This commit is contained in:
Daniel
2024-11-03 23:30:04 +02:00
parent 0f87325da1
commit bfefcd0718
6 changed files with 235 additions and 49 deletions

View File

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

View File

@@ -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<String, String> fields = new HashMap<>();
private final Map<String, HttpFileItem> 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<String, String> getFields() {
return Collections.unmodifiableMap(fields);
}
public Map<String, HttpFileItem> getFiles() {
return Collections.unmodifiableMap(files);
}
}

View File

@@ -1,62 +1,127 @@
package io.github.lumijiez.core.http; package io.github.lumijiez.core.http;
import io.github.lumijiez.core.logging.Logger;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
public class HttpMultipartParser { public class HttpMultipartParser {
private final String boundary;
private final BufferedReader reader; private final BufferedReader reader;
private final Map<String, String> 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) { public HttpMultipartParser(BufferedReader reader, String contentType) {
this.reader = reader; 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) { private String extractBoundary(String contentType) {
String[] parts = contentType.split(";"); String[] parts = contentType.split(";");
for (String part : parts) { for (String part : parts) {
if (part.trim().startsWith("boundary=")) { part = part.trim();
return "--" + part.split("=")[1].trim(); if (part.startsWith("boundary=")) {
String trim = part.substring(("boundary=".length())).trim();
Logger.debug("HTTP", "Boundary: " + trim);
return trim;
} }
} }
return null; return null;
} }
public Map<String, String> parse() throws IOException { public HttpMultipartData parse() throws IOException {
String line; String line;
StringBuilder content = new StringBuilder(); StringBuilder currentPart = new StringBuilder();
String currentName = null; Map<String, String> currentHeaders = new HashMap<>();
boolean isReadingHeaders = false;
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
Logger.debug("HTTP", "Reading line: '" + line + "'");
line = line.trim();
if (line.startsWith(boundary + "--")) {
if (!currentHeaders.isEmpty() && !currentPart.isEmpty()) {
processContent(currentHeaders, currentPart.toString());
}
Logger.debug("HTTP", "Found end boundary, finishing parse");
break;
}
if (line.startsWith(boundary)) { if (line.startsWith(boundary)) {
if (currentName != null) { if (!currentHeaders.isEmpty() && !currentPart.isEmpty()) {
parts.put(currentName, content.toString().trim()); processContent(currentHeaders, currentPart.toString());
content = new StringBuilder(); }
currentPart.setLength(0);
currentHeaders.clear();
isReadingHeaders = true;
continue;
} }
while ((line = reader.readLine()) != null && !line.isEmpty()) { if (isReadingHeaders) {
if (line.toLowerCase().startsWith("content-disposition:")) { if (line.isEmpty()) {
currentName = extractFieldName(line); 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 if (!line.equals(boundary + "--")) { } else {
content.append(line).append("\n"); currentPart.append(line).append("\r\n");
} }
} }
return parts; return multipartData;
} }
private String extractFieldName(String header) { private void processContent(Map<String, String> headers, String content) {
String[] parts = header.split(";"); String contentDisposition = headers.get("content-disposition");
if (contentDisposition == null) {
return;
}
Map<String, String> 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<String, String> parseContentDisposition(String contentDisposition) {
Map<String, String> params = new HashMap<>();
String[] parts = contentDisposition.split(";");
for (String part : parts) { for (String part : parts) {
if (part.trim().startsWith("name=")) { part = part.trim();
return part.split("=")[1].trim().replace("\"", ""); 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;
} }
} }

View File

@@ -21,6 +21,7 @@ public class HttpRequest {
private UrlParser urlParser; private UrlParser urlParser;
private HttpRequestBody body; private HttpRequestBody body;
private Map<String, String> formData; private Map<String, String> formData;
private HttpMultipartData multipartData;
private final Map<String, String> cookies; private final Map<String, String> cookies;
public HttpRequest(BufferedReader in) throws IOException { public HttpRequest(BufferedReader in) throws IOException {
@@ -115,6 +116,14 @@ public class HttpRequest {
int contentLength = Integer.parseInt(headers.get("content-length")); int contentLength = Integer.parseInt(headers.get("content-length"));
String contentType = headers.getOrDefault("content-type", "text/plain"); String contentType = headers.getOrDefault("content-type", "text/plain");
if (contentType.startsWith("multipart/form-data")) {
Logger.debug("CONTENT TYPE", contentType);
HttpMultipartParser parser = new HttpMultipartParser(in, contentType);
this.multipartData = parser.parse();
Logger.debug("HTTP", "Parsed multipart data with " +
multipartData.getFiles().size() + " files and " +
multipartData.getFields().size() + " fields");
} else {
if ("chunked".equalsIgnoreCase(headers.get("transfer-encoding"))) { if ("chunked".equalsIgnoreCase(headers.get("transfer-encoding"))) {
parseChunkedBody(in); parseChunkedBody(in);
return; return;
@@ -131,11 +140,7 @@ public class HttpRequest {
} }
String content = new String(buffer); String content = new String(buffer);
if (contentType.startsWith("multipart/form-data")) { if (contentType.startsWith("application/x-www-form-urlencoded")) {
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); this.formData = parseUrlEncodedForm(content);
Logger.debug("HTTP", "Parsed URL encoded form data: " + formData.size() + " fields"); Logger.debug("HTTP", "Parsed URL encoded form data: " + formData.size() + " fields");
} else { } else {
@@ -143,6 +148,7 @@ public class HttpRequest {
Logger.debug("HTTP", "Parsed body with content type: " + contentType); Logger.debug("HTTP", "Parsed body with content type: " + contentType);
} }
} }
}
private void parseChunkedBody(BufferedReader in) throws IOException { private void parseChunkedBody(BufferedReader in) throws IOException {
StringBuilder content = new StringBuilder(); StringBuilder content = new StringBuilder();
@@ -220,6 +226,10 @@ public class HttpRequest {
return new HashMap<>(headers); return new HashMap<>(headers);
} }
public HttpMultipartData getMultipartData() {
return multipartData;
}
public HttpRequestBody getBody() { public HttpRequestBody getBody() {
return body; return body;
} }

View File

@@ -10,7 +10,7 @@ public class Logger {
DEBUG, INFO, WARN, ERROR 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; private LogLevel currentLogLevel;

View File

@@ -1,6 +1,8 @@
package io.github.lumijiez.example; package io.github.lumijiez.example;
import io.github.lumijiez.core.config.ServerConfig; 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.HttpServer;
import io.github.lumijiez.core.http.HttpStatus; import io.github.lumijiez.core.http.HttpStatus;
import io.github.lumijiez.core.ws.WebSocketConnection; 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.example.models.Product;
import io.github.lumijiez.core.logging.Logger; import io.github.lumijiez.core.logging.Logger;
import java.io.File;
import java.util.Map;
public class Main { public class Main {
public static void main(String[] args) { public static void main(String[] args) {
ProductDao productDao = new ProductDao(); ProductDao productDao = new ProductDao();
@@ -32,6 +37,42 @@ public class Main {
res.sendResponse(HttpStatus.OK, "All good, lil bro"); 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) -> { server.GET("/user", (req, res) -> {
Product product = productDao.getProductById(5); Product product = productDao.getProductById(5);
res.sendJson(HttpStatus.OK, product); res.sendJson(HttpStatus.OK, product);