【golang】io.Readerを使いまわしてContentType判定、S3アップロードしたらハマった話

前書き:同じハマりを繰り返す

Single Page Application(SPA)をAmazon S3にアップロードする機能を持つspareコマンドを開発しているとき、io.Readerの使い方を間違えて少しハマってしまいました。ハマりの原因はio.Readerで読み出すデータが欠損していたことであり、欠損の原因はio.Readerを使いまわしたことです。

  

このハマり方は2回目なので、備忘録として記事にします。

   

ことの発端(時系列)

  1. spareコマンドにファイルをS3へアップロードする機能を追加し、S3にSPAをアップロード
  2. CloudFront経由でS3に格納されているindex.htmlをチェックしたら、画面には何も表示されず、ダウンロード処理が開始された
  3. 上記2.の挙動となった理由は、ファイルをS3へアップロードする時にContentTypeを指定していなかったため
  4. httpパッケージのDetectContentTypeメソッドでファイルのコンテンツタイプを判定し、その情報をS3へのアップロード時に付与する実装に変更
  5. SPAをS3へアップロードし直し
  6. 再度CloudFront経由でindex.htmlをチェックしたら、ダウンロード処理が開始されなくなったが、ブラウザには何も表示されなかった 

       

どこが駄目だったか

以下のUploadFileメソッドは、引数input.Dataを持っており、この引数がio.Readerです。

input.Data は、detectContentType() と s3manager.UploadInput() に渡されています。

 

detectContentType()は、io.Readerから512バイト読み込み、読み込んだデータからコンテンツタイプを判定します。

s3manager.UploadInput()は、渡されたio.Readerからデータを読み込み、S3へアップロードします

このinput.Dataの使い回しが原因で、ブラウザでindex.htmlを確認する時に何も表示されませんでした。

            

何が悪いか:io.Readerは読込毎に読込開始位置が進む

私は古い人間なので、「io.Readerは、C言語のファイルポインタと一緒か」とすぐ思いました。

 

io.Readerはデータの読み込み処理を実行すると、ファイルの読み込み開始位置が進みます。そのため、先程の例では「コンテンツタイプを読み込むために512Byte分、読み込み開始位置が移動」「S3へのアップロード時は、ファイルの先頭から512Byte進んだ地点のデータからファイル終端までをアップロード」という挙動をしていたことになります。

    

解決策1:io.ReadSeekerなどのio.Seekerインターフェースを利用

先程の例のinput.Data(io.Reader)がio.Seekerを満たす場合は、input.Dataの型をio.ReadSeekerに変更するのが簡単です。ファイルの読み出し位置を変えるには、以下の例のようにSeek()メソッドを用います。

       

解決策2:io.Readerを複製

io.Readerがio.Seekerインターフェースを満たしていない場合はどうでしょうか。この場合は、io.Readerを複製する方法が考えられます。

    

例えば、io.ReadAll()でデータをバイトスライスとして読み込み、バイトスライスから2つのio.Readerを作ることができます。ただし、この方法はio.Readerで読み込むデータのサイズが大きい場合、データ読み込みに時間がかかり処理が遅くなる点に注意してください。

  

実行時間を気にする場合は、io.TeeReader()を使う案が考えられます。io.TeeReader()は、データを読み取りながら、同時にそのデータを別の場所にコピーできます。そのため、io.Readerからデータを読み取りつつ、同じデータをバッファに書き込むことができます。

              

最後に:CSSとJSのコンテンツ判定方法が分からない

本記事で登場したhttp.DetectContentType()は、コンテンツがCSSかJavaScriptかを判定しません。そのため、CloudFrontからindex.htmlを確認した時に、表示が崩れる問題が発生しました。表示崩れの原因は、コンテンツタイプを正しく指定せずにS3へファイルをアップロードしたからです。

 

より細かくコンテンツタイプを判定できるgabriel-vasile/mimetypeを利用しましたが、こちらもCSSとJavaScriptを正しく判定できないようでした。そのため、以下の実装のように拡張子で判定する処理を暫定的に入れてます。

この方法は好ましくないので、上手な判定方法を見つけないとなと思ってます(が、他に優先する事項があるので、放置しています)

おすすめ