attachment simplified. Requires acra 5.2.0-rc2 if attachments are used.
This commit is contained in:
parent
7b3efe5d92
commit
35c14042b2
4 changed files with 13 additions and 498 deletions
|
@ -15,14 +15,11 @@
|
|||
*/
|
||||
package com.faendir.acra.rest;
|
||||
|
||||
import com.faendir.acra.rest.multipart.Rfc1341MultipartResolver;
|
||||
import com.github.ziplet.filter.compression.CompressingFilter;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.multipart.MultipartResolver;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
|
||||
|
@ -32,11 +29,6 @@ import javax.servlet.Filter;
|
|||
*/
|
||||
@Configuration
|
||||
public class RestConfiguration {
|
||||
@NonNull
|
||||
@Bean
|
||||
public static MultipartResolver multiPartResolver() {
|
||||
return new Rfc1341MultipartResolver();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.springframework.http.MediaType;
|
|||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
@ -36,7 +37,6 @@ import org.springframework.web.multipart.MultipartHttpServletRequest;
|
|||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
@ -56,6 +56,8 @@ public class RestReportInterface {
|
|||
public static final String PARAM_MAIL = "mail";
|
||||
public static final String REPORT_PATH = "report";
|
||||
public static final String MULTIPART_MIXED = "multipart/mixed";
|
||||
private static final String REPORT = "ACRA_REPORT";
|
||||
private static final String ATTACHMENT = "ACRA_ATTACHMENT";
|
||||
@NonNull private final DataService dataService;
|
||||
|
||||
@Autowired
|
||||
|
@ -72,24 +74,19 @@ public class RestReportInterface {
|
|||
}
|
||||
|
||||
@PreAuthorize("hasRole(T(com.faendir.acra.model.User$Role).REPORTER)")
|
||||
@RequestMapping(value = REPORT_PATH, consumes = MULTIPART_MIXED, method = RequestMethod.POST)
|
||||
@RequestMapping(value = REPORT_PATH, consumes = MediaType.MULTIPART_FORM_DATA_VALUE, method = RequestMethod.POST)
|
||||
public ResponseEntity report(@NonNull MultipartHttpServletRequest request, @NonNull Principal principal) throws IOException {
|
||||
String content = null;
|
||||
List<MultipartFile> attachments = new ArrayList<>();
|
||||
for (MultipartFile file : request.getMultiFileMap().get(null)) {
|
||||
String filename = file.getName();
|
||||
if (filename.isEmpty()) {
|
||||
content = StreamUtils.copyToString(file.getInputStream(), StandardCharsets.UTF_8);
|
||||
} else {
|
||||
attachments.add(file);
|
||||
}
|
||||
}
|
||||
if (content != null) {
|
||||
dataService.createNewReport(principal.getName(), content, attachments);
|
||||
return ResponseEntity.ok().build();
|
||||
} else {
|
||||
MultiValueMap<String, MultipartFile> fileMap = request.getMultiFileMap();
|
||||
if (!fileMap.containsKey(REPORT) || fileMap.get(REPORT).isEmpty()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
String content = StreamUtils.copyToString(fileMap.get(REPORT).get(0).getInputStream(), StandardCharsets.UTF_8);
|
||||
List<MultipartFile> attachments = fileMap.get(ATTACHMENT);
|
||||
if(attachments == null) {
|
||||
attachments = Collections.emptyList();
|
||||
}
|
||||
dataService.createNewReport(principal.getName(), content, attachments);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole(T(com.faendir.acra.model.User$Role).USER)")
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* (C) Copyright 2018 Lukas Morawietz (https://github.com/F43nd1r)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.faendir.acra.rest.multipart;
|
||||
|
||||
import org.apache.commons.fileupload.FileItemFactory;
|
||||
import org.apache.commons.fileupload.FileUpload;
|
||||
import org.apache.commons.fileupload.Rfc1341ServletFileUpload;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
|
||||
|
||||
/**
|
||||
* @author Lukas
|
||||
* @since 18.05.2017
|
||||
*/
|
||||
public class Rfc1341MultipartResolver extends CommonsMultipartResolver {
|
||||
@NonNull
|
||||
@Override
|
||||
protected FileUpload newFileUpload(@NonNull FileItemFactory fileItemFactory) {
|
||||
return new Rfc1341ServletFileUpload(fileItemFactory);
|
||||
}
|
||||
}
|
|
@ -1,439 +0,0 @@
|
|||
/*
|
||||
* (C) Copyright 2018 Lukas Morawietz (https://github.com/F43nd1r)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.commons.fileupload;
|
||||
|
||||
import org.apache.commons.fileupload.servlet.ServletFileUpload;
|
||||
import org.apache.commons.fileupload.util.Closeable;
|
||||
import org.apache.commons.fileupload.util.LimitedInputStream;
|
||||
import org.apache.commons.fileupload.util.Streams;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
/**
|
||||
* @author Lukas
|
||||
* @since 18.05.2017
|
||||
*/
|
||||
public class Rfc1341ServletFileUpload extends ServletFileUpload {
|
||||
public Rfc1341ServletFileUpload(@NonNull FileItemFactory fileItemFactory) {
|
||||
super(fileItemFactory);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public FileItemIterator getItemIterator(@Nullable RequestContext ctx) throws FileUploadException, IOException {
|
||||
return new Rfc1341FileItemIterator(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modified copy (with appropriate cast) of {@link FileUploadBase#parseRequest(RequestContext)}
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public List<FileItem> parseRequest(@NonNull RequestContext ctx) throws FileUploadException {
|
||||
List<FileItem> items = new ArrayList<>();
|
||||
boolean successful = false;
|
||||
try {
|
||||
FileItemIterator iter = getItemIterator(ctx);
|
||||
FileItemFactory fac = getFileItemFactory();
|
||||
if (fac == null) {
|
||||
throw new NullPointerException("No FileItemFactory has been set.");
|
||||
}
|
||||
while (iter.hasNext()) {
|
||||
final FileItemStream item = iter.next();
|
||||
// Don't use getName() here to prevent an InvalidFileNameException.
|
||||
final String fileName = ((Rfc1341FileItemIterator.Rfc1341FileItemStream) item).name;
|
||||
FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(), item.isFormField(), fileName);
|
||||
items.add(fileItem);
|
||||
try {
|
||||
Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
|
||||
} catch (FileUploadIOException e) {
|
||||
throw (FileUploadException) e.getCause();
|
||||
} catch (IOException e) {
|
||||
throw new IOFileUploadException(format("Processing of %s request failed. %s", MULTIPART_FORM_DATA, e.getMessage()), e);
|
||||
}
|
||||
final FileItemHeaders fih = item.getHeaders();
|
||||
fileItem.setHeaders(fih);
|
||||
}
|
||||
successful = true;
|
||||
return items;
|
||||
} catch (FileUploadIOException e) {
|
||||
throw (FileUploadException) e.getCause();
|
||||
} catch (IOException e) {
|
||||
throw new FileUploadException(e.getMessage(), e);
|
||||
} finally {
|
||||
if (!successful) {
|
||||
for (FileItem fileItem : items) {
|
||||
try {
|
||||
fileItem.delete();
|
||||
} catch (Throwable e) {
|
||||
// ignore it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modified copy of {@link FileItemIteratorImpl}
|
||||
*
|
||||
* @author Lukas
|
||||
* @since 18.05.2017
|
||||
*/
|
||||
private class Rfc1341FileItemIterator implements FileItemIterator {
|
||||
/**
|
||||
* The multi part stream to process.
|
||||
*/
|
||||
@NonNull private final MultipartStream multi;
|
||||
/**
|
||||
* The notifier, which used for triggering the
|
||||
* {@link ProgressListener}.
|
||||
*/
|
||||
@NonNull private final MultipartStream.ProgressNotifier notifier;
|
||||
/**
|
||||
* The boundary, which separates the various parts.
|
||||
*/
|
||||
private final byte[] boundary;
|
||||
/**
|
||||
* The item, which we currently process.
|
||||
*/
|
||||
@Nullable private Rfc1341FileItemStream currentItem;
|
||||
/**
|
||||
* Whether we are currently skipping the preamble.
|
||||
*/
|
||||
private boolean skipPreamble;
|
||||
/**
|
||||
* Whether the current item may still be read.
|
||||
*/
|
||||
private boolean itemValid;
|
||||
/**
|
||||
* Whether we have seen the end of the file.
|
||||
*/
|
||||
private boolean eof;
|
||||
private final long fileSizeMax;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param ctx The request context.
|
||||
* @throws FileUploadException An error occurred while
|
||||
* parsing the request.
|
||||
* @throws IOException An I/O error occurred.
|
||||
*/
|
||||
Rfc1341FileItemIterator(@Nullable RequestContext ctx) throws FileUploadException, IOException {
|
||||
this.fileSizeMax = getFileSizeMax();
|
||||
long sizeMax = getSizeMax();
|
||||
String headerEncoding = getHeaderEncoding();
|
||||
ProgressListener listener = getProgressListener();
|
||||
if (ctx == null) {
|
||||
throw new NullPointerException("ctx parameter");
|
||||
}
|
||||
|
||||
String contentType = ctx.getContentType();
|
||||
if ((null == contentType) || (!contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART))) {
|
||||
throw new InvalidContentTypeException(
|
||||
format("the request doesn't contain a %s or %s stream, content type header is %s", MULTIPART_FORM_DATA, MULTIPART_MIXED, contentType));
|
||||
}
|
||||
|
||||
InputStream input = ctx.getInputStream();
|
||||
|
||||
@SuppressWarnings("deprecation") // still has to be backward compatible
|
||||
final int contentLengthInt = ctx.getContentLength();
|
||||
|
||||
final long requestSize = UploadContext.class.isAssignableFrom(ctx.getClass())
|
||||
// Inline conditional is OK here CHECKSTYLE:OFF
|
||||
? ((UploadContext) ctx).contentLength() : contentLengthInt;
|
||||
// CHECKSTYLE:ON
|
||||
|
||||
if (sizeMax >= 0) {
|
||||
if (requestSize != -1 && requestSize > sizeMax) {
|
||||
throw new SizeLimitExceededException(format("the request was rejected because its size (%s) exceeds the configured maximum (%s)", requestSize, sizeMax),
|
||||
requestSize, sizeMax);
|
||||
}
|
||||
input = new LimitedInputStream(input, sizeMax) {
|
||||
@Override
|
||||
protected void raiseError(long pSizeMax, long pCount) throws IOException {
|
||||
FileUploadException ex = new SizeLimitExceededException(
|
||||
format("the request was rejected because its size (%s) exceeds the configured maximum (%s)", pCount, pSizeMax), pCount, pSizeMax);
|
||||
throw new FileUploadIOException(ex);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
String charEncoding = headerEncoding;
|
||||
if (charEncoding == null) {
|
||||
charEncoding = ctx.getCharacterEncoding();
|
||||
}
|
||||
|
||||
boundary = getBoundary(contentType);
|
||||
if (boundary == null) {
|
||||
throw new FileUploadException("the request was rejected because no multipart boundary was found");
|
||||
}
|
||||
|
||||
notifier = new MultipartStream.ProgressNotifier(listener, requestSize);
|
||||
try {
|
||||
multi = new MultipartStream(input, boundary, notifier);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
throw new InvalidContentTypeException(format("The boundary specified in the %s header is too long", CONTENT_TYPE), iae);
|
||||
}
|
||||
multi.setHeaderEncoding(charEncoding);
|
||||
|
||||
skipPreamble = true;
|
||||
findNextItem();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called for finding the next item, if any.
|
||||
*
|
||||
* @return True, if an next item was found, otherwise false.
|
||||
* @throws IOException An I/O error occurred.
|
||||
*/
|
||||
private boolean findNextItem() throws IOException {
|
||||
if (eof) {
|
||||
return false;
|
||||
}
|
||||
if (currentItem != null) {
|
||||
currentItem.close();
|
||||
currentItem = null;
|
||||
}
|
||||
boolean nextPart;
|
||||
if (skipPreamble) {
|
||||
nextPart = multi.skipPreamble();
|
||||
} else {
|
||||
nextPart = multi.readBoundary();
|
||||
}
|
||||
if (!nextPart) {
|
||||
eof = true;
|
||||
return false;
|
||||
}
|
||||
FileItemHeaders headers = getParsedHeaders(multi.readHeaders());
|
||||
String fileName = getFileName(headers);
|
||||
currentItem = new Rfc1341FileItemStream(fileName, headers.getHeader(CONTENT_TYPE), getContentLength(headers));
|
||||
currentItem.setHeaders(headers);
|
||||
notifier.noteItem();
|
||||
itemValid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private long getContentLength(@NonNull FileItemHeaders pHeaders) {
|
||||
try {
|
||||
return Long.parseLong(pHeaders.getHeader(CONTENT_LENGTH));
|
||||
} catch (Exception e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns, whether another instance of {@link FileItemStream}
|
||||
* is available.
|
||||
*
|
||||
* @return True, if one or more additional file items
|
||||
* are available, otherwise false.
|
||||
* @throws FileUploadException Parsing or processing the
|
||||
* file item failed.
|
||||
* @throws IOException Reading the file item failed.
|
||||
*/
|
||||
public boolean hasNext() throws FileUploadException, IOException {
|
||||
if (eof) {
|
||||
return false;
|
||||
}
|
||||
if (itemValid) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return findNextItem();
|
||||
} catch (FileUploadIOException e) {
|
||||
// unwrap encapsulated SizeException
|
||||
throw (FileUploadException) e.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next available {@link FileItemStream}.
|
||||
*
|
||||
* @return FileItemStream instance, which provides
|
||||
* access to the next file item.
|
||||
* @throws java.util.NoSuchElementException No more items are
|
||||
* available. Use {@link #hasNext()} to prevent this exception.
|
||||
* @throws FileUploadException Parsing or processing the
|
||||
* file item failed.
|
||||
* @throws IOException Reading the file item failed.
|
||||
*/
|
||||
@Nullable
|
||||
public FileItemStream next() throws FileUploadException, IOException {
|
||||
if (eof || (!itemValid && !hasNext())) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
itemValid = false;
|
||||
return currentItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of {@link FileItemStream}.
|
||||
*/
|
||||
class Rfc1341FileItemStream implements FileItemStream {
|
||||
/**
|
||||
* The file items content type.
|
||||
*/
|
||||
private final String contentType;
|
||||
/**
|
||||
* The file items file name.
|
||||
*/
|
||||
private final String name;
|
||||
/**
|
||||
* The file items input stream.
|
||||
*/
|
||||
private final InputStream stream;
|
||||
/**
|
||||
* Whether the file item was already opened.
|
||||
*/
|
||||
private boolean opened;
|
||||
/**
|
||||
* The headers, if any.
|
||||
*/
|
||||
private FileItemHeaders headers;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param pName The items file name, or null.
|
||||
* @param pContentType The items content type, or null.
|
||||
* @param pContentLength The items content length, if known, or -1
|
||||
* @throws IOException Creating the file item failed.
|
||||
*/
|
||||
Rfc1341FileItemStream(String pName, String pContentType, long pContentLength) throws IOException {
|
||||
name = pName;
|
||||
contentType = pContentType;
|
||||
final MultipartStream.ItemInputStream itemStream = multi.newInputStream();
|
||||
InputStream istream = itemStream;
|
||||
if (fileSizeMax != -1) {
|
||||
if (pContentLength != -1 && pContentLength > fileSizeMax) {
|
||||
FileSizeLimitExceededException e = new FileSizeLimitExceededException(
|
||||
format("The file %s exceeds its maximum permitted size of %s bytes.", name, fileSizeMax), pContentLength, fileSizeMax);
|
||||
e.setFileName(pName);
|
||||
throw new FileUploadIOException(e);
|
||||
}
|
||||
istream = new LimitedInputStream(istream, fileSizeMax) {
|
||||
@Override
|
||||
protected void raiseError(long pSizeMax, long pCount) throws IOException {
|
||||
itemStream.close(true);
|
||||
FileSizeLimitExceededException e = new FileSizeLimitExceededException(
|
||||
format("The file %s exceeds its maximum permitted size of %s bytes.", name, pSizeMax), pCount, pSizeMax);
|
||||
e.setFileName(name);
|
||||
throw new FileUploadIOException(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
stream = istream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the items content type, or null.
|
||||
*
|
||||
* @return Content type, if known, or null.
|
||||
*/
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Files are not associated to fields.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
@Nullable
|
||||
public String getFieldName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the items file name.
|
||||
*
|
||||
* @return File name, if known, or null.
|
||||
* @throws InvalidFileNameException The file name contains a NUL character,
|
||||
* which might be an indicator of a security attack. If you intend to
|
||||
* use the file name anyways, catch the exception and use
|
||||
* InvalidFileNameException#getName().
|
||||
*/
|
||||
public String getName() {
|
||||
return Streams.checkFileName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns, whether this is a form field.
|
||||
*
|
||||
* @return True, if the item is a form field,
|
||||
* otherwise false.
|
||||
*/
|
||||
public boolean isFormField() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an input stream, which may be used to
|
||||
* read the items contents.
|
||||
*
|
||||
* @return Opened input stream.
|
||||
* @throws IOException An I/O error occurred.
|
||||
*/
|
||||
public InputStream openStream() throws IOException {
|
||||
if (opened) {
|
||||
throw new IllegalStateException("The stream was already opened.");
|
||||
}
|
||||
if (((Closeable) stream).isClosed()) {
|
||||
throw new ItemSkippedException();
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the file item.
|
||||
*
|
||||
* @throws IOException An I/O error occurred.
|
||||
*/
|
||||
void close() throws IOException {
|
||||
stream.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file item headers.
|
||||
*
|
||||
* @return The items header object
|
||||
*/
|
||||
public FileItemHeaders getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file item headers.
|
||||
*
|
||||
* @param pHeaders The items header object
|
||||
*/
|
||||
public void setHeaders(FileItemHeaders pHeaders) {
|
||||
headers = pHeaders;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue