Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.Component;
Expand Down Expand Up @@ -60,6 +63,10 @@ public final class TransferUtil {
*/
public static int DEFAULT_BUFFER_SIZE = 16384;

private static Logger getLogger() {
return LoggerFactory.getLogger(TransferUtil.class);
}

/**
* Transfers data from the given input stream to the output stream while
* notifying the progress to the given listeners.
Expand Down Expand Up @@ -146,6 +153,8 @@ public static void handleUpload(UploadHandler handler,
VaadinRequest request, VaadinResponse response,
VaadinSession session, Element owner) {
boolean isMultipartUpload = isMultipartContent(request);
List<String> acceptedFiles = new ArrayList<>();
List<UploadResult.RejectedFile> rejectedFiles = new ArrayList<>();
try {
if (isMultipartUpload) {
Collection<Part> parts = Collections.EMPTY_LIST;
Expand All @@ -164,9 +173,19 @@ public static void handleUpload(UploadHandler handler,
session, part.getSubmittedFileName(),
part.getSize(), part.getContentType(), owner,
part);

handleUploadRequest(handler, event);

if (event.isRejected()) {
rejectedFiles.add(new UploadResult.RejectedFile(
event.getFileName(),
event.getRejectionMessage()));
} else {
acceptedFiles.add(event.getFileName());
}
}
handler.responseHandled(new UploadResult(true, response));
handler.responseHandled(new UploadResult(true, response,
null, acceptedFiles, rejectedFiles));
} else {
LoggerFactory.getLogger(UploadHandler.class)
.warn("Multipart request has no parts");
Expand All @@ -181,7 +200,15 @@ public static void handleUpload(UploadHandler handler,
owner, null);

handleUploadRequest(handler, event);
handler.responseHandled(new UploadResult(true, response));

if (event.isRejected()) {
rejectedFiles.add(new UploadResult.RejectedFile(
event.getFileName(), event.getRejectionMessage()));
} else {
acceptedFiles.add(event.getFileName());
}
handler.responseHandled(new UploadResult(true, response, null,
acceptedFiles, rejectedFiles));
}
} catch (UploadSizeLimitExceededException
| UploadFileSizeLimitExceededException
Expand All @@ -190,23 +217,23 @@ public static void handleUpload(UploadHandler handler,
+ "extend StreamRequestHandler, override {} method for "
+ "UploadHandler and provide a higher limit.";
if (e instanceof UploadSizeLimitExceededException) {
LoggerFactory.getLogger(UploadHandler.class).warn(limitInfoStr,
"Request size", "getRequestSizeMax");
getLogger().warn(limitInfoStr, "Request size",
"getRequestSizeMax");
} else if (e instanceof UploadFileSizeLimitExceededException fileSizeException) {
LoggerFactory.getLogger(UploadHandler.class).warn(
limitInfoStr + " File: {}", "File size",
getLogger().warn(limitInfoStr + " File: {}", "File size",
"getFileSizeMax", fileSizeException.getFileName());
} else if (e instanceof UploadFileCountLimitExceededException) {
LoggerFactory.getLogger(UploadHandler.class).warn(limitInfoStr,
"File count", "getFileCountMax");
getLogger().warn(limitInfoStr, "File count", "getFileCountMax");
}
LoggerFactory.getLogger(UploadHandler.class)
.warn("File upload failed.", e);
handler.responseHandled(new UploadResult(false, response, e));
handler.responseHandled(new UploadResult(false, response, e,
acceptedFiles, rejectedFiles));
} catch (Exception e) {
LoggerFactory.getLogger(UploadHandler.class)
.error("Exception during upload", e);
handler.responseHandled(new UploadResult(false, response, e));
handler.responseHandled(new UploadResult(false, response, e,
acceptedFiles, rejectedFiles));
}
}

Expand Down Expand Up @@ -324,6 +351,16 @@ private static void validateUploadLimits(UploadHandler handler,
}
}

/**
* Handles an upload request.
*
* @param handler
* the upload handler
* @param event
* the upload event
* @throws IOException
* if an I/O error occurs
*/
private static void handleUploadRequest(UploadHandler handler,
UploadEvent event) throws IOException {
Component owner = event.getOwningComponent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public class UploadEvent {

private final Part part;

private boolean rejected = false;
private String rejectionMessage;

/**
* Create a new download event with required data.
*
Expand Down Expand Up @@ -90,8 +93,15 @@ public UploadEvent(VaadinRequest request, VaadinResponse response,
*
* @return the input stream from which the contents of the request can be
* read
* @throws IllegalStateException
* if the upload has been rejected
*/
public InputStream getInputStream() {
if (rejected) {
throw new IllegalStateException(
"Cannot access input stream of rejected upload: "
+ rejectionMessage);
}
try {
if (part != null) {
return part.getInputStream();
Expand Down Expand Up @@ -201,4 +211,51 @@ private UI getUiFromSession(Component value) {
session.unlock();
}
}

/**
* Rejects this upload with a default message.
* <p>
* When called, the file will not be processed (or will be cleaned up if
* already processed) and the rejection will be communicated to the client.
* The default rejection message "File rejected" will be used.
*
* @see #reject(String)
*/
public void reject() {
reject("File rejected");
}

/**
* Rejects this upload with a custom message.
* <p>
* When called, the file will not be processed (or will be cleaned up if
* already processed) and the rejection will be communicated to the client
* with the provided message.
*
* @param message
* the rejection message to send to the client
*/
public void reject(String message) {
this.rejected = true;
this.rejectionMessage = message;
}

/**
* Checks whether this upload has been rejected.
*
* @return {@code true} if the upload has been rejected, {@code false}
* otherwise
*/
public boolean isRejected() {
return rejected;
}

/**
* Gets the rejection message if this upload has been rejected.
*
* @return the rejection message, or {@code null} if not rejected
*/
public String getRejectionMessage() {
return rejectionMessage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,15 @@
package com.vaadin.flow.server.streams;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

import org.slf4j.LoggerFactory;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;

import com.vaadin.flow.dom.Element;
import com.vaadin.flow.internal.JacksonUtils;
import com.vaadin.flow.server.HttpStatusCode;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinResponse;
Expand Down Expand Up @@ -111,25 +118,79 @@ public interface UploadHandler extends ElementRequestHandler {
* {@link UploadHandler#handleUploadRequest(UploadEvent)} methods have been
* called for all files.
* <p>
* This method sets the http response return codes according to internal
* exception handling in the framework.
* This method sets the HTTP response return codes and writes JSON responses
* for rejected files:
* <ul>
* <li>200 OK - all files accepted</li>
* <li>422 Unprocessable Entity - all files rejected (with JSON body)</li>
* <li>207 Multi-Status - some files accepted, some rejected (with JSON
* body)</li>
* <li>500 Internal Server Error - exception occurred</li>
* </ul>
* <p>
* If you want custom exception handling and to set the return code,
* implement this method and overwrite the default functionality.
*
* @param result
* the result of the upload operation containing success status,
* response object, and any exception that occurred
* response object, any exception that occurred, and lists of
* accepted/rejected files
*/
default void responseHandled(UploadResult result) {
if (result.success()) {
result.response().setStatus(HttpStatusCode.OK.getCode());
} else {
result.response()
.setStatus(HttpStatusCode.INTERNAL_SERVER_ERROR.getCode());
VaadinResponse response = result.response();
try {
if (result.exception() != null) {
response.setStatus(
HttpStatusCode.INTERNAL_SERVER_ERROR.getCode());
} else if (result.allRejected()) {
response.setStatus(422); // Unprocessable Entity
response.setContentType("application/json");
writeJsonResponse(response,
new RejectedFilesResponse(result.rejectedFiles()));
} else if (result.hasMixed()) {
response.setStatus(207); // Multi-Status
response.setContentType("application/json");
writeJsonResponse(response, new MixedUploadResponse(
result.acceptedFiles(), result.rejectedFiles()));
} else {
response.setStatus(HttpStatusCode.OK.getCode());
}
} catch (IOException e) {
LoggerFactory.getLogger(UploadHandler.class)
.error("Error writing upload response", e);
response.setStatus(HttpStatusCode.INTERNAL_SERVER_ERROR.getCode());
}
}

private static void writeJsonResponse(VaadinResponse response,
Object responseObject) throws IOException {
ObjectMapper mapper = JacksonUtils.getMapper();
try {
String json = mapper.writeValueAsString(responseObject);
PrintWriter writer = response.getWriter();
writer.write(json);
} catch (JacksonException e) {
throw new IOException("Failed to serialize response to JSON", e);
}
}

/**
* JSON response structure for rejected files.
*/
record RejectedFilesResponse(List<UploadResult.RejectedFile> rejected)
implements
java.io.Serializable {
}

/**
* JSON response structure for mixed upload results.
*/
record MixedUploadResponse(List<String> accepted,
List<UploadResult.RejectedFile> rejected)
implements
java.io.Serializable {
}

default void handleRequest(VaadinRequest request, VaadinResponse response,
VaadinSession session, Element owner) throws IOException {
TransferUtil.handleUpload(this, request, response, session, owner);
Expand Down
Loading
Loading