LaravelでStorageでS3(minio)へアクセスする

## 概要
LaravelのStorageを使ってAWS S3(minIO)へアクセスすることがありました。
その際に調べた内容をメモしておく。

:::note info
お知らせ
minIOのバージョンがアップされています。
この記事のminIOは１つ前のものです。
※ 最新のminIOの場合、異なる部分があると思うのでご注意ください
:::
**<参考サイト>**
※ [MinIOを使ってローカルで外部ストレージをAmazonS3から置き換える](https://www.ritolab.com/entry/232)

## ファイル構成
下記ローカルS3環境を構築して使っております。

```
├ docker
│ └ minio
│   ├ config               ... minio設定 (git管理対象外)
│   ├ data                 ... minioデータ (git管理対象外) 
│   ├ export
│   │ └ static             ... 起動時に取り込むファイル
│   │   ├ hoge
│   │   │  └ sample.txt
│   │   └ fuga
│   └ policies
│     └ buckets
│        └ static
│           └ policy.json  ... staticバケットのポリシー定義
│
├ src                      ... Laravelのソースコード
│ ├ app
│ ├ config
│ ├ routes
│   ...
│
│.gitignore
├ docker-compose.yml
└ REAMDE.md
```
※ローカルS3環境(minio)を構築については、[こちら](https://qiita.com/reflet/items/3e0f07bc9d64314515c1)を参照ください

## Laravelの設定について
下記の設定ファイルがあります。

```php:./src/config/filesystems.php
's3' => [
    'driver'   => 's3',
    'key'      => env('AWS_ACCESS_KEY_ID'),
    'secret'   => env('AWS_SECRET_ACCESS_KEY'),
    'region'   => env('AWS_DEFAULT_REGION', 'ap-northeast-1'),
    'bucket'   => env('AWS_BUCKET'),
    'url'      => env('AWS_URL'),
    'endpoint' => env('AWS_ENDPOINT'),
    'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
],
```
ローカル環境で、minIOを使うため、envファイルで値を調整する。

```bash:./src/.env
# [AWS設定]
AWS_ACCESS_KEY_ID=minio
AWS_SECRET_ACCESS_KEY=minio123
AWS_DEFAULT_REGION=ap-northeast-1
AWS_BUCKET=static
AWS_USE_PATH_STYLE_ENDPOINT=true
AWS_ENDPOINT=http://minio:9000
```

## StorageのS3ドライバーについて
公式サイトを参考に、下記ライブラリをインスールする。
※ 参考サイト: [Laravel 8.x ファイルストレージ](https://readouble.com/laravel/8.x/ja/filesystem.html)

```bash
$ ./sail composer require --with-all-dependencies league/flysystem-aws-s3-v3 "^1.0"
```

## フォルダ一覧を取得する
フォルダ一覧を返すAPIを作ってみる。

```bash
$ ./sail artisan make:controller FindDirectoryController --invokable
```

ソースコードを書いてみる。

```php:./src/app/Http/Controllers/FindDirectoryController.php
<?php

namespace APP\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\Controller;

class FindDirectoryController extends Controller
{
    /**
     * @param  Request $request
     * @return JsonResponse
     */
    public function __invoke(Request $request): JsonResponse
    {
        $path = '/static';
        $directories = Storage::disk('s3')->directories($path);
        return response()->json([
            'path'      => $path,
            'directories' => $directories,
        ]);
    }
}
```
Routes設定を追加します。

```php:./src/routes/api.php
Route::namespace('App\Http\Controllers')->group(function () {
    Route::group(['middleware' => ['api']], function () {
        Route::get('/directories', 'FindDirectoryController')->name('api.directories'); // ← 追加!!
    });
});
```
リクエストしてみると以下のレスポンスが返ってきました。

```json:GET　/api/directories
{
  "path":"/static",
  "directories":[
    "static/hoge",
    "static/fuga"
  ]
}
```

## ファイル一覧を取得する
ファイル一覧を返すAPIを作ってみる。

```bash
$ ./sail artisan make:controller FindFileController --invokable
```
ソースコードを書いてみる。

```php:./src/app/Http/Controllers/FindFileController.php
<?php

namespace APP\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\Controller;

class FindFileController extends Controller
{
    /**
     * @param  Request $request
     * @return JsonResponse
     */
    public function __invoke(Request $request): JsonResponse
    {
        $path = '/static/hoge';
        $files = Storage::disk('s3')->files($path);
        return response()->json([
            'path'  => $path,
            'files' => $files,
        ]);
    }
}
```
Routes設定を追加します。

```php:./src/routes/api.php
Route::namespace('App\Http\Controllers')->group(function () {
    Route::group(['middleware' => ['api']], function () {
        ...
        Route::get('/files', 'FindFileController')->name('api.files'); // ← 追加!!
    });
});
```

リクエストしてみると以下のレスポンスが返ってきました。

```json:GET　/api/files
{
  "path":"/static/hoge",
  "files":[
    "static/hoge/.gitkeep",
    "static/hoge/sample.txt"
  ]
}
```

## ファイルをダウンロードする
ファイルをダウンロードするAPIを作ってみる。

```bash
$ ./sail artisan make:controller DownloadFileController --invokable
```
ソースコードを書いてみる。

```php:./src/app/Http/Controllers/DownloadFileController.php
<?php

namespace APP\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Support\Facades\Storage;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use App\Http\Controllers\Controller;

class DownloadFileController extends Controller
{
    /**
     * @param  Request $request
     * @return Response|ResponseFactory
     * @throws FileNotFoundException
     */
    public function __invoke(Request $request)
    {
        $path = '/static/hoge/sample.txt';
        $name = 'sample.txt';

        // 存在確認
        if (empty($path) || ! Storage::disk('s3')->exists($path)) {
            throw new FileNotFoundException('file is not exists.');
        }

        $file = Storage::disk('s3')->get($path);
        $last_modified = Storage::disk('s3')->lastModified($path);

        return response($file, 200, [
            'filename'            => $name,
            'Content-Type'        => 'text/plain',
            'Last-Modified'       => gmdate('D, d M Y H:i:s', $last_modified) . ' GMT',
            'Etag'                => md5($last_modified),
            'Content-Disposition' => 'attachment; filename="' . $name . '"',
        ]);
    }
}
```
Routes設定を追加します。

```php:./src/routes/api.php
Route::namespace('App\Http\Controllers')->group(function () {
    Route::group(['middleware' => ['api']], function () {
        ...
        Route::get('/files/sample.txt', 'DownloadFileController')->name('api.files.download'); // ← 追加!!
    });
});
```
リクエストしてみるとファイルがダウンロードされました。

## ファイルをアップロードしてみる
POST送信で送られてきたファイルをS3へアップロードするAPIを追加してみる。

```bash
$ ./sail artisan make:controller UploadFileController --invokable
```
ソースコードを書いてみる。

```php:./src/app/Http/Controllers/UploadFileController.php
<?php

namespace APP\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\Controller;

class UploadFileController extends Controller
{
    /**
     * @param  Request $request
     * @return JsonResponse
     */
    public function __invoke(Request $request): JsonResponse
    {
        $file = $request->file('file');

        // ファイルをリネームしてアップロードする
        $path = '/static/fuga'
        $name = 'sample.txt';
        Storage::disk('s3')->putFileAs($path, $file, $name);

        return response()->json([
            'path'  => $path . '/' . $name,
            'file'  => $file->getClientOriginalName(),
        ]);
    }
}
```
Routes設定を追加します。

```php:./src/routes/api.php
Route::namespace('App\Http\Controllers')->group(function () {
    Route::group(['middleware' => ['api']], function () {
        ...
        Route::post('/files/sample.txt', 'UploadFileController')->name('api.files.upload'); // ← 追加!!
    });
});
```
リクエストしてみると以下のレスポンスが返ってきました。

```json:POST　/api/files/sample.txt
{
  "path":"/static/fuga/sample.txt",
  "file":"xxxxxxxx.txt"
}
```

## 参考サイト
* [Unable to download file from s3 using return response()->download($file);](https://stackoverflow.com/questions/46762997/unable-to-download-file-from-s3-using-return-response-downloadfile/46763205)
* [Laravel5でAmazon S3からファイルをダウンロードする方法](https://qiita.com/sakuraya/items/7b404d131353916aaad9)
* [Help with minio and Laravel 6](https://github.com/laradock/laradock/issues/2383)
* [Laravelのファイル保存先をS3に変更してみた](https://dev.classmethod.jp/articles/laravel-s3-try/)
* [超簡単！LaravelでS3を利用する手順](https://qiita.com/tiwu_dev/items/ecb115a92ebfebf6a92f)
* [Laravel で amazon S3 のファイル操作を行うメソッド一覧](https://pgmemo.tokyo/data/archives/1759.html)

以上