メインコンテンツへ移動

Android + Node によるファイルアップロードの実装

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

Nodeを使うことでサーバーを迅速に構築でき、細かな制御が可能なほか、膨大なオープンソースモジュールにより高度な操作もサポートされます。

はじめに

ファイルをアップロードする際、通常は userid など、ファイルと密接に関連しておりサーバー側で直接取得できない追加情報が必要になります。そのため、 multipart/form-data は柔軟な選択肢であり、フォームを通じてファイルと関連するユーザーデータを同時に送信できます。

1. Androidクライアント

1. HTTP通信プロセスのラップ

まず、ファイルのアップロードプロセスがUIの更新に影響を与えないようにしたいため、ここでは AsyncTask を使って実装します:

public class HttpUpload extends AsyncTask<Void, Integer, Void> {

    public HttpUpload(Context context, String filePath) {
        super();
        this.context = context;     // UIの更新(進捗表示)に使用
        this.filePath = filePath;
    }

    @Override
    protected void onPreExecute() {
        // クライアントパラメータの設定
        // プログレスバー/ダイアログの表示
    }
    
    @Override
    protected Void doInBackground(Void... params) {
        // POSTリクエストの作成
        // フォームフィールドと値の入力
        // 進捗の監視
        // POSTリクエストの送信
        // レスポンス結果の処理
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        // 進捗の更新
    }

    @Override
    protected void onPostExecute(Void result) {
        // 進捗表示の非表示
    }
}

2. HTTP通信の実装

HTTP通信は、以下のように Apache HttpClient を使用して実現できます:

//--- onPreExecute
// クライアントパラメータの設定
int timeout = 10000;
HttpParams httpParameters = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParameters, timeout);
HttpConnectionParams.setSoTimeout(httpParameters, timeout);
// クライアントの作成
client = new DefaultHttpClient(httpParameters);
// 進捗表示の作成と表示
System.out.println("upload start");

//--- doInBackground
try {
    File file = new File(filePath);

    // POSTリクエストの作成
    HttpPost post = new HttpPost(url);

    // multipartメッセージボディの作成
    // および進捗の監視
    MultipartEntity entity = new MyMultipartEntity(new ProgressListener() {
        @Override
        public void transferred(long num) {
            // 完了した割合で onProgressUpdate メソッドを呼び出す
            // publishProgress((int) ((num / (float) totalSize) * 100));
            System.out.println(num + " - " + totalSize);
        }
    });
    // ファイルの入力(複数ファイル可)
    ContentBody cbFile = new FileBody(file, "image/png");
    entity.addPart("source", cbFile);   // <input type="file" name="source"> に相当
    // フィールドの入力
    entity.addPart("userid", new StringBody("u30018512", Charset.forName("UTF-8")));
    entity.addPart("username", new StringBody("中文不乱码", Charset.forName("UTF-8")));

    // メッセージボディの総長の初期化(進捗記述用)
    int totalSize = entity.getContentLength();

    // POSTリクエストのメッセージボディを設定
    post.setEntity(entity);

    // POSTリクエストの送信
    HttpResponse response = client.execute(post);
    int statusCode = response.getStatusLine().getStatusCode();

    if (statusCode == HttpStatus.SC_OK) {
        // 200を返す
        String fullRes = EntityUtils.toString(response.getEntity());
        System.out.println("OK: " + fullRes);
    } else {
        // その他のエラーステータスコード
        System.out.println("Error: " + statusCode);
    }
} catch (ClientProtocolException e) {
    // HTTPプロトコル関連のエラー(URLの形式不正など)
    e.printStackTrace();
} catch (IOException e) {
    // IOエラー(ファイルが見つからないなど)
    e.printStackTrace();
}

//--- onProgressUpdate
// 進捗の更新

//--- onPostExecute
// 進捗表示の非表示

3. 進捗の記録

Apache HttpClient 自体は進捗データを提供していないため、ここでは 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);
        }
    }
}

内部で FilterOutputStream をオーバーラ���ドしてカウントをサポートすることで、進捗データを取得します。

4. HttpUploadの呼び出し

必要な場所で new HttpUpload(this, filePath).execute(); を呼び出すだけです。一般的な AsyncTask と大きな違いはありません。

2. Nodeサーバーサイド

1. サーバーの構築

express を使用して、以下のように素早くHTTPサーバーを構築します:

var app = express();
// ...ルーティング制御
app.listen(3000);

最も簡単な方法は、古典的な Hello World のような http.createServer().listen(3000) ですが、ここで express を使用するのは主にルーティング制御とミドルウェア管理を簡素化するためです。 express は成熟したルーティング制御とミドルウェア管理メカニズムを提供しており、自分で書くのは少し手間がかかります。

2. multipartリクエストのプリプロセス

リクエストのプリプロセスはミドルウェアの本来の役割です。ここではConnectミドルウェアの connect-multiparty を採用します:

// ミドルウェアによるプリプロセス
app.use('/upload', require('connect-multiparty')());

// multipart POSTリクエストの処理
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);
    // 存在チェック
    fs.stat(imagePath, function(err, stat) {
        if (err && err.code !== 'ENOENT') {
            res.writeHead(500);
            res.end('fs.stat() error');
        }
        else {
            // 既に存在する場合は新しい名前を生成
            if (stat && stat.isFile()) {
                imagePath = path.join(imageDir, new Date().getTime() + req.files.source.name);
            }
            // リネーム
            fs.rename(
                req.files.source.path,
                imagePath,
                function(err){
                    if(err !== null){
                        console.log(err);
                        res.send({error: 'Server Writting Failed'});
                    } else {
                        res.send('ok');
                    }
                }
            );
        }
    });
});

ここでは、単に rename を使って画像を目的のパスに配置するだけの処理を行っています。サムネイルの作成、クロップ、合成(ウォーターマーク)などのより複雑な操作は、 imagemagick などの関連するオープンソースモジュールを通じて実行できます。

connect-multipartyreq を受け取ると、まずシステムの一時フォルダにすべてのファイルをストリームとして受信します(もちろん、受信パスは uploadDir 属性で設定でき、 maxFieldsmaxFieldsSize などの制限パラメータも設定可能です。詳細は andrewrk/node-multiparty を参照してください)。同時にフォームフィールドを解析し、最終的に処理結果を req オブジェクトに紐付けます( req.body はフォームフィールドとその値のオブジェクト、 req.files は一時ファイルオブジェクトのオブジェクトです)。例:

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"}}

一時ファイルのパスを取得した後、 rename を行うことでファイルをサーバーの目的の場所に配置します。

注意:Windows環境では一時フォルダは C:/Users/[username]/AppData/Local/Temp/ になります。目的のパスがCドライブ以外にある場合はエラーが発生します(ドライブをまたぐと rename が失敗するため)。以下の通りです:

{ [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' }

解決策についてですが、WindowsでNodeをいじるのはお勧めしません。様々な奇妙な問題に遭遇し、煩わしさが増すだけです。それに、VBoxで仮想マシンを構築するのもそれほど手間ではありません。

3. 静的ファイルのレスポンス

ファイルをアップロードした後、それがアクセス可能なもの(画像など)であれば、静的ファイルとしてレスポンスを返す必要があります。 express では静的ディレクトリを設定するだけで済むので、非常に楽です。以下の通りです:

// static files
app.use('/images', express.static(path.join(__dirname, 'images')));

3. プロジェクトのURL

GitHub: ayqy/MultipartUpload

P.S. そういえば、GitHubのプロフィールが空っぽなのは少し格好がつかないので、これからは積極的に何か公開していこうと思います。エンジニアのキャリアガイドの一つに「ポートフォリオを蓄積すること」がありますし、もう怠ける理由はありません。気合を入れて取り組んでいきます。

参考文献

  • danysantiago/NodeJS-Android-Photo-Upload

    この記事はこのプロジェクトをフォークしたものですが、元のプロジェクトは長らくメンテナンスされていなかったため(最終コミットは2012年11月19日の 518c106)、筆者が修正を加えました。

  • gotev/android-upload-service

    より成熟し、人気のあるAndroid用アップロードコンポーネントです。NodeとPHPのサーバーサイド実装も提供されていますが、当然ながら規模も比較的大きくなります。

コメント

コメントはまだありません

コメントを書く