Skip to main content

File Upload Implementation with Android + Node

Free2016-03-20#Node#Android#安卓文件上传#android file upload#安卓multipart#android multipart form data#Node文件上传

Node allows for rapid server creation and supports fine-grained control, with a vast number of open-source modules offering support for more complex operations.

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.

  • gotev/android-upload-service

    A more mature and popular Android upload component that provides server-side implementations for Node and PHP. Of course, it is also relatively large.

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment