アニメ映画の巨匠のドキュメンタリーを繰り返し閲覧してしまい、メンタルだけでもクリエイターとして 同じ心構えを持ちたいと思う今日この頃...

Hackerの皆様いかがお過ごしでしょうか?

本記事執筆の経緯

現在私が関わっているLaravelプロジェクト で、ユーザーがWebサイトから投稿したZIPなどの大容量ファイルを大量保存させたいというニーズが生じました。

Webサーバのディスク利用料はとても高い=低価格クラウドストレージにおきたい

ファイル容量がとても重いファイルをそのまま大量にサーバにおいてしまっては、AWSやGCPなどのクラウドサービスのディスク容量がとても嵩んでしまいます。

例えばAWS EC2で使用するEBS(ディスクストレージ)の汎用 SSDの使用量は1GBあたり 0.096ドル(2023/12現在)。 1ドル140円と仮定すると 13.44円。100GB使用した場合1344円/月 もかかってしまいます。

ましてや、CGM(ユーザーが作成したコンテンツ)でユーザーが自由に100MB以上のファイルをあげられる。という案件においては、これがいかに直接設計要件に関わってくることは理解できるでしょう。

そこで、AWS S3のような低価格、かつ容量上限を気にせず保存できるストレージの必要性が増します。

EBSはかなり高額。一時ストレージとして使うのが望ましい

EBSはかなり高額。一時ストレージとして使うのが望ましい

S3にアップロードするシステムは本記事では割愛し、S3にファイルが設置してある状況から、いかにサーバに一時的にファイルを保存せず 直接ユーザーのブラウザ経由でダウンロードさせるかというのを本記事では考えてみます。


また、こうすることで貴重なサーバリソースであるメモリを大量に消費することを避けられ、安定稼働につながると考えました。

本記事のゴール

Amazon S3から、巨大なファイル(100MB~)をユーザーに直接ダウンロードさせたい。

作業

早速作業を始めていきます。

前提

今回は

を前提に作業を進めてまいりますので、まだの方は私が以前に書いた類似する作業の ポストをよろしければご覧ください。

また、サンプルとして、test.zipという100MB以上のファイルを、作成したs3バケットの/data/test.zipに入れておきます。

※特に、下記の記事にLaravel + S3の初期設定については書きましたので、本記事では詳しい説明は省略させていただきます。

S3からサーバにDLさせる方法はこちら

参考記事:

モルドスプーンアイコン

こちらのポストは内容が似通ってはいますが、ユーザーのブラウザを経由して、ユーザーのPCにダウンロードさせる、という点で異なります。

Laravel Sailの初期設定についてはこちら

参考記事:

モルドスプーンアイコン

必要なライブラリのインストール

以下のコマンドで必要なライブラリthephpleague/flysystem-aws-s3-v3を予めcomposerを経由して入れておきます。

composer league/flysystem-aws-s3-v3

envファイルにAWSのキーを書いておく

.envの以下の箇所を実際のものに差し替えておきます。

AWS_ACCESS_KEY_ID=XXXXXXX
AWS_SECRET_ACCESS_KEY=XXXXX
AWS_DEFAULT_REGION=ap-northeast-1
AWS_BUCKET=BucketName
AWS_USE_PATH_STYLE_ENDPOINT=false

configファイルを用意

config/aws.phpを作っておき、

<?php

return [
  'bucket' => env('AWS_BUCKET', ''),
  'credentials' => [
    'key'    => env('AWS_ACCESS_KEY_ID', ''),
    'secret' => env('AWS_SECRET_ACCESS_KEY', ''),
  ],
  'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
  'version' => 'latest',
  // You can override settings for specific services
  'Ses' => [
    'region' => 'us-east-1',
  ],
];

このように書いて保存します。

php artisan config:cache

でキャッシュするのを忘れず。

Controllerの作成

今回の実装にあたり、FileDownloadController.phpを作成しました。

app/Http/Controllers/FileDownloadController.php
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller as Controller;
use Illuminate\Http\Request;
use AWS;
use App\Models\TmpDownload;

class FileDownloadController extends Controller
{
  public function index(Request $request)
  {

    // Validation
    $request->validate([
      'backup_name' => 'required|string'
    ]);

    $s3 = AWS::createClient('s3');

    $s3->registerStreamWrapper();
    $zipName = "test.zip"; // 前述通り、100MB以上のテスト用zipファイル

    $key = "s3://" . config('aws.bucket') . "/data/" . $zipName; // 上記で設定したバケット名が使えるはず

    $size = filesize($key);
    header('Content-Type: application/octet-json');
    header('Content-Length: ' . $size);
    header('Content-Disposition: attachment; filename="' . rawurlencode($zipName) . '"');
    readfile($key);
    exit;
  }
}

ポイントとしては

ことです。

この方法であれば、Laravelに限らず他のPHPプロジェクトでもダウンロードさせることは可能 です。

反面afterFilterなどのフレームワークの事後処理を通らないので、この方法が本当に良いかは疑問 ではあります。とはいえ、今回はファイルを実際にダウンロードさせさえすればよいことから、妥協したいと思います...。

FileStreamを使う方法はうまくいかなかった

FileStreamを使う方法を実行している例もGoogle検索するといくつかありましたが、Laravel10の公式ドキュメントでは記述を見つけられず。また結果として私の環境ではうまくいきませんでした。

もしうまくいかれた方おられましたらご教示ください...mm

なお、LaravelのFileStreamは、中身は\Symfony\Component\HttpFoundation\StreamedResponseで元々Symfonyで開発されたライブラリのようですね。先人の開発遺産に感謝です🥲

ルートを設定しておく

ダウンロードの用のルートを予め設定しておきます。

use App\Http\Controllers\FileDownloadController;

// 省略 ...

Route::get('/file/download', [FileDownloadController::class, 'index']);

実際にリクエストしてみる

今回の例ではブラウザから直接エンドポイントを叩いてみます。

https://domainToProject.com/file/download

を叩くと、ダウンロードが始まり実際にファイルがダウンロードできるはずです。お疲れ様でした。

まとめ

afterFilterを使えないことで後にどういうマイナス面が出てくるのかちょっと理解できていないことも ありますが実現できることがまず大事..と思い、ご紹介したいと思いました。

この記事が何かのお役に立てれば幸いです。
最後までお読みいただきありがとうございました!