MULTIPART UPLOAD WORKS
This commit is contained in:
35
src/main/java/io/github/lumijiez/core/http/HttpFileItem.java
Normal file
35
src/main/java/io/github/lumijiez/core/http/HttpFileItem.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user