LaravelとS3(AWS)とSVG

0
60

少し前にLaravelで拡張子が「.jpeg」のJPEGファイルをアップロードした際に挙動がおかしかった件について投稿しましたが、今度はSVGに関してです。
しかも今回はS3(AWS)も絡みます。

現象

SVGをアップロード&参照する仕組みにおいて発生した問題です。
バックエンドのシステムはLaravelで構築し、StorageはS3バケットを使用しています。
同環境で、SVGのアップロードは問題なくできたのですが、参照時に画面上に表示されずにダウンロードになってしまうという問題が発生しました。

ダウンロードされたファイルは間違いなくアップロードしたSVGで、同ファイルのS3への格納および参照自体は正しく行えているようです。

原因

直接的な原因は当該ファイル参照時のレスポンスヘッダにおけるContent-Typeが「image/svg」になっていたことでした。
ここは正しくは「image/svg+xml」であるべきです。
惜しい!と言ったところでしょうか…

実は本システムではセキュリティ上の事情からS3バケット上のファイルにはブラウザから直接参照できるようにはしておらず、Laravel(PHP)経由でアクセスするようにしてあります。
具体的には「routes/api.php」内で以下のような処理を行なっています。

Route::get('/storage/{path}', function ($path) {
    $response = Response::make(Storage::disk('private')->get('/storage/'.$path), 200);
    $response->withHeaders([
        'Content-Type' => Storage::disk('private')->mimeType('/storage/'.$path)
    ]); 
    return $response;
})->where('path', '.*');

実際にはもう少し複雑ですがポイントのみ抜き出すと上記のような処理になっています。
「/storage/」で始まるパスが指定された場合、当該アクションをディスク「private」に対応するS3バケット上の「/storage/<指定されたパス>」で示されたファイルの参照とみなし、同ファイルを返すレスポンスを生成しています。

重要なのは、問題となっているContent-Typeの指定に「Storage::mimeType」メソッドを使用している点です。
つまり、同メソッドの実行で返された文字列が「image/svg+xml」ではなく「image/svg」となってしまっているということになります。

ここで若干話は変わりますが、ネット上で同様の問題に関する記事を探すと、Content-Typeが「image/svg」ではなく「binary/octet-stream」になってしまって、それで単なる参照ではなくダウンロードになってしまうという記事がいくつか見つかります。
その解決方法としてはS3に当該ファイルを格納する際にContent-Typeを正しく指定しましょう、というものです。

改めてS3バケット内の当該SVGの情報を見ると、確かに「メタデータ」としてキー「Content-Type」、値「image/svg」が設定されています。つまり、S3ではファイルと合わせてContent-Type情報がしっかり管理されているようですが、その情報が間違っており、それがそのままレスポンスヘッダに反映されていたことが今回の問題の直接原因でした。

しかし、実のところStorageへのファイル格納に際してContent-Typeを意識したことなど今までなかったのですが?
本システムでファイル格納に関する処理は以下のようになっています。

$request->file(<対象ファイル>)->store('/storage/'.$tmpFilePath,'private');
...
Storage::disk('private')->put('/storage/'.$savePath, Storage::disk('private')->get('/storage/'.$tmpFilePath));

こちらもかなり端折っていますが、要はアップロードされたファイルを一旦一時ファイルとしてディスク「private」上の別の場所に格納しておき、後に本来のパスに格納し直しているだけです。
この間にContent-Typeは全く関与してきません。
そもそもLaravelのStorage処理としてはS3を対象とすることはむしろ特殊ケースで、通常のローカルディスクを想定する方がデフォルトかと思います。Storage処理の汎用性を考えればContent-Typeが関与しなくて良いインタフェースになっている方が納得できます。

なお、上記処理で対象としているのはSVGだけではなく他の一般的な画像形式(JPEG、PNG、GIF)も含んでおり、後者では問題は発生していません。つまりは、Content-Typeを明示的に指定しなくても同処理内でブラックボックス的によしなに設定してくれるようになっているのだと思いますし、実際にSVGに関しても「image/svg」と惜しいところまでは設定できているので、ここが正しく「image/svg+xml」となるように対処できれば良い訳ですが…残念ながらLaravelのソース内もある程度追ってみました、私の読解力では上記カラクリを解明するには至りませんでした。

対処

途中でも書いたように、Content-Typeが「binary/octet-stream」になってしまって困ったという記事はいくつか見つけられ、その際の対処方針としても明示的にContent-Typeを指定するというものでしたので、それらの中から本システムに適用できそうな処理を見つけてきました。
具体的には以下のような内容になります。

Storage::disk('private')->put('/storage/'.$savePath, Storage::disk('private')->get('/storage/'.$tmpFilePath), ['mimetype'=> 'image/svg+xml']);

putメソッドでは第三引数としてオプションを配列形式で指定できるようです。
対象ファイルがSVGの場合は上記オプションにキー「mimetype」、値「image/svg+xml」を明示的に指定することで、S3の「Content-Type」情報に「image/svg+xml」が設定されるようになり、参照時のmimeTypeメソッドでも正しい値が取得できるようになります。

総括

理想的にはLaravelのStorage機能とS3の連携においてSVG格納時にContent-Typeとして「image/svg+xml」が自動的に設定されるような対処ができれば良かったのですが、残念ながらそのような方法は発見できず、結果として前述のように明示的に指定する方法を採りました。
途中でも書きましたが、Storage関連の処理はファイルシステムの実体に依存しない書き方ができるべきで、ここにContent-Typeの指定を関与させることはスマートではないと思うのですが。

また、ネット情報ではContent-Typeを明確に指定しなかった場合「binary/octet-stream」になってしまったという話ばかりでimage/svg」になったという話は今のところ見つかっていません。この点も謎です。
繰り返しになりますが惜しいところまではできているので、Content-Typeを自動的に設定するという仕掛けが全く機能していない訳ではないと言う中途半端感にも疑問が残ります。
LaravelのStorage機能とS3の連携部分も進化していて、当該ファイル形式がSVGである点は判別できるようになったものの、Content-Typeとして設定する文字列が今一歩であった、という話であれば今後改善されて自動的に解決するという期待もなくはないですが…どんなもんでしょう。

いずれにしても、まずは対症療法的に前述のような対応にしておいて、より良い方法が発見できたら改めて対処したいと思います。