Skip to content

Commit 049ee8d

Browse files
committed
857 Support multipart files using InputStream from source file
1 parent 3b3a7da commit 049ee8d

File tree

7 files changed

+275
-3
lines changed

7 files changed

+275
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Use the `addBodyPart` method to add a multipart part to the request.
110110
This part can be of type:
111111
* `ByteArrayPart`
112112
* `FilePart`
113+
* `InputStreamPart`
113114
* `StringPart`
114115

115116
### Dealing with Responses
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.asynchttpclient.request.body.multipart;
2+
3+
import java.io.InputStream;
4+
import java.nio.charset.Charset;
5+
6+
import static org.asynchttpclient.util.Assertions.assertNotNull;
7+
8+
public class InputStreamPart extends FileLikePart {
9+
10+
private final InputStream inputStream;
11+
private final long contentLength;
12+
13+
public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName) {
14+
this(name, inputStream, contentLength, fileName, null);
15+
}
16+
17+
public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName, String contentType) {
18+
this(name, inputStream, contentLength, fileName, contentType, null);
19+
}
20+
21+
public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName, String contentType, Charset charset) {
22+
this(name, inputStream, contentLength, fileName, contentType, charset, null);
23+
}
24+
25+
public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName, String contentType, Charset charset,
26+
String contentId) {
27+
this(name, inputStream, contentLength, fileName, contentType, charset, contentId, null);
28+
}
29+
30+
public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName, String contentType, Charset charset,
31+
String contentId, String transferEncoding) {
32+
super(name,
33+
contentType,
34+
charset,
35+
fileName,
36+
contentId,
37+
transferEncoding);
38+
this.inputStream = assertNotNull(inputStream, "inputStream");
39+
this.contentLength = contentLength;
40+
}
41+
42+
public InputStream getInputStream() {
43+
return inputStream;
44+
}
45+
46+
public long getContentLength() {
47+
return contentLength;
48+
}
49+
}

client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ public static List<MultipartPart<? extends Part>> generateMultipartParts(List<Pa
7575
} else if (part instanceof StringPart) {
7676
multipartParts.add(new StringMultipartPart((StringPart) part, boundary));
7777

78+
} else if (part instanceof InputStreamPart) {
79+
multipartParts.add(new InputStreamMultipartPart((InputStreamPart) part, boundary));
80+
7881
} else {
7982
throw new IllegalArgumentException("Unknown part type: " + part);
8083
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package org.asynchttpclient.request.body.multipart.part;
2+
3+
import io.netty.buffer.ByteBuf;
4+
import org.asynchttpclient.netty.request.body.BodyChunkedInput;
5+
import org.asynchttpclient.request.body.multipart.InputStreamPart;
6+
7+
import java.io.IOException;
8+
import java.io.InputStream;
9+
import java.nio.ByteBuffer;
10+
import java.nio.channels.Channels;
11+
import java.nio.channels.ReadableByteChannel;
12+
import java.nio.channels.WritableByteChannel;
13+
14+
import static org.asynchttpclient.util.MiscUtils.closeSilently;
15+
16+
public class InputStreamMultipartPart extends FileLikeMultipartPart<InputStreamPart> {
17+
18+
private long position = 0L;
19+
private ByteBuffer buffer;
20+
private ReadableByteChannel channel;
21+
22+
public InputStreamMultipartPart(InputStreamPart part, byte[] boundary) {
23+
super(part, boundary);
24+
}
25+
26+
private ByteBuffer getBuffer() {
27+
if (buffer == null) {
28+
buffer = ByteBuffer.allocateDirect(BodyChunkedInput.DEFAULT_CHUNK_SIZE);
29+
}
30+
return buffer;
31+
}
32+
33+
private ReadableByteChannel getChannel() {
34+
if (channel == null) {
35+
channel = Channels.newChannel(part.getInputStream());
36+
}
37+
return channel;
38+
}
39+
40+
@Override
41+
protected long getContentLength() {
42+
return part.getContentLength();
43+
}
44+
45+
@Override
46+
protected long transferContentTo(ByteBuf target) throws IOException {
47+
InputStream inputStream = part.getInputStream();
48+
int transferred = target.writeBytes(inputStream, target.writableBytes());
49+
if (transferred > 0) {
50+
position += transferred;
51+
}
52+
if (position == getContentLength() || transferred < 0) {
53+
state = MultipartState.POST_CONTENT;
54+
inputStream.close();
55+
}
56+
return transferred;
57+
}
58+
59+
@Override
60+
protected long transferContentTo(WritableByteChannel target) throws IOException {
61+
ReadableByteChannel channel = getChannel();
62+
ByteBuffer buffer = getBuffer();
63+
64+
int transferred = 0;
65+
int read = channel.read(buffer);
66+
67+
if (read > 0) {
68+
buffer.flip();
69+
while (buffer.hasRemaining()) {
70+
transferred += target.write(buffer);
71+
}
72+
buffer.compact();
73+
position += transferred;
74+
}
75+
if (position == getContentLength() || read < 0) {
76+
state = MultipartState.POST_CONTENT;
77+
if (channel.isOpen()) {
78+
channel.close();
79+
}
80+
}
81+
82+
return transferred;
83+
}
84+
85+
@Override
86+
public void close() {
87+
super.close();
88+
closeSilently(part.getInputStream());
89+
closeSilently(channel);
90+
}
91+
92+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright (c) 2010-2012 Sonatype, Inc. All rights reserved.
3+
*
4+
* This program is licensed to you under the Apache License Version 2.0,
5+
* and you may not use this file except in compliance with the Apache License Version 2.0.
6+
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
7+
*
8+
* Unless required by applicable law or agreed to in writing,
9+
* software distributed under the Apache License Version 2.0 is distributed on an
10+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
12+
*/
13+
package org.asynchttpclient.request.body;
14+
15+
import org.asynchttpclient.AbstractBasicTest;
16+
import org.asynchttpclient.AsyncHttpClient;
17+
import org.asynchttpclient.Response;
18+
import org.asynchttpclient.request.body.multipart.FilePart;
19+
import org.asynchttpclient.request.body.multipart.InputStreamPart;
20+
import org.eclipse.jetty.server.Request;
21+
import org.eclipse.jetty.server.handler.AbstractHandler;
22+
import org.testng.annotations.Test;
23+
24+
import javax.servlet.ServletInputStream;
25+
import javax.servlet.http.HttpServletRequest;
26+
import javax.servlet.http.HttpServletResponse;
27+
import java.io.*;
28+
29+
import static java.nio.charset.StandardCharsets.UTF_8;
30+
import static org.asynchttpclient.Dsl.asyncHttpClient;
31+
import static org.asynchttpclient.Dsl.config;
32+
import static org.asynchttpclient.test.TestUtils.LARGE_IMAGE_FILE;
33+
import static org.asynchttpclient.test.TestUtils.createTempFile;
34+
import static org.testng.Assert.assertEquals;
35+
36+
public class InputStreamPartLargeFileTest extends AbstractBasicTest {
37+
38+
@Override
39+
public AbstractHandler configureHandler() throws Exception {
40+
return new AbstractHandler() {
41+
42+
public void handle(String target, Request baseRequest, HttpServletRequest req, HttpServletResponse resp) throws IOException {
43+
44+
ServletInputStream in = req.getInputStream();
45+
byte[] b = new byte[8192];
46+
47+
int count;
48+
int total = 0;
49+
while ((count = in.read(b)) != -1) {
50+
b = new byte[8192];
51+
total += count;
52+
}
53+
resp.setStatus(200);
54+
resp.addHeader("X-TRANSFERRED", String.valueOf(total));
55+
resp.getOutputStream().flush();
56+
resp.getOutputStream().close();
57+
58+
baseRequest.setHandled(true);
59+
}
60+
};
61+
}
62+
63+
@Test
64+
public void testPutImageFile() throws Exception {
65+
try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) {
66+
InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE));
67+
Response response = client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.length(), LARGE_IMAGE_FILE.getName(), "application/octet-stream", UTF_8)).execute().get();
68+
assertEquals(response.getStatusCode(), 200);
69+
}
70+
}
71+
72+
@Test
73+
public void testPutLargeTextFile() throws Exception {
74+
File file = createTempFile(1024 * 1024);
75+
InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
76+
77+
try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) {
78+
Response response = client.preparePut(getTargetUrl())
79+
.addBodyPart(new InputStreamPart("test", inputStream, file.length(), file.getName(), "application/octet-stream", UTF_8)).execute().get();
80+
assertEquals(response.getStatusCode(), 200);
81+
}
82+
}
83+
}

client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
import org.asynchttpclient.request.body.Body.BodyState;
2020
import org.testng.annotations.Test;
2121

22-
import java.io.File;
23-
import java.io.IOException;
22+
import java.io.*;
2423
import java.net.URISyntaxException;
2524
import java.net.URL;
2625
import java.nio.ByteBuffer;
@@ -63,7 +62,15 @@ private static File getTestfile() throws URISyntaxException {
6362
}
6463

6564
private static MultipartBody buildMultipart() {
66-
return MultipartUtils.newMultipartBody(PARTS, EmptyHttpHeaders.INSTANCE);
65+
List<Part> parts = new ArrayList<>(PARTS);
66+
try {
67+
File testFile = getTestfile();
68+
InputStream inputStream = new BufferedInputStream(new FileInputStream(testFile));
69+
parts.add(new InputStreamPart("isPart", inputStream, testFile.length(), testFile.getName()));
70+
} catch (URISyntaxException | FileNotFoundException e) {
71+
throw new ExceptionInInitializerError(e);
72+
}
73+
return MultipartUtils.newMultipartBody(parts, EmptyHttpHeaders.INSTANCE);
6774
}
6875

6976
private static long transferWithCopy(MultipartBody multipartBody, int bufferSize) throws IOException {

client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,21 +77,33 @@ public void testSendingSmallFilesAndByteArray() throws Exception {
7777
File testResource1File = getClasspathFile(testResource1);
7878
File testResource2File = getClasspathFile(testResource2);
7979
File testResource3File = getClasspathFile(testResource3);
80+
InputStream inputStreamFile1 = new BufferedInputStream(new FileInputStream(testResource1File));
81+
InputStream inputStreamFile2 = new BufferedInputStream(new FileInputStream(testResource2File));
82+
InputStream inputStreamFile3 = new BufferedInputStream(new FileInputStream(testResource3File));
8083

8184
List<File> testFiles = new ArrayList<>();
8285
testFiles.add(testResource1File);
8386
testFiles.add(testResource2File);
8487
testFiles.add(testResource3File);
88+
testFiles.add(testResource3File);
89+
testFiles.add(testResource2File);
90+
testFiles.add(testResource1File);
8591

8692
List<String> expected = new ArrayList<>();
8793
expected.add(expectedContents);
8894
expected.add(expectedContents2);
8995
expected.add(expectedContents3);
96+
expected.add(expectedContents3);
97+
expected.add(expectedContents2);
98+
expected.add(expectedContents);
9099

91100
List<Boolean> gzipped = new ArrayList<>();
92101
gzipped.add(false);
93102
gzipped.add(true);
94103
gzipped.add(false);
104+
gzipped.add(false);
105+
gzipped.add(true);
106+
gzipped.add(false);
95107

96108
File tmpFile = File.createTempFile("textbytearray", ".txt");
97109
try (OutputStream os = Files.newOutputStream(tmpFile.toPath())) {
@@ -109,8 +121,11 @@ public void testSendingSmallFilesAndByteArray() throws Exception {
109121
.addBodyPart(new StringPart("Name", "Dominic"))
110122
.addBodyPart(new FilePart("file3", testResource3File, "text/plain", UTF_8))
111123
.addBodyPart(new StringPart("Age", "3")).addBodyPart(new StringPart("Height", "shrimplike"))
124+
.addBodyPart(new InputStreamPart("inputStream3", inputStreamFile3, testResource3File.length(), testResource3File.getName(), "text/plain", UTF_8))
125+
.addBodyPart(new InputStreamPart("inputStream2", inputStreamFile2, testResource2File.length(), testResource2File.getName(), "application/x-gzip", null))
112126
.addBodyPart(new StringPart("Hair", "ridiculous")).addBodyPart(new ByteArrayPart("file4",
113127
expectedContents.getBytes(UTF_8), "text/plain", UTF_8, "bytearray.txt"))
128+
.addBodyPart(new InputStreamPart("inputStream1", inputStreamFile1, testResource1File.length(), testResource1File.getName(), "text/plain", UTF_8))
114129
.build();
115130

116131
Response res = c.executeRequest(r).get();
@@ -142,6 +157,28 @@ public void sendEmptyFileZeroCopy() throws Exception {
142157
sendEmptyFile0(false);
143158
}
144159

160+
private void sendEmptyFileInputStream0(boolean disableZeroCopy) throws Exception {
161+
File file = getClasspathFile("empty.txt");
162+
try (AsyncHttpClient c = asyncHttpClient(config().setDisableZeroCopy(disableZeroCopy))) {
163+
InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
164+
Request r = post("http://localhost" + ":" + port1 + "/upload")
165+
.addBodyPart(new InputStreamPart("file", inputStream, file.length(), file.getName(), "text/plain", UTF_8)).build();
166+
167+
Response res = c.executeRequest(r).get();
168+
assertEquals(res.getStatusCode(), 200);
169+
}
170+
}
171+
172+
@Test
173+
public void sendEmptyFileInputStream() throws Exception {
174+
sendEmptyFileInputStream0(true);
175+
}
176+
177+
@Test
178+
public void sendEmptyFileInputStreamZeroCopy() throws Exception {
179+
sendEmptyFileInputStream0(false);
180+
}
181+
145182
/**
146183
* Test that the files were sent, based on the response from the servlet
147184
*/

0 commit comments

Comments
 (0)