Cloud FunctionsからPDFやZipなどのファイルを作成し、ダウンロードURLをフロントに返却するといった処理を実装したときに、次のようなエラーを受けました。
ダウンロードURLをフロントで開くとNoSuchKeyのエラーが表示されてファイルのダウンロードができません。
上記のエラーをうけても、そのままページをリロードすると今度はダウンロードすることができます。つまりダウンロードURLをクリックした時点ではまだダウンロードする対象のファイルがStorageに保存されていないために発生するわけです。
リロードでダウンロードが出来るようになるのは、少しの時間で正しくダウンロードするファイルがStorageに書き込まれ、ダウンロードできるようになったためエラーが発生しないわけです。
参考までにエラーの発生するコードを大幅に簡略化して掲載します
上記コードは未検証です。確かこんな感じだったという記憶を頼りに記述してます
Storageの書き込みが完了するまで処理を待つように処理を止める方法がわからず混乱#
上記コードの問題は色々あるけど何よりも問題となるのが
上記の処理が完了する前に次の行へ処理が流れていってしまうことです。いわゆる非同期処理というやつですね。Nodejsにとって非同期処理はイデオムといえます。
大抵はasync / awaitを使えば問題は解決しますが、nodeJSに不慣れな私にとってpdfDoc.pipe()の完了をどのように受け取るかやり方が分かっていませんでした。
例えばこんな感じで記述ができれば世話ない話なんですけどね。
ただ当然ながらこの記述はうまく機能しません。VSCode上でも警告が発せられるので割と早い段階で気づくことができますが、つまりawaitを使わずにpdfDoc.pipeの完了まで待つ処理を記述する必要があります。
結局のところはPromiseとStreamを混同していたことが原因です。PromiseもStreamも非同期処理で似たようなものだと思いがちですが、当然ながら全くの別ものです。
筆者の環境ではStreamをほぼ使わないため馴染みがなかったのも要因です
Streamの書き込みが完了するまで待ってからダウンロードURLをフロントに返却するようにプログラムを修正する#
pdfDoc.pipeは結局の所Internal.WriteStreamなのでこのStreamが完了するまで処理を止めればよいわけです。cloud Functionsはreturnで関数自体が終了してしまうので、Streamの書き込みが終わるまではreturnを実行してはいけません。
処理を止めるにはPromiseを使えば簡単です。やり方はいくつかあると思います。
pdfDoc.pipeの処理周りをごっそりpromiseでくくってしまい、streamの’finish’を検知してresolveしてあげるように処理を修正しました。
これでstreamの書き込みが終わるまで次の処理へ進むことなく止まってくれます。
なお余談ですが以下のような書き方も可能です
のおかげでStreamの書き込みが完了後に、ダウンロードURLの取得処理が走るようになりました。よって本ページトップで書いたようなnoSuchKeyエラーは発生しなくなります。
SigningErrorが発生する場合#
上記コードを書いて実行すると、Cloud Functionsのログに
と出力されることがあります。これはgetSignedUrl()関数の実行権限が足りていないために起こります。権限を追加するにはFirebaseの外側にあるGCPからIAMを編集する必要があります。
まずはGCPを開き、IAMのページを開きます。IAMページではたくさんのアカウントが並んでいます。ここからプロジェクト名@appspot.gserviceaccount.comのアカウントを見つけ出してください。
名前にApp Engine default service accountと書かれているからすぐ見つかると思うよ
見つけたらそのアカウントの鉛筆ボタンをクリックし、ロールを2つ追加します
- Cloud Datastore インポート / エクスポート管理者
- サービス アカウント トークン作成者
最終的には次のような形になればOKです
IAMの設定が完了したら再び生成を試みてください。筆者の環境ではこれで問題なく動作することが確認できました。
この情報はStackOverFlowに記載されていました。大変助かりました。