Skip to content

Commit

Permalink
Merge pull request quarkusio#24477 from Sgitario/multipart_large
Browse files Browse the repository at this point in the history
Support large files in multipart for resteasy reactive
  • Loading branch information
geoand authored Mar 24, 2022
2 parents 2cef51f + d7ae188 commit 03741c1
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/jdk-early-access-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ env:
# Workaround testsuite locale issue
LANG: en_US.UTF-8
MAVEN_OPTS: -Xmx2g -XX:MaxMetaspaceSize=1g
JVM_TEST_MAVEN_OPTS: "-e -B --settings .github/mvn-settings.xml -Dtest-containers -Dstart-containers -Dformat.skip"
JVM_TEST_MAVEN_OPTS: "-e -B --settings .github/mvn-settings.xml -Dtest-containers -Dstart-containers -Dtest-resteasy-reactive-large-files -Dformat.skip"
DB_USER: hibernate_orm_test
DB_PASSWORD: hibernate_orm_test
DB_NAME: hibernate_orm_test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.quarkus.resteasy.reactive.server.test.multipart;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.List;

import javax.ws.rs.GET;
Expand All @@ -19,6 +21,7 @@ public class MultipartOutputResource {
public static final List<String> RESPONSE_VALUES = List.of("one", "two");
public static final boolean RESPONSE_ACTIVE = true;

private static final long ONE_GIGA = 1024l * 1024l * 1024l * 1l;
private final File TXT_FILE = new File("./src/test/resources/lorem.txt");

@GET
Expand Down Expand Up @@ -57,13 +60,28 @@ public String usingString() {
@GET
@Path("/with-file")
@Produces(MediaType.MULTIPART_FORM_DATA)
public MultipartOutputFileResponse complex() {
public MultipartOutputFileResponse file() {
MultipartOutputFileResponse response = new MultipartOutputFileResponse();
response.name = RESPONSE_NAME;
response.file = TXT_FILE;
return response;
}

@GET
@Path("/with-large-file")
@Produces(MediaType.MULTIPART_FORM_DATA)
public MultipartOutputFileResponse largeFile() throws IOException {
File largeFile = File.createTempFile("rr-large-file", ".tmp");
largeFile.deleteOnExit();
RandomAccessFile f = new RandomAccessFile(largeFile, "rw");
f.setLength(ONE_GIGA);

MultipartOutputFileResponse response = new MultipartOutputFileResponse();
response.name = RESPONSE_NAME;
response.file = largeFile;
return response;
}

@GET
@Path("/with-null-fields")
@Produces(MediaType.MULTIPART_FORM_DATA)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.resteasy.reactive.server.test.multipart.other.OtherPackageFormDataBase;
Expand Down Expand Up @@ -84,6 +85,16 @@ public void testWithFiles() {
assertContainsFile(response, "file", MediaType.APPLICATION_OCTET_STREAM, "lorem.txt");
}

@EnabledIfSystemProperty(named = "test-resteasy-reactive-large-files", matches = "true")
@Test
public void testWithLargeFiles() {
RestAssured.given()
.get("/multipart/output/with-large-file")
.then()
.contentType(ContentType.MULTIPART)
.statusCode(200);
}

@Test
public void testWithNullFields() {
RestAssured
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
Expand All @@ -29,6 +30,7 @@
import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.RestForm;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
Expand All @@ -38,6 +40,8 @@
public class MultipartResponseTest {

public static final String WOO_HOO_WOO_HOO_HOO = "Woo hoo, woo hoo hoo";
private static final long ONE_GIGA = 1024l * 1024l * 1024l * 1l;

@TestHTTPResource
URI baseUri;

Expand Down Expand Up @@ -98,6 +102,15 @@ void shouldParseMultipartResponseWithSmallFile() {
assertThat(data.numberz).isNull();
}

@EnabledIfSystemProperty(named = "test-resteasy-reactive-large-files", matches = "true")
@Test
void shouldParseMultipartResponseWithLargeFile() {
Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class);
MultipartData data = client.getLargeFile();
assertThat(data.file).exists();
assertThat(data.file.length()).isEqualTo(ONE_GIGA);
}

@Test
void shouldParseMultipartResponseWithNulls() {
Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class);
Expand Down Expand Up @@ -164,6 +177,11 @@ public interface Client {
@Path("/small")
MultipartData getSmallFile();

@GET
@Produces(MediaType.MULTIPART_FORM_DATA)
@Path("/large")
MultipartData getLargeFile();

@GET
@Produces(MediaType.MULTIPART_FORM_DATA)
@Path("/empty")
Expand Down Expand Up @@ -193,8 +211,7 @@ public static class Resource {
@GET
@Produces(MediaType.MULTIPART_FORM_DATA)
public MultipartData getFile() throws IOException {
File file = File.createTempFile("toDownload", ".txt");
file.deleteOnExit();
File file = createTempFileToDownload();
// let's write Woo hoo, woo hoo hoo 10k times
try (FileOutputStream out = new FileOutputStream(file)) {
for (int i = 0; i < 10000; i++) {
Expand All @@ -209,15 +226,24 @@ public MultipartData getFile() throws IOException {
@Produces(MediaType.MULTIPART_FORM_DATA)
@Path("/small")
public MultipartData getSmallFile() throws IOException {
File file = File.createTempFile("toDownload", ".txt");
file.deleteOnExit();
File file = createTempFileToDownload();
// let's write Woo hoo, woo hoo hoo 1 time
try (FileOutputStream out = new FileOutputStream(file)) {
out.write(WOO_HOO_WOO_HOO_HOO.getBytes(StandardCharsets.UTF_8));
}
return new MultipartData("foo", file, null, 1984, null);
}

@GET
@Produces(MediaType.MULTIPART_FORM_DATA)
@Path("/large")
public MultipartData getLargeFile() throws IOException {
File file = createTempFileToDownload();
RandomAccessFile f = new RandomAccessFile(file, "rw");
f.setLength(ONE_GIGA);
return new MultipartData("foo", file, null, 1984, null);
}

@GET
@Produces(MediaType.MULTIPART_FORM_DATA)
@Path("/empty")
Expand All @@ -231,6 +257,12 @@ public MultipartData getEmptyData() {
public MultipartData throwError() {
throw new RuntimeException("forced error");
}

private static File createTempFileToDownload() throws IOException {
File file = File.createTempFile("toDownload", ".txt");
file.deleteOnExit();
return file;
}
}

public static class MultipartData {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.jboss.resteasy.reactive.client.impl;

import io.vertx.core.buffer.Buffer;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
Expand Down Expand Up @@ -104,10 +103,10 @@ public static Buffer invokeClientWriter(Entity<?> entity, Object entityObject, C

if (writer.isWriteable(entityClass, entityType, entity.getAnnotations(), entity.getMediaType())) {
if (writerInterceptors == null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
VertxBufferOutputStream out = new VertxBufferOutputStream();
writer.writeTo(entityObject, entityClass, entityType, entity.getAnnotations(),
entity.getMediaType(), headerMap, baos);
return Buffer.buffer(baos.toByteArray());
entity.getMediaType(), headerMap, out);
return out.getBuffer();
} else {
return runClientWriterInterceptors(entityObject, entityClass, entityType, entity.getAnnotations(),
entity.getMediaType(), headerMap, writer, writerInterceptors, properties, serialisers,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.jboss.resteasy.reactive.client.impl;

import io.vertx.core.buffer.Buffer;
import java.io.IOException;
import java.io.OutputStream;

public class VertxBufferOutputStream extends OutputStream {
private Buffer buffer;

public VertxBufferOutputStream() {
this.buffer = Buffer.buffer();
}

@Override
public void write(int b) throws IOException {
buffer.appendByte((byte) (b & 0xFF));
}

@Override
public void write(byte[] b) throws IOException {
buffer.appendBytes(b);
}

@Override
public void write(byte[] b, int off, int len) throws IOException {
buffer.appendBytes(b, off, len);
}

public Buffer getBuffer() {
return this.buffer.copy();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.jboss.resteasy.reactive.server.core.multipart;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
Expand Down Expand Up @@ -81,7 +80,7 @@ private void write(List<PartItem> parts, String boundary, OutputStream outputStr
writeLine(outputStream, charset);

// write content
write(outputStream, serialiseEntity(part.getValue(), part.getType(), requestContext));
writeEntity(outputStream, part.getValue(), part.getType(), requestContext);
// extra line
writeLine(outputStream, charset);
}
Expand Down Expand Up @@ -116,7 +115,7 @@ private void writeLine(OutputStream os, Charset defaultCharset) throws IOExcepti
os.write(LINE_SEPARATOR.getBytes(defaultCharset));
}

private byte[] serialiseEntity(Object entity, MediaType mediaType, ResteasyReactiveRequestContext context)
private void writeEntity(OutputStream os, Object entity, MediaType mediaType, ResteasyReactiveRequestContext context)
throws IOException {
ServerSerialisers serializers = context.getDeployment().getSerialisers();
Class<?> entityClass = entity.getClass();
Expand All @@ -125,13 +124,12 @@ private byte[] serialiseEntity(Object entity, MediaType mediaType, ResteasyReact
MessageBodyWriter<Object>[] writers = (MessageBodyWriter<Object>[]) serializers
.findWriters(null, entityClass, mediaType, RuntimeType.SERVER)
.toArray(ServerSerialisers.NO_WRITER);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
boolean wrote = false;
for (MessageBodyWriter<Object> writer : writers) {
if (writer.isWriteable(entityClass, entityType, Serialisers.NO_ANNOTATION, mediaType)) {
// FIXME: spec doesn't really say what headers we should use here
writer.writeTo(entity, entityClass, entityType, Serialisers.NO_ANNOTATION, mediaType,
Serialisers.EMPTY_MULTI_MAP, baos);
Serialisers.EMPTY_MULTI_MAP, os);
wrote = true;
break;
}
Expand All @@ -140,7 +138,6 @@ private byte[] serialiseEntity(Object entity, MediaType mediaType, ResteasyReact
if (!wrote) {
throw new IllegalStateException("Could not find MessageBodyWriter for " + entityClass + " as " + mediaType);
}
return baos.toByteArray();
}

private String generateBoundary() {
Expand Down

0 comments on commit 03741c1

Please sign in to comment.