[
https://issues.apache.org/jira/browse/WW-5388?focusedWorklogId=902253&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-902253
]
ASF GitHub Bot logged work on WW-5388:
--------------------------------------
Author: ASF GitHub Bot
Created on: 29/Jan/24 10:12
Start Date: 29/Jan/24 10:12
Worklog Time Spent: 10m
Work Description: kusalk commented on code in PR #861:
URL: https://github.com/apache/struts/pull/861#discussion_r1469317669
##########
core/src/main/java/org/apache/struts2/action/UploadedFilesAware.java:
##########
@@ -27,14 +27,14 @@
* The {@link org.apache.struts2.interceptor.ActionFileUploadInterceptor} will
use the interface
* to notify action about the multiple uploaded files.
*/
-public interface UploadedFilesAware {
+public interface UploadedFilesAware<T> {
Review Comment:
Not sure I understand the benefit of this generic type given this interface
is coupled with the `ActionFileUploadInterceptor`. In what situation would an
action implement `UploadedFilesAware<NotFile>`?
##########
core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java:
##########
@@ -52,309 +51,159 @@
*
* @since 2.3.18
*/
-public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
+public class JakartaStreamMultiPartRequest extends
AbstractMultiPartRequest<File> {
- static final Logger LOG =
LogManager.getLogger(JakartaStreamMultiPartRequest.class);
+ private static final Logger LOG =
LogManager.getLogger(JakartaStreamMultiPartRequest.class);
/**
- * Map between file fields and file data.
- */
- protected Map<String, List<FileInfo>> fileInfos = new HashMap<>();
-
- /**
- * Map between non-file fields and values.
- */
- protected Map<String, List<String>> parameters = new HashMap<>();
-
- /* (non-Javadoc)
- * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp()
+ * Processes the upload.
+ *
+ * @param request the servlet request
+ * @param saveDir location of the save dir
*/
- public void cleanUp() {
- LOG.debug("Performing File Upload temporary storage cleanup.");
- for (List<FileInfo> fileInfoList : fileInfos.values()) {
- for (FileInfo fileInfo : fileInfoList) {
- File file = fileInfo.getFile();
- LOG.debug("Deleting file '{}'.", file.getName());
- if (!file.delete()) {
- LOG.warn("There was a problem attempting to delete file
'{}'.", file.getName());
- }
+ @Override
+ protected void processUpload(HttpServletRequest request, String saveDir)
throws IOException {
+ String charset = StringUtils.isBlank(request.getCharacterEncoding())
+ ? defaultEncoding
+ : request.getCharacterEncoding();
+
+ Path location = Path.of(saveDir);
+ JakartaServletDiskFileUpload servletFileUpload =
+ prepareServletFileUpload(Charset.forName(charset), location);
+
+ LOG.debug("Using Jakarta Stream API to process request");
+ servletFileUpload.getItemIterator(request).forEachRemaining(item -> {
+ if (item.isFormField()) {
+ LOG.debug(() -> "Processing a form field: " +
sanitizeNewlines(item.getFieldName()));
+ processFileItemAsFormField(item);
+ } else {
+ LOG.debug(() -> "Processing a file: " +
sanitizeNewlines(item.getFieldName()));
+ processFileItemAsFileField(item, location);
}
- }
+ });
}
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getContentType(java.lang.String)
- */
- public String[] getContentType(String fieldName) {
- List<FileInfo> infos = fileInfos.get(fieldName);
- if (infos == null) {
- return null;
- }
+ protected JakartaServletDiskFileUpload createJakartaFileUpload(Charset
charset, Path location) {
+ DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();
- List<String> types = new ArrayList<>(infos.size());
- for (FileInfo fileInfo : infos) {
- types.add(fileInfo.getContentType());
- }
+ LOG.debug("Using file save directory: {}", location);
+ builder.setPath(location);
- return types.toArray(new String[0]);
- }
+ LOG.debug("Sets buffer size: {}", bufferSize);
+ builder.setBufferSize(bufferSize);
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFile(java.lang.String)
- */
- public UploadedFile[] getFile(String fieldName) {
- List<FileInfo> infos = fileInfos.get(fieldName);
- if (infos == null) {
- return null;
- }
+ LOG.debug("Using charset: {}", charset);
+ builder.setCharset(charset);
- return infos.stream().map(fileInfo ->
- StrutsUploadedFile.Builder.create(fileInfo.getFile())
- .withContentType(fileInfo.contentType)
- .withOriginalName(fileInfo.originalName)
- .build()
- ).toArray(UploadedFile[]::new);
+ DiskFileItemFactory factory = builder.get();
+ return new JakartaServletDiskFileUpload(factory);
}
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileNames(java.lang.String)
- */
- public String[] getFileNames(String fieldName) {
- List<FileInfo> infos = fileInfos.get(fieldName);
- if (infos == null) {
- return null;
- }
-
- List<String> names = new ArrayList<>(infos.size());
- for (FileInfo fileInfo : infos) {
- names.add(getCanonicalName(fileInfo.getOriginalName()));
+ private String readStream(InputStream inputStream) throws IOException {
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ for (int length; (length = inputStream.read(buffer)) != -1; ) {
+ result.write(buffer, 0, length);
}
-
- return names.toArray(new String[0]);
+ return result.toString(StandardCharsets.UTF_8);
}
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames()
- */
- public Enumeration<String> getFileParameterNames() {
- return Collections.enumeration(fileInfos.keySet());
- }
-
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFilesystemName(java.lang.String)
+ /**
+ * Processes the FileItem as a normal form field.
+ *
+ * @param fileItemInput a form field item input
*/
- public String[] getFilesystemName(String fieldName) {
- List<FileInfo> infos = fileInfos.get(fieldName);
- if (infos == null) {
- return null;
- }
+ protected void processFileItemAsFormField(FileItemInput fileItemInput)
throws IOException {
+ String fieldName = fileItemInput.getFieldName();
+ String fieldValue = readStream(fileItemInput.getInputStream());
- List<String> names = new ArrayList<>(infos.size());
- for (FileInfo fileInfo : infos) {
- names.add(fileInfo.getFile().getName());
+ if (exceedsMaxStringLength(fieldName, fieldValue)) {
+ return;
}
- return names.toArray(new String[0]);
- }
-
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameter(java.lang.String)
- */
- public String getParameter(String name) {
- List<String> values = parameters.get(name);
- if (values != null && !values.isEmpty()) {
- return values.get(0);
+ List<String> values;
+ if (parameters.containsKey(fieldName)) {
+ values = parameters.get(fieldName);
+ } else {
+ values = new ArrayList<>();
+ parameters.put(fieldName, values);
}
- return null;
+ values.add(fieldValue);
}
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterNames()
+ /**
+ * @return actual size of already uploaded files
*/
- public Enumeration<String> getParameterNames() {
- return Collections.enumeration(parameters.keySet());
+ protected Long actualSizeOfUploadedFiles() {
+ return uploadedFiles.values().stream()
+ .map(files ->
files.stream().map(UploadedFile::length).reduce(0L, Long::sum))
+ .reduce(0L, Long::sum);
}
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterValues(java.lang.String)
- */
- public String[] getParameterValues(String name) {
- List<String> values = parameters.get(name);
- if (values != null && !values.isEmpty()) {
- return values.toArray(new String[0]);
- }
- return null;
- }
-
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#parse(jakarta.servlet.http.HttpServletRequest,
java.lang.String)
- */
- public void parse(HttpServletRequest request, String saveDir) throws
IOException {
- try {
- setLocale(request);
- processUpload(request, saveDir);
- } catch (Exception e) {
- LOG.debug("Error occurred during parsing of multi part request",
e);
- LocalizedMessage errorMessage = buildErrorMessage(e, new
Object[]{});
+ private boolean exceedsMaxFiles(FileItemInput fileItemInput) {
+ if (maxFiles != null && maxFiles == uploadedFiles.size()) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Cannot accept another file: {} as it will exceed
max files: {}",
+ sanitizeNewlines(fileItemInput.getName()), maxFiles);
+ }
+ LocalizedMessage errorMessage = buildErrorMessage(new
FileUploadFileCountLimitException(
+ String.format("File %s exceeds allowed maximum
number of files %s",
+ fileItemInput.getName(), maxFiles),
+ maxFiles, uploadedFiles.size()),
+ new Object[]{maxFiles, uploadedFiles.size()});
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
+ return true;
}
+ return false;
}
- /**
- * Processes the upload.
- *
- * @param request the servlet request
- * @param saveDir location of the save dir
- */
- protected void processUpload(HttpServletRequest request, String saveDir)
throws Exception {
-
- // Sanity check that the request is a multi-part/form-data request.
- if (JakartaServletFileUpload.isMultipartContent(request)) {
-
- // Sanity check on request size.
- boolean requestSizePermitted = isRequestSizePermitted(request);
-
- // Interface with Commons FileUpload API
- // Using the Streaming API
- JakartaServletFileUpload<DiskFileItem, DiskFileItemFactory>
servletFileUpload = new JakartaServletFileUpload<>();
- if (maxSize != null) {
- servletFileUpload.setSizeMax(maxSize);
- }
- if (maxFiles != null) {
- servletFileUpload.setFileCountMax(maxFiles);
- }
- if (maxFileSize != null) {
- servletFileUpload.setFileSizeMax(maxFileSize);
- }
- FileItemInputIterator i =
servletFileUpload.getItemIterator(request);
-
- // Iterate the file items
- while (i.hasNext()) {
- try {
- FileItemInput itemStream = i.next();
-
- // If the file item stream is a form field, delegate to the
- // field item stream handler
- if (itemStream.isFormField()) {
- processFileItemStreamAsFormField(itemStream);
- }
-
- // Delegate the file item stream for a file field to the
- // file item stream handler, but delegation is skipped
- // if the requestSizePermitted check failed based on the
- // complete content-size of the request.
- else {
-
- // prevent processing file field item if request size
not allowed.
- if (!requestSizePermitted) {
- addFileSkippedError(itemStream.getName(), request);
- LOG.debug("Skipped stream '{}', request maximum
size ({}) exceeded.", itemStream.getName(), maxSize);
- continue;
- }
-
- processFileItemStreamAsFileField(itemStream, saveDir);
- }
- } catch (IOException e) {
- LOG.warn("Error occurred during process upload", e);
- }
- }
+ private void exceedsMaxSizeOfFiles(FileItemInput fileItemInput, File file,
Long currentFilesSize) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("File: {} of size: {} exceeds allowed max size: {},
actual size of already uploaded files: {}",
+ sanitizeNewlines(fileItemInput.getName()), file.length(),
maxSizeOfFiles, currentFilesSize
+ );
}
- }
-
- /**
- * Defines whether the request allowed based on content length.
- *
- * @param request the servlet request
- * @return true if request size is permitted
- */
- protected boolean isRequestSizePermitted(HttpServletRequest request) {
- // if maxSize is specified as -1, there is no sanity check and it's
- // safe to return true for any request, delegating the failure
- // checks later in the upload process.
- if (maxSize == null || maxSize == -1 || request == null) {
- return true;
+ LocalizedMessage errorMessage = buildErrorMessage(new
FileUploadSizeException(
+ String.format("Size %s of file %s exceeds allowed max
size %s",
+ file.length(), fileItemInput.getName(),
maxSizeOfFiles),
+ maxSizeOfFiles, currentFilesSize),
+ new Object[]{maxSizeOfFiles, currentFilesSize});
+ if (!errors.contains(errorMessage)) {
+ errors.add(errorMessage);
}
- return request.getContentLength() < maxSize;
- }
-
- /**
- * @param request the servlet request
- * @return the request content length.
- */
- protected long getRequestSize(HttpServletRequest request) {
- return request != null ? request.getContentLength() : 0;
- }
-
- /**
- * Add a file skipped message notification for action messages.
- *
- * @param fileName file name
- * @param request the servlet request
- */
- protected void addFileSkippedError(String fileName, HttpServletRequest
request) {
- String exceptionMessage = "Skipped file " + fileName + "; request size
limit exceeded.";
- long allowedMaxSize = maxSize != null ? maxSize : -1;
- FileUploadSizeException exception = new
FileUploadSizeException(exceptionMessage, getRequestSize(request),
allowedMaxSize);
- LocalizedMessage message = buildErrorMessage(exception, new
Object[]{fileName, getRequestSize(request), allowedMaxSize});
- if (!errors.contains(message)) {
- errors.add(message);
+ if (!file.delete() && LOG.isWarnEnabled()) {
+ LOG.warn("Cannot delete file: {} which exceeds maximum size: {} of
all files!",
+ sanitizeNewlines(fileItemInput.getName()), maxSizeOfFiles);
}
}
/**
- * Processes the FileItemStream as a Form Field.
+ * Processes the FileItem as a file field.
*
- * @param itemStream file item stream
+ * @param fileItemInput file item representing upload file
+ * @param location location
*/
- protected void processFileItemStreamAsFormField(FileItemInput itemStream) {
- String fieldName = itemStream.getFieldName();
- try {
- List<String> values;
-
- String fieldValue = itemStream.getInputStream().toString();
- if (!parameters.containsKey(fieldName)) {
- values = new ArrayList<>();
- parameters.put(fieldName, values);
- } else {
- values = parameters.get(fieldName);
- }
- values.add(fieldValue);
- } catch (IOException e) {
- LOG.warn("Failed to handle form field '{}'.", fieldName, e);
+ protected void processFileItemAsFileField(FileItemInput fileItemInput,
Path location) throws IOException {
+ // Skip file uploads that don't have a file name - meaning that no
file was selected.
+ if (fileItemInput.getName() == null ||
fileItemInput.getName().trim().isEmpty()) {
+ LOG.debug(() -> "No file has been uploaded for the field: " +
sanitizeNewlines(fileItemInput.getFieldName()));
+ return;
}
- }
- /**
- * Processes the FileItemStream as a file field.
- *
- * @param itemStream file item stream
- * @param location location
- */
- protected void processFileItemStreamAsFileField(FileItemInput itemStream,
String location) {
- // Skip file uploads that don't have a file name - meaning that no
file was selected.
- if (itemStream.getName() == null ||
itemStream.getName().trim().isEmpty()) {
- LOG.debug("No file has been uploaded for the field: {}",
itemStream.getFieldName());
+ if (exceedsMaxFiles(fileItemInput)) {
return;
}
- File file = null;
- try {
- // Create the temporary upload file.
- file = createTemporaryFile(itemStream.getName(), location);
+ File file = createTemporaryFile(fileItemInput.getName(), location);
+ streamFileToDisk(fileItemInput, file);
- if (streamFileToDisk(itemStream, file)) {
- createFileInfoFromItemStream(itemStream, file);
- }
- } catch (IOException e) {
- if (file != null) {
- try {
- file.delete();
- } catch (SecurityException se) {
- LOG.warn("Failed to delete '{}' due to security exception
above.", file.getName(), se);
- }
- }
+ Long currentFilesSize = actualSizeOfUploadedFiles();
Review Comment:
This seems expensive to compute from scratch every time a file field is
encountered, especially when the application might not have even configured a
`maxSizeOfFiles` limit!
##########
core/src/main/java/org/apache/struts2/StrutsConstants.java:
##########
@@ -142,18 +142,25 @@ public final class StrutsConstants {
/** The maximum size of a multipart request (file upload) */
public static final String STRUTS_MULTIPART_MAXSIZE =
"struts.multipart.maxSize";
+ /** The maximum size of all uploaded files.
+ Used only with {@link
org.apache.struts2.dispatcher.multipart.JakartaStreamMultiPartRequest} */
+ public static final String STRUTS_MULTIPART_MAXSIZE_OF_FILES =
"struts.multipart.maxSizeOfFiles";
Review Comment:
Still not very consistent - shouldn't it be `MAX_SIZE_OF_FILES`?
##########
core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java:
##########
@@ -21,375 +21,116 @@
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload2.core.DiskFileItem;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
-import org.apache.commons.fileupload2.core.FileItem;
-import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException;
-import org.apache.commons.fileupload2.core.FileUploadContentTypeException;
-import org.apache.commons.fileupload2.core.FileUploadException;
-import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException;
-import org.apache.commons.fileupload2.core.FileUploadSizeException;
-import org.apache.commons.fileupload2.core.RequestContext;
-import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload;
+import
org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.apache.struts2.dispatcher.LocalizedMessage;
import java.io.File;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
import java.nio.charset.Charset;
+import java.nio.file.Path;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
/**
- * Multipart form data request adapter for Jakarta Commons Fileupload package.
+ * Multipart form data request adapter for Jakarta Commons FileUpload package.
*/
-public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
+public class JakartaMultiPartRequest extends AbstractMultiPartRequest<File> {
- static final Logger LOG =
LogManager.getLogger(JakartaMultiPartRequest.class);
+ private static final Logger LOG =
LogManager.getLogger(JakartaMultiPartRequest.class);
- // maps parameter name -> List of FileItem objects
- protected Map<String, List<FileItem>> files = new HashMap<>();
+ @Override
+ protected void processUpload(HttpServletRequest request, String saveDir)
throws IOException {
+ String charset = StringUtils.isBlank(request.getCharacterEncoding())
+ ? defaultEncoding
+ : request.getCharacterEncoding();
- // maps parameter name -> List of param values
- protected Map<String, List<String>> params = new HashMap<>();
+ JakartaServletDiskFileUpload servletFileUpload =
+ prepareServletFileUpload(Charset.forName(charset),
Path.of(saveDir));
- /**
- * Creates a new request wrapper to handle multipart data using methods
adapted from Jason Pell's
- * multipart classes (see class description).
- *
- * @param saveDir the directory to save off the file
- * @param request the request containing the multipart
- * @throws java.io.IOException is thrown if encoding fails.
- */
- public void parse(HttpServletRequest request, String saveDir) throws
IOException {
- try {
- setLocale(request);
- processUpload(request, saveDir);
- } catch (FileUploadException e) {
- LOG.debug("Request exceeded size limit!", e);
- LocalizedMessage errorMessage;
- if (e instanceof FileUploadByteCountLimitException) {
- FileUploadByteCountLimitException ex =
(FileUploadByteCountLimitException) e;
- errorMessage = buildErrorMessage(e, new Object[]{
- ex.getFieldName(), ex.getFileName(),
ex.getPermitted(), ex.getActualSize()
- });
- } else if (e instanceof FileUploadFileCountLimitException) {
- FileUploadFileCountLimitException ex =
(FileUploadFileCountLimitException) e;
- errorMessage = buildErrorMessage(e, new Object[]{
- ex.getPermitted(), ex.getActualSize()
- });
- } else if (e instanceof FileUploadSizeException) {
- FileUploadSizeException ex = (FileUploadSizeException) e;
- errorMessage = buildErrorMessage(e, new Object[]{
- ex.getPermitted(), ex.getActualSize()
- });
- } else if (e instanceof FileUploadContentTypeException) {
- FileUploadContentTypeException ex =
(FileUploadContentTypeException) e;
- errorMessage = buildErrorMessage(e, new Object[]{
- ex.getContentType()
- });
+ for (DiskFileItem item : servletFileUpload.parseRequest(request)) {
Review Comment:
Yeah this is by design (see [this
comment](https://github.com/apache/commons-fileupload/pull/203#issuecomment-1438293083)),
none of the other options mitigate against this as the risk is processing a
very large amount of request parts, not necessarily large ones. Though we can
probably raise our default limit from 256 to 1000 (used in latest Tomcat).
##########
core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java:
##########
@@ -52,309 +51,159 @@
*
* @since 2.3.18
*/
-public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
+public class JakartaStreamMultiPartRequest extends
AbstractMultiPartRequest<File> {
- static final Logger LOG =
LogManager.getLogger(JakartaStreamMultiPartRequest.class);
+ private static final Logger LOG =
LogManager.getLogger(JakartaStreamMultiPartRequest.class);
/**
- * Map between file fields and file data.
- */
- protected Map<String, List<FileInfo>> fileInfos = new HashMap<>();
-
- /**
- * Map between non-file fields and values.
- */
- protected Map<String, List<String>> parameters = new HashMap<>();
-
- /* (non-Javadoc)
- * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp()
+ * Processes the upload.
+ *
+ * @param request the servlet request
+ * @param saveDir location of the save dir
*/
- public void cleanUp() {
- LOG.debug("Performing File Upload temporary storage cleanup.");
- for (List<FileInfo> fileInfoList : fileInfos.values()) {
- for (FileInfo fileInfo : fileInfoList) {
- File file = fileInfo.getFile();
- LOG.debug("Deleting file '{}'.", file.getName());
- if (!file.delete()) {
- LOG.warn("There was a problem attempting to delete file
'{}'.", file.getName());
- }
+ @Override
+ protected void processUpload(HttpServletRequest request, String saveDir)
throws IOException {
+ String charset = StringUtils.isBlank(request.getCharacterEncoding())
+ ? defaultEncoding
+ : request.getCharacterEncoding();
+
+ Path location = Path.of(saveDir);
+ JakartaServletDiskFileUpload servletFileUpload =
+ prepareServletFileUpload(Charset.forName(charset), location);
+
+ LOG.debug("Using Jakarta Stream API to process request");
+ servletFileUpload.getItemIterator(request).forEachRemaining(item -> {
+ if (item.isFormField()) {
+ LOG.debug(() -> "Processing a form field: " +
sanitizeNewlines(item.getFieldName()));
+ processFileItemAsFormField(item);
+ } else {
+ LOG.debug(() -> "Processing a file: " +
sanitizeNewlines(item.getFieldName()));
+ processFileItemAsFileField(item, location);
}
- }
+ });
}
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getContentType(java.lang.String)
- */
- public String[] getContentType(String fieldName) {
- List<FileInfo> infos = fileInfos.get(fieldName);
- if (infos == null) {
- return null;
- }
+ protected JakartaServletDiskFileUpload createJakartaFileUpload(Charset
charset, Path location) {
+ DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();
- List<String> types = new ArrayList<>(infos.size());
- for (FileInfo fileInfo : infos) {
- types.add(fileInfo.getContentType());
- }
+ LOG.debug("Using file save directory: {}", location);
+ builder.setPath(location);
- return types.toArray(new String[0]);
- }
+ LOG.debug("Sets buffer size: {}", bufferSize);
+ builder.setBufferSize(bufferSize);
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFile(java.lang.String)
- */
- public UploadedFile[] getFile(String fieldName) {
- List<FileInfo> infos = fileInfos.get(fieldName);
- if (infos == null) {
- return null;
- }
+ LOG.debug("Using charset: {}", charset);
+ builder.setCharset(charset);
- return infos.stream().map(fileInfo ->
- StrutsUploadedFile.Builder.create(fileInfo.getFile())
- .withContentType(fileInfo.contentType)
- .withOriginalName(fileInfo.originalName)
- .build()
- ).toArray(UploadedFile[]::new);
+ DiskFileItemFactory factory = builder.get();
+ return new JakartaServletDiskFileUpload(factory);
}
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileNames(java.lang.String)
- */
- public String[] getFileNames(String fieldName) {
- List<FileInfo> infos = fileInfos.get(fieldName);
- if (infos == null) {
- return null;
- }
-
- List<String> names = new ArrayList<>(infos.size());
- for (FileInfo fileInfo : infos) {
- names.add(getCanonicalName(fileInfo.getOriginalName()));
+ private String readStream(InputStream inputStream) throws IOException {
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ for (int length; (length = inputStream.read(buffer)) != -1; ) {
+ result.write(buffer, 0, length);
}
-
- return names.toArray(new String[0]);
+ return result.toString(StandardCharsets.UTF_8);
}
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames()
- */
- public Enumeration<String> getFileParameterNames() {
- return Collections.enumeration(fileInfos.keySet());
- }
-
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFilesystemName(java.lang.String)
+ /**
+ * Processes the FileItem as a normal form field.
+ *
+ * @param fileItemInput a form field item input
*/
- public String[] getFilesystemName(String fieldName) {
- List<FileInfo> infos = fileInfos.get(fieldName);
- if (infos == null) {
- return null;
- }
+ protected void processFileItemAsFormField(FileItemInput fileItemInput)
throws IOException {
+ String fieldName = fileItemInput.getFieldName();
+ String fieldValue = readStream(fileItemInput.getInputStream());
- List<String> names = new ArrayList<>(infos.size());
- for (FileInfo fileInfo : infos) {
- names.add(fileInfo.getFile().getName());
+ if (exceedsMaxStringLength(fieldName, fieldValue)) {
+ return;
}
- return names.toArray(new String[0]);
- }
-
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameter(java.lang.String)
- */
- public String getParameter(String name) {
- List<String> values = parameters.get(name);
- if (values != null && !values.isEmpty()) {
- return values.get(0);
+ List<String> values;
+ if (parameters.containsKey(fieldName)) {
+ values = parameters.get(fieldName);
+ } else {
+ values = new ArrayList<>();
+ parameters.put(fieldName, values);
}
- return null;
+ values.add(fieldValue);
}
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterNames()
+ /**
+ * @return actual size of already uploaded files
*/
- public Enumeration<String> getParameterNames() {
- return Collections.enumeration(parameters.keySet());
+ protected Long actualSizeOfUploadedFiles() {
+ return uploadedFiles.values().stream()
+ .map(files ->
files.stream().map(UploadedFile::length).reduce(0L, Long::sum))
+ .reduce(0L, Long::sum);
}
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterValues(java.lang.String)
- */
- public String[] getParameterValues(String name) {
- List<String> values = parameters.get(name);
- if (values != null && !values.isEmpty()) {
- return values.toArray(new String[0]);
- }
- return null;
- }
-
- /* (non-Javadoc)
- * @see
org.apache.struts2.dispatcher.multipart.MultiPartRequest#parse(jakarta.servlet.http.HttpServletRequest,
java.lang.String)
- */
- public void parse(HttpServletRequest request, String saveDir) throws
IOException {
- try {
- setLocale(request);
- processUpload(request, saveDir);
- } catch (Exception e) {
- LOG.debug("Error occurred during parsing of multi part request",
e);
- LocalizedMessage errorMessage = buildErrorMessage(e, new
Object[]{});
+ private boolean exceedsMaxFiles(FileItemInput fileItemInput) {
+ if (maxFiles != null && maxFiles == uploadedFiles.size()) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Cannot accept another file: {} as it will exceed
max files: {}",
+ sanitizeNewlines(fileItemInput.getName()), maxFiles);
+ }
+ LocalizedMessage errorMessage = buildErrorMessage(new
FileUploadFileCountLimitException(
+ String.format("File %s exceeds allowed maximum
number of files %s",
+ fileItemInput.getName(), maxFiles),
+ maxFiles, uploadedFiles.size()),
+ new Object[]{maxFiles, uploadedFiles.size()});
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
+ return true;
}
+ return false;
}
- /**
- * Processes the upload.
- *
- * @param request the servlet request
- * @param saveDir location of the save dir
- */
- protected void processUpload(HttpServletRequest request, String saveDir)
throws Exception {
-
- // Sanity check that the request is a multi-part/form-data request.
- if (JakartaServletFileUpload.isMultipartContent(request)) {
-
- // Sanity check on request size.
- boolean requestSizePermitted = isRequestSizePermitted(request);
-
- // Interface with Commons FileUpload API
- // Using the Streaming API
- JakartaServletFileUpload<DiskFileItem, DiskFileItemFactory>
servletFileUpload = new JakartaServletFileUpload<>();
- if (maxSize != null) {
- servletFileUpload.setSizeMax(maxSize);
- }
- if (maxFiles != null) {
- servletFileUpload.setFileCountMax(maxFiles);
- }
- if (maxFileSize != null) {
- servletFileUpload.setFileSizeMax(maxFileSize);
- }
- FileItemInputIterator i =
servletFileUpload.getItemIterator(request);
-
- // Iterate the file items
- while (i.hasNext()) {
- try {
- FileItemInput itemStream = i.next();
-
- // If the file item stream is a form field, delegate to the
- // field item stream handler
- if (itemStream.isFormField()) {
- processFileItemStreamAsFormField(itemStream);
- }
-
- // Delegate the file item stream for a file field to the
- // file item stream handler, but delegation is skipped
- // if the requestSizePermitted check failed based on the
- // complete content-size of the request.
- else {
-
- // prevent processing file field item if request size
not allowed.
- if (!requestSizePermitted) {
- addFileSkippedError(itemStream.getName(), request);
- LOG.debug("Skipped stream '{}', request maximum
size ({}) exceeded.", itemStream.getName(), maxSize);
- continue;
- }
-
- processFileItemStreamAsFileField(itemStream, saveDir);
- }
- } catch (IOException e) {
- LOG.warn("Error occurred during process upload", e);
- }
- }
+ private void exceedsMaxSizeOfFiles(FileItemInput fileItemInput, File file,
Long currentFilesSize) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("File: {} of size: {} exceeds allowed max size: {},
actual size of already uploaded files: {}",
+ sanitizeNewlines(fileItemInput.getName()), file.length(),
maxSizeOfFiles, currentFilesSize
+ );
}
- }
-
- /**
- * Defines whether the request allowed based on content length.
- *
- * @param request the servlet request
- * @return true if request size is permitted
- */
- protected boolean isRequestSizePermitted(HttpServletRequest request) {
- // if maxSize is specified as -1, there is no sanity check and it's
- // safe to return true for any request, delegating the failure
- // checks later in the upload process.
- if (maxSize == null || maxSize == -1 || request == null) {
- return true;
+ LocalizedMessage errorMessage = buildErrorMessage(new
FileUploadSizeException(
+ String.format("Size %s of file %s exceeds allowed max
size %s",
+ file.length(), fileItemInput.getName(),
maxSizeOfFiles),
+ maxSizeOfFiles, currentFilesSize),
+ new Object[]{maxSizeOfFiles, currentFilesSize});
+ if (!errors.contains(errorMessage)) {
+ errors.add(errorMessage);
}
- return request.getContentLength() < maxSize;
- }
-
- /**
- * @param request the servlet request
- * @return the request content length.
- */
- protected long getRequestSize(HttpServletRequest request) {
- return request != null ? request.getContentLength() : 0;
- }
-
- /**
- * Add a file skipped message notification for action messages.
- *
- * @param fileName file name
- * @param request the servlet request
- */
- protected void addFileSkippedError(String fileName, HttpServletRequest
request) {
- String exceptionMessage = "Skipped file " + fileName + "; request size
limit exceeded.";
- long allowedMaxSize = maxSize != null ? maxSize : -1;
- FileUploadSizeException exception = new
FileUploadSizeException(exceptionMessage, getRequestSize(request),
allowedMaxSize);
- LocalizedMessage message = buildErrorMessage(exception, new
Object[]{fileName, getRequestSize(request), allowedMaxSize});
- if (!errors.contains(message)) {
- errors.add(message);
+ if (!file.delete() && LOG.isWarnEnabled()) {
+ LOG.warn("Cannot delete file: {} which exceeds maximum size: {} of
all files!",
+ sanitizeNewlines(fileItemInput.getName()), maxSizeOfFiles);
}
}
/**
- * Processes the FileItemStream as a Form Field.
+ * Processes the FileItem as a file field.
*
- * @param itemStream file item stream
+ * @param fileItemInput file item representing upload file
+ * @param location location
*/
- protected void processFileItemStreamAsFormField(FileItemInput itemStream) {
- String fieldName = itemStream.getFieldName();
- try {
- List<String> values;
-
- String fieldValue = itemStream.getInputStream().toString();
- if (!parameters.containsKey(fieldName)) {
- values = new ArrayList<>();
- parameters.put(fieldName, values);
- } else {
- values = parameters.get(fieldName);
- }
- values.add(fieldValue);
- } catch (IOException e) {
- LOG.warn("Failed to handle form field '{}'.", fieldName, e);
+ protected void processFileItemAsFileField(FileItemInput fileItemInput,
Path location) throws IOException {
+ // Skip file uploads that don't have a file name - meaning that no
file was selected.
+ if (fileItemInput.getName() == null ||
fileItemInput.getName().trim().isEmpty()) {
+ LOG.debug(() -> "No file has been uploaded for the field: " +
sanitizeNewlines(fileItemInput.getFieldName()));
+ return;
}
- }
- /**
- * Processes the FileItemStream as a file field.
- *
- * @param itemStream file item stream
- * @param location location
- */
- protected void processFileItemStreamAsFileField(FileItemInput itemStream,
String location) {
- // Skip file uploads that don't have a file name - meaning that no
file was selected.
- if (itemStream.getName() == null ||
itemStream.getName().trim().isEmpty()) {
- LOG.debug("No file has been uploaded for the field: {}",
itemStream.getFieldName());
+ if (exceedsMaxFiles(fileItemInput)) {
return;
}
- File file = null;
- try {
- // Create the temporary upload file.
- file = createTemporaryFile(itemStream.getName(), location);
+ File file = createTemporaryFile(fileItemInput.getName(), location);
+ streamFileToDisk(fileItemInput, file);
- if (streamFileToDisk(itemStream, file)) {
- createFileInfoFromItemStream(itemStream, file);
- }
- } catch (IOException e) {
- if (file != null) {
- try {
- file.delete();
- } catch (SecurityException se) {
- LOG.warn("Failed to delete '{}' due to security exception
above.", file.getName(), se);
- }
- }
+ Long currentFilesSize = actualSizeOfUploadedFiles();
Review Comment:
Tbh I'm not convinced this option adds much value in the first place - I
think `maxSize` serves this need well enough. I'm trying to think in what
situation would an application need to configure `maxSizeOfFiles` instead of or
in addition to `maxSize`. They want to accepts lots of big form fields but want
to limit the amount of data written to disk? I guess it's possible
Issue Time Tracking
-------------------
Worklog Id: (was: 902253)
Time Spent: 6h (was: 5h 50m)
> Upgrade Commons Fileupload to FileUpload Jakarta Servlet 6
> ----------------------------------------------------------
>
> Key: WW-5388
> URL: https://issues.apache.org/jira/browse/WW-5388
> Project: Struts 2
> Issue Type: Improvement
> Components: Core
> Reporter: Lukasz Lenart
> Assignee: Lukasz Lenart
> Priority: Major
> Fix For: 7.0.0
>
> Time Spent: 6h
> Remaining Estimate: 0h
>
> There is a new version of JakartaEE FileUpload
> {code:xml}
> <dependency>
> <groupId>org.apache.commons</groupId>
> <artifactId>commons-fileupload2-jakarta-servlet6</artifactId>
> <version>2.0.0-M2</version>
> </dependency>
> {code}
--
This message was sent by Atlassian Jira
(v8.20.10#820010)