【Node.js】Koaで multipart/form-data のファイルを保存する

今回は久しぶりのNodejs関連です。Koa(WAF)でmultipart/form-dataのファイルを保存する方法について。
co-busboyを使ってmultipart/form-dataのハンドリングを行い, Uploadされた画像を保存します。

nodejs

Environment

ES6で追加された同期処理っぽい処理ができる yield を使うためには, v0.11.xx以降が必要です。
Koa + co-busboy をインストールします。busboyはHTMLフォームデータのための StreamingPasrser で, co-busboyは ES6 の yield 対応版です。

# OS X 10.9.4 using node v0.11.12
$ node -e 'console.log(process.versions.v8)'
3.22.24.19
$ npm install koa koa-route co-busboy

v0.11.12のインストールは以下。

$ git clone git://github.com/creationix/nvm.git ~/.nvm
$ source ~/.nvm/nvm.sh
$ nvm install 0.11.12
$ nvm use 0.11.12
$ node -v

Client

クライアント側のコードです。formで取得した画像データをAjaxでPOSTします。

$.ajax('/upload', {method: 'POST',contentType: false, processData: false,
     data: formData , dataType: 'json',
        error: function() {
          console.log('error')
        },
        success: function(data) {
          alert(data.message)
          console.log('success')
        }
});

FormDataに画像とメッセージ(Key-Value)をappendして一緒にPOSTします。Bodyは以下のようになります。


-----------------------------9840774291617783656799451725 Content-Disposition: form-data; name="image_0"; filename="sample_pic.jpg" Content-Type: image/jpeg ÿØÿà� JFIF� �� � ��ÿþ�;CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 90 ÿÛ�C�   ŸzBÞ• jilÒ¸...
.
.
.
-----------------------------9840774291617783656799451725 Content-Disposition: form-data; name="message" test_msg 

Server

ほぼco-busboyのチュートリアルにある通りです。
クライアントから送信されたデータはpart.lengthで0以外の場合,つまりKey-Value形式はkeyがpart[0],valueはpart[1]で受け取れます。
それ以外のStreamはPipeで繋いで,tmp/img に保存しています。

var koa = require('koa');
var parse = require('co-busboy');
var fs = require('fs');
var app = koa();

// POST /upload
app.use(route.post('/upload', function*() {

    var parts = parse(this);
    var part;

    while (part = yield parts) {
        if (part.length) {
            // arrays are busboy fields
            console.log('key: ' + part[0] + " val: " +[part[1]])
        } else {
            // handle stream
            part.pipe(fs.createWriteStream('tmp/img'))
        }
    }

    this.body = {"message": "upload done!"};
}));

簡単な応用例として,DBに直接保存してGETリクエストで直接データを返す方法です。
受け取った画像をMongoDBなりDBにBase64エンコードして保存しておきます。

* Base64はそのアルゴリズム上,ファイルファイズが約30%増えてしまいます

# mongodb
{ "_id" : ObjectId("53e8c3e97151636c2f946f83"), "message" : "test_msg", 
"created_at" : ISODate("2014-08-11T13:23:53.124Z"), "img_0" : { "data" : BinData(0,
"/9j/4AAQSkZJRgABAQAAAQABAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgS
lBFRyB2NjIpLCBxdWFsaXR5ID0gOTAK/9sAQwADAgIDAgIDAwMDBAMDBAUIBQUEBAUKBwcGCAwKDAwL
CgsLDQ4SEA0OEQ4LCxAWEBETFBUVFQwPFxgWFBgSFBUU/9sAQwEDBAQFBAUJBQUJFA0LDRQUFBQUFBQ
UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU/8AAEQgBPwHhAwEiAAIRAQ
...

GETリクエストに対して,以下のようにBase64エンコードされたデータをDBから取得して,直接imgタグのsrcに入れることでクライアント側のブラウザでデコードされて表示されます。

// using mongoose
app.use(route.get('/image', function *(next) {
  var items = yield  sampleImageModel.find({ name: /^test_image/ }).exec();
  var dataBase64 =  items[0]["img_0"].data

  this.body = '';
}))

余計な手間がなくて良いですね。

ファイルIO (fs) は書き込む時は fs.write で, 削除は fs.unlink なのが分かりにくい。ディレクトリ削除は fs.rmdir。


[1] KoaのExamples
[2] yieldableについて。
[3] mongoose(object modeling tool)の使い方
[4] ここはstdinを使うときのハマりどころについて。
[5] devnullは要らないstreamを/dev/nullで破棄するときに使える。
[6] Nodejs関連の欲しいmoduleはここで大体見つかる