Reputation: 14513
My web service hosted on Play! framework. I have few image files uploaded from a non-play! framework based client using the standard HTTP client request with content-type of multipart/form-data.
On the web service side, I tried using Play! ApacheMultipartParser to parse the Http.request.body, but failed with the Java IO Bad File Descriptor exception.
The problem seems come from Java MultipartStream, by looking at the following callstack
at java.io.FileInputStream.readBytes(Native Method)
at java.io.FileInputStream.read(FileInputStream.java:208)
at org.apache.commons.fileupload.MultipartStream$ItemInputStream.makeAvailable(MultipartStream.java:976)
at org.apache.commons.fileupload.MultipartStream$ItemInputStream.read(MultipartStream.java:886)
at java.io.InputStream.read(InputStream.java:85)
I also tried directly reading the http.request.body into a big buffer for experiment, got the same exception. What could be wrong?
The http data sent out from client side is something like the following. On web service side, I could using IO.write to save it to a file w/o any problem.
Content-Type: multipart/form-data; boundary=--3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f
--3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f
Content-Disposition: form-data; name="foo1.jpg"; filename="foo1.jpg"
Content-Length: 5578
Content-Type: image/jpeg
<image data 1 omitted>
--3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f
Content-Disposition: form-data; name="foo2.jpg"; filename="foo2.jpg"
Content-Length: 327
Content-Type: image/jpeg
<image data 2 omitted>
--3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f--
Upvotes: 1
Views: 3006
Reputation: 13622
I had the exact same issue. The problem lies in the way that Play! handles multipart uploads. Usually you can add a FileUpload to your upload method and get your files there. This helps a lot as you can get the filenames and sizes and all this stuff directly from Play:
public static void uploadFile(File fileUpload) {
String name = fileUpload.getName() // etc.
}
However using this logic prevents you from using the HTTPRequest. So if you use a non-Play way of uploading files (e.g with XMLHTTPRequest) where the automatic mapping to the fileUpload won't work the following thing happens:
Now the request input stream has already been consumed by Play and you get your "Bad File Descriptor" message.
The solution to this is, to not use any Play! magic, if you want to use the same method for uploading via Form and XMLHttpRequest (XHR). I wanted to use Valum's file uploader script (http://github.com/valums/file-uploader) in addition to my own form based upload method. One uses XHR, the other uses plain multipart form uploads. I created the following method in my controller, that takes the uploaded file from the "qqfile" parameter and works with form based and XHR-Uploads:
@SuppressWarnings({"UnusedDeclaration"})
public static void uploadFile() {
FileUpload qqfile = null;
DataParser parser = DataParser.parsers.get(request.contentType);
if (parser != null) {
// normal upload. I have to manually parse this because
// play kills the body input stream for XHR-requests when I put the file upload as a method
// argument to {@link #uploadFile)
parser.parse(request.body);
@SuppressWarnings({"unchecked"})
ArrayList<FileUpload> uploads = (ArrayList<FileUpload>) request.args.get("__UPLOADS");
for (FileUpload upload : uploads) {
if ("qqfile".equals(upload.getFieldName())) {
qqfile = upload;
break;
}
}
} else {
// XHR upload
qqfile = new FileUpload(new XHRFileItem("qqfile"));
}
if (qqfile == null) {
badRequest();
return;
}
// and now do something with your Fileupload object here (e.g. write it to db or something else)
}
You probably can skip the IF-part of the if, if you split this method into two, so you can use the normal Play! magic for default uploads and use a separate method for your XHR uploads.
I also had to create the XHRFileItem
class which just wraps around a file item that is posted via an XMLHttpRequest. You might have to modify it a bit to work with multiple files and your particular file uploader, but nevertheless here it is:
package application.util;
import org.apache.commons.fileupload.FileItem;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import static play.mvc.Http.Request.current;
/**
* An implementation of FileItem to deal with XmlHttpRequest file uploads.
*/
public class XHRFileItem implements FileItem {
private String fieldName;
public XHRFileItem(String fieldName) {
this.fieldName = fieldName;
}
public InputStream getInputStream() throws IOException {
return current().body;
}
public String getContentType() {
return current().contentType;
}
public String getName() {
String fileName = current().params.get(fieldName);
if (fileName == null) {
fileName = current().headers.get("x-file-name").value();
}
return fileName;
}
public boolean isInMemory() {
return false;
}
public long getSize() {
return 0;
}
public byte[] get() {
return new byte[0];
}
public String getString(String s) throws UnsupportedEncodingException {
return s;
}
public String getString() {
return "";
}
public void write(File file) throws Exception {
FileOutputStream fos = new FileOutputStream(file);
InputStream is = getInputStream();
byte[] buf = new byte[64000];
int read;
while ((read = is.read(buf)) != -1) {
fos.write(buf, 0, read);
}
fos.close();
}
public void delete() {
}
public String getFieldName() {
return fieldName;
}
public void setFieldName(String fieldName) {
this.fieldName = fieldName;
}
public boolean isFormField() {
return false;
}
public void setFormField(boolean b) {
}
@Nullable
public OutputStream getOutputStream() throws IOException {
return null;
}
}
Hope this helps, it took me about a day to make this work on my end.
Upvotes: 3