環境
- Ubuntu 16.04 LTS
- Node.js 4.x, 6.x
動機
aws-sdk の AWS.S3#putObject()
1 でファイルをS3にUploadする時、 ContentMD5
で Base64 でデコードしたMD5値を指定すると、アップロード後のMD5と比較してチェックしてくれる。
この機能を使いたくて、Node.jsでMD5値を求める必要があったので備忘録も兼ねてまとめみた。
シンプル版
まずは簡単に同期的に書いてみる
var crypto = require("crypto");
var fs = require("fs");
function md5(filePath) {
let b = fs.readFileSync(filePath);
let md5hash = crypto.createHash('md5');
md5hash.update(b);
return md5hash.digest("base64");
}
問題点
この実装だと、読み込むファイルのサイズが大きいとクラッシュしてしまう。
試しに 8GBファイルのMD5を求めると↓のようになる。
$ node md5pf.js
buffer.js:25
const ui8 = new Uint8Array(size);
^
RangeError: Invalid typed array length
at new Uint8Array (native)
at createBuffer (buffer.js:25:17)
at allocate (buffer.js:158:12)
at new Buffer (buffer.js:56:12)
at Object.fs.readFileSync (fs.js:419:16)
at md5F (/home/yasuhiroki/md5pf.js:33:16)
at Object.<anonymous> (/home/yasuhiroki/md5pf.js:41:13)
at Module._compile (module.js:409:26)
at Object.Module._extensions..js (module.js:416:10)
at Module.load (module.js:343:32)
MD5というより、 readFileSync()
で大きなファイルを読み込もうとしているのが問題。
Streamを使った版
大きなデータを扱うならば Stream を使えば良い。
となると非同期処理になるので、Promiseを返す関数にする。
var crypto = require("crypto");
var fs = require("fs");
function md5(filePath) {
return new Promise((resolve, reject) => {
var readStream = fs.createReadStream(filePath);
var md5hash = crypto.createHash('md5');
md5hash.setEncoding('base64');
readStream.pipe(md5hash);
readStream.on('end', () => {
resolve(md5hash.read());
});
readStream.on('error', (e) => {
reject(e);
});
});
}
crypto は pipe に渡せるので、シンプルに readStream.pipe(md5hash)
と書くことができる。
あとは、 stream の end のタイミングで計算結果を読み込めば良い。
先程クラッシュした8GBファイルのMD5値を計算すると↓のようにうまくいく。
$ node md5pf.js
N11F4wTV+6JFRuh89jqaqQ==
おまけ
そもそもの動機だった、 AWS.S3#putObject()
で ContentMD5を指定してチェックする方法は、サイズの大きいファイルで使うと The Content-MD5 you specified is invalid for multi-part uploads
というエラーが発生する。
巨大なファイルは multipart upload が使用されるので、ファイルが分割されて少しずつアップロードされる。そのため、MD5値は分割された単位で求めなければならず、せっかく↑で求めたMD5値は使えなかった。
https://github.com/aws/aws-sdk-js/blob/5691296bd1c981a4fed04ee6eb42179c277ac079/lib/s3/managed_upload.js#L47 を読むと、代わりに computeChecksums
を使うべしとある。
このフラグを使うと、aws-sdkがいい感じにMD5値のチェックをしてくれるので、自分で計算しなくていい。
も、もっと早くに知っていれば....