Foreword
When uploading files, it is usually necessary to attach extra information, such as userid and other data closely related to the file that the server cannot obtain directly. Therefore, multipart/form-data is a flexible choice, allowing the simultaneous submission of files and their associated user data through a form.
I. Android Client
1. Wrapping the HTTP Communication Process
First, we don't want the file upload process to interfere with UI updates. Here, we implement it using AsyncTask:
public class HttpUpload extends AsyncTask<Void, Integer, Void> {
public HttpUpload(Context context, String filePath) {
super();
this.context = context; // Used for UI updates (showing progress)
this.filePath = filePath;
}
@Override
protected void onPreExecute() {
// Set client parameters
// Show progress bar/dialog
}
@Override
protected Void doInBackground(Void... params) {
// Create post request
// Fill form fields and values
// Listen for progress
// Send post request
// Handle response results
}
@Override
protected void onProgressUpdate(Integer... progress) {
// Update progress
}
@Override
protected void onPostExecute(Void result) {
// Hide progress
}
}
2. Implementing HTTP Communication
HTTP communication can be implemented through Apache HttpClient, as follows:
//--- onPreExecute
// Set client parameters
int timeout = 10000;
HttpParams httpParameters = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParameters, timeout);
HttpConnectionParams.setSoTimeout(httpParameters, timeout);
// Create client
client = new DefaultHttpClient(httpParameters);
// Create and show progress
System.out.println("upload start");
//---doInBackground
try {
File file = new File(filePath);
// Create post request
HttpPost post = new HttpPost(url);
// Create multipart body
// and listen for progress
MultipartEntity entity = new MyMultipartEntity(new ProgressListener() {
@Override
public void transferred(long num) {
// Call the onProgressUpdate method with the percent
// completed
// publishProgress((int) ((num / (float) totalSize) * 100));
System.out.println(num + " - " + totalSize);
}
});
// Fill file (can have multiple files)
ContentBody cbFile = new FileBody(file, "image/png");
entity.addPart("source", cbFile); // Equivalent to <input type="file" name="source">
// Fill fields
entity.addPart("userid", new StringBody("u30018512", Charset.forName("UTF-8")));
entity.addPart("username", new StringBody("中文不乱码", Charset.forName("UTF-8")));
// Initialize total body length (to describe progress)
int totalSize = entity.getContentLength();
// Set the body of the post request
post.setEntity(entity);
// Send the post request
HttpResponse response = client.execute(post);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
// Returns 200
String fullRes = EntityUtils.toString(response.getEntity());
System.out.println("OK: " + fullRes);
} else {
// Other error status codes
System.out.println("Error: " + statusCode);
}
} catch (ClientProtocolException e) {
// Any error related to the Http Protocol (e.g. malformed url)
e.printStackTrace();
} catch (IOException e) {
// Any IO error (e.g. File not found)
e.printStackTrace();
}
//--- onProgressUpdate
// Update progress
//--- onPostExecute
// Hide progress
3. Tracking Progress
Apache HttpClient does not provide progress data itself; it needs to be implemented manually by overriding MultipartEntity:
public class MyMultipartEntity extends MultipartEntity {
private final ProgressListener listener;
public MyMultipartEntity(final ProgressListener listener) {
super();
this.listener = listener;
}
public MyMultipartEntity(final HttpMultipartMode mode, final ProgressListener listener) {
super(mode);
this.listener = listener;
}
public MyMultipartEntity(HttpMultipartMode mode, final String boundary, final Charset charset,
final ProgressListener listener) {
super(mode, boundary, charset);
this.listener = listener;
}
@Override
public void writeTo(final OutputStream outstream) throws IOException {
super.writeTo(new CountingOutputStream(outstream, this.listener));
}
public static interface ProgressListener {
void transferred(long num);
}
public static class CountingOutputStream extends FilterOutputStream {
private final ProgressListener listener;
private long transferred;
public CountingOutputStream(final OutputStream out, final ProgressListener listener) {
super(out);
this.listener = listener;
this.transferred = 0;
}
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
this.transferred += len;
this.listener.transferred(this.transferred);
}
public void write(int b) throws IOException {
out.write(b);
this.transferred++;
this.listener.transferred(this.transferred);
}
}
}
Internally, it supports counting by overriding FilterOutputStream to get progress data.
4. Invoking HttpUpload
Simply call new HttpUpload(this, filePath).execute(); wherever needed; it is no different from a regular AsyncTask.
II. Node Server
1. Setting up the Server
express can quickly set up an HTTP server, as follows:
var app = express();
// ... routing control
app.listen(3000);
The simplest way is, of course, the classic Hello World http.createServer().listen(3000). express is used here primarily to simplify routing control and middleware management. express provides a mature mechanism for routing and middleware; writing it yourself would be... a bit of a hassle.
2. Multipart Request Preprocessing
Request preprocessing is the duty of middleware. Here, we use the Connect middleware: connect-multiparty
// Middleware preprocessing
app.use('/upload', require('connect-multiparty')());
// Handle multipart post request
app.post('/upload', function(req, res){
console.log(req.body.userid);
console.log(req.body.username);
console.log('Received file:\n' + JSON.stringify(req.files));
var imageDir = path.join(__dirname, 'images');
var imagePath = path.join(imageDir, req.files.source.name);
// if exists
fs.stat(imagePath, function(err, stat) {
if (err && err.code !== 'ENOENT') {
res.writeHead(500);
res.end('fs.stat() error');
}
else {
// already exists, gen a new name
if (stat && stat.isFile()) {
imagePath = path.join(imageDir, new Date().getTime() + req.files.source.name);
}
// rename
fs.rename(
req.files.source.path,
imagePath,
function(err){
if(err !== null){
console.log(err);
res.send({error: 'Server Writing Failed'});
} else {
res.send('ok');
}
}
);
}
});
});
This part simply uses rename to move the image to the target path. More complex operations, such as creating thumbnails, cropping, or compositing (watermarks), can be completed via relevant open-source modules, such as imagemagick.
connect-multiparty receives all files as a stream to the system's temporary folder upon receiving the req (of course, the receiving path can be set via the uploadDir property, and limit parameters like maxFields and maxFieldsSize can also be set; for detailed usage, please see andrewrk/node-multiparty). At the same time, it parses form fields and finally attaches the processing result to the req object (req.body is an object composed of form fields and values, and req.files is an object composed of temporary file objects). For example:
u30018512
中文不乱码
Received file:
{"source":{"fieldName":"source","originalFilename":"activity.png","path":"C:\\Us
ers\\ay\\AppData\\Local\\Temp\\KjgxW_Rmz8XL1er1yIVhEqU9.png","headers":{"content
-disposition":"form-data; name=\"source\"; filename=\"activity.png\"","content-t
ype":"image/png","content-transfer-encoding":"binary"},"size":66148,"name":"acti
vity.png","type":"image/png"}}
After obtaining the temporary file path, rename it to place the file in the server's target location.
Note: On Windows, the temporary folder is C:/Users/[username]/AppData/Local/Temp/. If the target path is not on the C drive, an error will occur (cross-drive moves will cause a rename error), as shown below:
{ [Error: EXDEV, rename 'C:\Users\ay\AppData\Local\Temp\KjgxW_Rmz8XL1er1yIVhEqU9
.png']
errno: -4037,
code: 'EXDEV',
path: 'C:\\Users\\ay\\AppData\\Local\\Temp\\KjgxW_Rmz8XL1er1yIVhEqU9.png' }
As for the solution, I don't recommend using Windows for Node.js development, as you'll encounter all sorts of strange problems that only add to the frustration. Besides, installing a virtual machine with VirtualBox is not that difficult.
3. Serving Static Files
After the file is uploaded, if it is to be accessible (such as an image), you also need to serve static files. With express, you can just configure a static directory, which saves a lot of trouble, as follows:
// static files
app.use('/images', express.static(path.join(__dirname, 'images')));
III. Project Address
GitHub: ayqy/MultipartUpload
P.S. Speaking of which, the GitHub homepage is empty, which is a bit hard to justify. In the coming period, I will try to put some things on there. One of the career guides for engineers is to accumulate a portfolio, and by now there is no reason to be lazy. Cheer up, ready to work!
References
-
danysantiago/NodeJS-Android-Photo-Upload
This article is forked from that project, but the original project has been out of repair for a long time (Latest commit 518c106 on 19 Nov 2012), and the author has made some repairs.
-
A more mature and popular Android upload component that provides server-side implementations for Node and PHP. Of course, it is also relatively large.
No comments yet. Be the first to share your thoughts.