🤸‍♂️

busboy と fsモジュールの微妙な関係性

2022/06/04に公開

kirisima部活やめるってよ

皆さんいかがお過ごしでしょうか。NodeJSをつかった開発では画像ファイルなどの送信をbusboyで受け取って、fsでどこかに一旦保存して、storageにあげるみたいな処理をすることが多いのじゃないかなーと思います。僕もその一人です。しかしながらこの間、こんな相思相愛の二人の微妙な関係性をみつけてしまいました。しかしながら、なかなかに言及しづらい部分なのか、あまり記事がなかったので共有いたします!!

ええ、kirisima部活やめるん?マジで?

さて、起きた事象を以下に書き下してみます。最も困ったことはこれが不定期に起きていたことでした。

busboyでContent-type: multipart/form-dataなHTTPリクエストを受け取る
busboy.on('file', func(){})のfunc内でfs.createWriteStreamを用いて、指定の場所に一意のファイルネームをもつjpegファイルを作成
busboy.on('end' func(){})でbusboyの処理を終了
次の処理にてpath/一意のファイルネーム.jpegを見に行くと、ファイルサイズが0
ファイルサイズが0の場合はエラーで止まるようにしていた。

おい、なんだそれ!?!?しかも、これが不定期に起きているだと!?!?!?

おい、kirisima聞いたぞ〜、なんでなん?

僕はサバサバ系なので結論から言ってしまうと、fs.createWriteStreamがpath/一意のファイルネーム.jpegに書き込んでいる最中に、次の処理にてpath/一意のファイルネーム.jpegを見に行ってしまっていたことがでした。
つまり、busboy.on('end' func(){})とwritableなstrema.on('end', func(){})の両方を確認してから、次の処理に進む必要があったわけなんですね。
みなさんも、ちゃんとbusboyのendイベントとfsのendイベントの両方を待つようにしましょうね!!

やめても、kirisimaはkirisimaだから、これからもよろしくな!!

じゃあ、両方まてば終わりじゃん!!というわけなんですが、実際にそのようなコードを書くと大体以下のような感じになりました。雰囲気が伝われば十分なので、ちょっと雑です。

コード1
let writable_stream = '';
busboy.on('file', (file, etc...)=>{
  writable_stream = fs.createWriteStream(filepath);
  file.pipe(writable_stream)
})

const await_busboy_and_fs = new Promise((resolve, reject) => {
  busboy.on('end', () => { return resolve(); })
}).then (() => {
  return new Promise((resolve, reject) => {
    writable_stream.on('end', () => {return resolve();}
  })
});
await Promise.all([await_busboy_and_fs]);

これで、どちらのendイベントも待てるようになったわけですが、こちらの欠点としては、fsイベントのendイベントが受け取れない可能性があるということです。なぜなら、busboy.on('end')の処理が終わってからしか、writable_stream.on('end')を受け取れないからです。
なので本当は以下のようにかきたかったです。

コード2
let writable_stream = '';
busboy.on('file', (file, etc...)=>{
  writable_stream = fs.createWriteStream(filepath);
  file.pipe(writable_stream)
})

const await_busboy = new Promise((resolve, reject) => {
  busboy.on('end', () => { return resolve(); })
});

const await_fs = new Promise((resolve, reject) => {
  writable_stream.on('end', () => { return resolve(); })
});

await Promise.all([await_busboy, await_fs]);

しかしながら、これだとエラーがでます。なぜなら、busboy.on('file')内で初めて、writableなstreamを生成しているからです。なので確実にbusboy.on('file')内の処理を通ったあとに、writable_stream.on('end')は書かないといけないという制約が爆誕しているのです。

そう、これが誰も言及したがらない、busboyとfsモジュールの微妙な関係ということです。

kirisimaのその後

じゃあ結局どうしたのかといえば、コード1で行いました。理由としては以下の2点です
・実行ログをみていると、改修入れる前からbusboyのendイベントが常に先に来ていた。
・busboy('end')内ではほぼ処理をしていない。
ということで、コード1でも成立しました。

しかしながら、個人的には画像データをbase64でエンコードしてjsonにつめてしまうというのが良いと思います。ただ、そうすると送信するデータ量が増えてしまうはずなのでそことの相談ですね。

Discussion