Skip to content

Commit 6103cd3

Browse files
committed
file uploads/send file fix #340
1 parent 025e2cd commit 6103cd3

8 files changed

Lines changed: 187 additions & 48 deletions

File tree

coverage-report/src/test/java/org/jooby/DownloadFeature.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
public class DownloadFeature extends ServerFeature {
99

1010
{
11+
get("/content-disposition", (req, rsp) -> rsp
12+
.header("content-disposition", "attachment; filename=myfile.txt;")
13+
.download("name.js", new File("src/test/resources/"
14+
+ DownloadFeature.class.getName().replace('.', '/') + ".js")));
15+
1116
get("/download-reader", (req, rsp) -> rsp.download("name.js",
1217
new File("src/test/resources/"
1318
+ DownloadFeature.class.getName().replace('.', '/') + ".js")));
@@ -20,7 +25,8 @@ public class DownloadFeature extends ServerFeature {
2025

2126
get("/location", (req, rsp) -> rsp.download("name", req.param("file").value()));
2227

23-
get("/file", (req, rsp) -> rsp.download(DownloadFeature.class.getName().replace('.', '/') + ".json"));
28+
get("/file",
29+
(req, rsp) -> rsp.download(DownloadFeature.class.getName().replace('.', '/') + ".json"));
2430

2531
get("/fs", (req, rsp) -> rsp.download(new File(req.param("file").value())));
2632

@@ -39,6 +45,16 @@ public void downloadReader() throws Exception {
3945
.header("Content-Type", "application/javascript;charset=UTF-8");
4046
}
4147

48+
@Test
49+
public void contentDisposition() throws Exception {
50+
request()
51+
.get("/content-disposition")
52+
.expect(200)
53+
.header("Content-Disposition", "attachment; filename=myfile.txt;")
54+
.header("Content-Length", "20")
55+
.header("Content-Type", "application/javascript;charset=UTF-8");
56+
}
57+
4258
@Test
4359
public void downloadBinWithLocation() throws Exception {
4460
request()
@@ -54,7 +70,8 @@ public void downloadBinWithFsLocation() throws Exception {
5470
request()
5571
.get("/fs?file=src/test/resources/" + getClass().getName().replace('.', '/') + ".ico")
5672
.expect(200)
57-
.header("Content-Disposition", "attachment; filename=\"DownloadFeature.ico\"; filename*=utf-8''DownloadFeature.ico")
73+
.header("Content-Disposition",
74+
"attachment; filename=\"DownloadFeature.ico\"; filename*=utf-8''DownloadFeature.ico")
5875
.header("Content-Length", "2238")
5976
.header("Content-Type", "image/x-icon");
6077
}
@@ -82,7 +99,8 @@ public void downloadTextWithFsLocation() throws Exception {
8299
request()
83100
.get("/fs?file=src/test/resources/" + getClass().getName().replace('.', '/') + ".js")
84101
.expect("(function () {})();\n")
85-
.header("Content-Disposition", "attachment; filename=\"DownloadFeature.js\"; filename*=utf-8''DownloadFeature.js")
102+
.header("Content-Disposition",
103+
"attachment; filename=\"DownloadFeature.js\"; filename*=utf-8''DownloadFeature.js")
86104
.header("Content-Length", "20")
87105
.header("Content-Type", "application/javascript;charset=UTF-8");
88106
}
@@ -92,7 +110,8 @@ public void customType() throws Exception {
92110
request()
93111
.get("/customtype?type=json")
94112
.expect("{}\n")
95-
.header("Content-Disposition", "attachment; filename=\"name.json\"; filename*=utf-8''name.json")
113+
.header("Content-Disposition",
114+
"attachment; filename=\"name.json\"; filename*=utf-8''name.json")
96115
.header("Content-Length", "3")
97116
.header("Content-Type", "application/json;charset=UTF-8");
98117
}
@@ -102,7 +121,8 @@ public void downladInputStream() throws Exception {
102121
request()
103122
.get("/favicon.ico")
104123
.expect(200)
105-
.header("Content-Disposition", "attachment; filename=\"favicon.ico\"; filename*=utf-8''favicon.ico")
124+
.header("Content-Disposition",
125+
"attachment; filename=\"favicon.ico\"; filename*=utf-8''favicon.ico")
106126
.header("Content-Length", "2238")
107127
.header("Content-Type", "image/x-icon");
108128
}
@@ -112,7 +132,8 @@ public void download() throws Exception {
112132
request()
113133
.get("/file")
114134
.expect(200)
115-
.header("Content-Disposition", "attachment; filename=\"downloadfeature.json\"; filename*=utf-8''downloadfeature.json")
135+
.header("Content-Disposition",
136+
"attachment; filename=\"downloadfeature.json\"; filename*=utf-8''downloadfeature.json")
116137
.header("Content-Length", "3")
117138
.header("Content-Type", "application/json;charset=utf-8");
118139
}

coverage-report/src/test/java/org/jooby/MultipartFormParamFeature.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public String optional(final Optional<Upload> upload) throws IOException {
6767
});
6868

6969
post("/form/files", (req, rsp) -> {
70-
List<Upload> uploads = req.param("uploads").toList(Upload.class);
70+
List<Upload> uploads = req.files("uploads");
7171
StringBuilder buffer = new StringBuilder();
7272
for (Upload upload : uploads) {
7373
try (Upload u = upload) {
@@ -78,7 +78,7 @@ public String optional(final Optional<Upload> upload) throws IOException {
7878
});
7979

8080
post("/form/use/file", (req, rsp) -> {
81-
Upload upload = req.param("myfile").toUpload();
81+
Upload upload = req.file("myfile");
8282
File file = upload.file();
8383
try (Upload u = upload) {
8484
assertEquals("p=1", Files.readAllLines(file.toPath()).stream()

jooby/src/main/java/org/jooby/Request.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ public Mutant param(final String name) {
138138
return req.param(name);
139139
}
140140

141+
@Override
142+
public Upload file(final String name) {
143+
return req.file(name);
144+
}
145+
146+
@Override
147+
public List<Upload> files(final String name) {
148+
return req.files(name);
149+
}
150+
141151
@Override
142152
public Mutant header(final String name) {
143153
return req.header(name);
@@ -548,6 +558,28 @@ default Optional<MediaType> accepts(final MediaType... types) {
548558
*/
549559
Mutant param(String name);
550560

561+
/**
562+
* Get a file {@link Upload} with the given name. The request must be a POST with
563+
* <code>multipart/form-data</code> content-type.
564+
*
565+
* @param name File's name.
566+
* @return An {@link Upload}.
567+
*/
568+
default Upload file(final String name) {
569+
return param(name).toUpload();
570+
}
571+
572+
/**
573+
* Get a list of file {@link Upload} with the given name. The request must be a POST with
574+
* <code>multipart/form-data</code> content-type.
575+
*
576+
* @param name File's name.
577+
* @return A list of {@link Upload}.
578+
*/
579+
default List<Upload> files(final String name) {
580+
return param(name).toList(Upload.class);
581+
}
582+
551583
/**
552584
* Get a HTTP header.
553585
*

jooby/src/main/java/org/jooby/internal/RequestImpl.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
import com.google.inject.Injector;
5757
import com.google.inject.Key;
5858

59+
import javaslang.control.Try;
60+
5961
public class RequestImpl implements Request {
6062

6163
private final Map<String, Mutant> params = new HashMap<>();
@@ -182,7 +184,8 @@ public Mutant params() {
182184
public Mutant param(final String name) {
183185
Mutant param = this.params.get(name);
184186
if (param == null) {
185-
List<NativeUpload> files = files(name);
187+
List<NativeUpload> files = Try.of(() -> req.files(name)).getOrElseThrow(
188+
ex -> new Err(Status.BAD_REQUEST, "Upload " + name + " resulted in error", ex));
186189
if (files.size() > 0) {
187190
List<Upload> uploads = files.stream()
188191
.map(upload -> new UploadImpl(injector, upload))
@@ -276,13 +279,13 @@ public long length() {
276279
@Override
277280
public List<Locale> locales(
278281
final BiFunction<List<Locale.LanguageRange>, List<Locale>, List<Locale>> filter) {
279-
return lang.map(h -> filter.apply(LocaleUtils.range(h), locales))
282+
return lang.map(h -> filter.apply(LocaleUtils.range(h), locales))
280283
.orElseGet(() -> filter.apply(ImmutableList.of(), locales));
281284
}
282285

283286
@Override
284287
public Locale locale(final BiFunction<List<LanguageRange>, List<Locale>, Locale> filter) {
285-
return lang.map(h -> filter.apply(LocaleUtils.range(h), locales))
288+
return lang.map(h -> filter.apply(LocaleUtils.range(h), locales))
286289
.orElseGet(() -> filter.apply(ImmutableList.of(), locales));
287290
}
288291

@@ -369,14 +372,6 @@ public String toString() {
369372
return route().toString();
370373
}
371374

372-
private List<NativeUpload> files(final String name) {
373-
try {
374-
return req.files(name);
375-
} catch (Exception ex) {
376-
throw new Err(Status.BAD_REQUEST, "Upload " + name + " resulted in error", ex);
377-
}
378-
}
379-
380375
private List<String> paramNames() {
381376
try {
382377
return req.paramNames();

jooby/src/main/java/org/jooby/internal/ResponseImpl.java

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,7 @@ public void download(final String filename, final InputStream stream) throws Exc
9999
requireNonNull(stream, "A stream is required.");
100100

101101
// handle type
102-
MediaType type = MediaType.byPath(filename).orElse(MediaType.octetstream);
103-
type(type().orElseGet(() -> type));
102+
type(type().orElseGet(() -> MediaType.byPath(filename).orElse(MediaType.octetstream)));
104103

105104
Asset asset = new InputStreamAsset(stream, filename, type().get());
106105
contentDisposition(filename);
@@ -114,9 +113,8 @@ public void download(final String filename, final String location) throws Except
114113
throw new FileNotFoundException(location);
115114
}
116115
// handle type
117-
MediaType type = MediaType.byPath(filename).orElse(MediaType.byPath(location)
118-
.orElse(MediaType.octetstream));
119-
type(type().orElseGet(() -> type));
116+
type(type().orElseGet(() -> MediaType.byPath(filename).orElse(MediaType.byPath(location)
117+
.orElse(MediaType.octetstream))));
120118

121119
URLAsset asset = new URLAsset(url, location, type().get());
122120
length(asset.length());
@@ -341,15 +339,18 @@ void route(final Route route) {
341339
}
342340

343341
private void contentDisposition(final String filename) throws IOException {
344-
String basename = filename;
345-
int last = filename.lastIndexOf('/');
346-
if (last >= 0) {
347-
basename = basename.substring(last + 1);
348-
}
342+
List<String> headers = rsp.headers("Content-Disposition");
343+
if (headers.isEmpty()) {
344+
String basename = filename;
345+
int last = filename.lastIndexOf('/');
346+
if (last >= 0) {
347+
basename = basename.substring(last + 1);
348+
}
349349

350-
String ebasename = URLEncoder.encode(basename, charset.name()).replaceAll("\\+", "%20");
351-
header("Content-Disposition",
352-
String.format(CONTENT_DISPOSITION, basename, charset.name(), ebasename));
350+
String cs = charset.name();
351+
String ebasename = URLEncoder.encode(basename, cs).replaceAll("\\+", "%20");
352+
header("Content-Disposition", String.format(CONTENT_DISPOSITION, basename, cs, ebasename));
353+
}
353354
}
354355

355356
@SuppressWarnings("unchecked")

jooby/src/test/java/org/jooby/RequestForwardingTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,33 @@ public void charset() throws Exception {
341341
});
342342
}
343343

344+
@Test
345+
public void file() throws Exception {
346+
new MockUnit(Request.class, Upload.class)
347+
.expect(unit -> {
348+
Request req = unit.get(Request.class);
349+
expect(req.file("f")).andReturn(unit.get(Upload.class));
350+
})
351+
.run(unit -> {
352+
assertEquals(unit.get(Upload.class),
353+
new Request.Forwarding(unit.get(Request.class)).file("f"));
354+
});
355+
}
356+
357+
@SuppressWarnings("unchecked")
358+
@Test
359+
public void files() throws Exception {
360+
new MockUnit(Request.class, List.class)
361+
.expect(unit -> {
362+
Request req = unit.get(Request.class);
363+
expect(req.files("f")).andReturn(unit.get(List.class));
364+
})
365+
.run(unit -> {
366+
assertEquals(unit.get(List.class),
367+
new Request.Forwarding(unit.get(Request.class)).files("f"));
368+
});
369+
}
370+
344371
@Test
345372
public void length() throws Exception {
346373
new MockUnit(Request.class)

md/req.md

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
The request object contains methods for reading params, headers and body (between others). In the next section we will mention the most important method of a request object, if you need more information please refer to the [javadoc]({{apidocs}}/org/jooby/Request.html).
44

5-
## request params
5+
## params
66

77
Retrieval of param is done via: [req.param("name")]({{apidocs}}/org/jooby/Request.html#param-java.lang.String-) method.
88

@@ -142,7 +142,7 @@ get("/", req -> {
142142
});
143143
```
144144

145-
## request headers
145+
## headers
146146

147147
Retrieval of request headers is done via: [request.header("name")]({{}}Request.html#header-java.lang.String-). All the explained before for [request params](#request-request-params) apply for headers too.
148148

@@ -172,7 +172,7 @@ get("/", req -> {
172172
});
173173
```
174174

175-
## request body
175+
## body
176176

177177
Retrieval of request body is done via [request.body()]({{defdocs}}/Request.html#body--).
178178

@@ -202,9 +202,39 @@ public void configure(Mode mode, Config config, Binder binder) {
202202
}
203203
```
204204

205-
## local variables
205+
## file upload
206206

207-
Local variables are bound to the current request. They are created every time a new request comes in and destroyed at the end of the request.
207+
File uploads are accessible via: [request.file(name)]({{defdocs}}/Request.html#file-java.lang.String-):
208+
209+
```html
210+
<form enctype="multipart/form-data" action="/upload" method="post">
211+
<input name="myfile" type="file"/>
212+
</form>
213+
```
214+
215+
```java
216+
// Script API
217+
218+
{
219+
post("/upload", req -> {
220+
Upload upload = req.file("myfile");
221+
...
222+
});
223+
}
224+
225+
// MVC API
226+
227+
class Controller {
228+
229+
@Path("/upload") @POST
230+
public Object upload(Upload myfile) {
231+
}
232+
}
233+
```
234+
235+
## locals
236+
237+
Local attributes are bound to the current request. They are created every time a new request comes in and destroyed at the end of the request.
208238

209239
```java
210240
{

0 commit comments

Comments
 (0)