Laravel 10でS3互換Oracle Cloud Storageを使う方法

S3互換のクラウドストレージは多くありますが、Laravel 10でOracleのS3互換ストレージを利用する記事が少なく、また参考にした記事の多くはLaravelのバージョンが古かったため、情報共有と備忘録を兼ねてこの記事を書きました

# 前提

Laravelプロジェクトを既に立ち上げているものとします

# Oracle Cloud Infrastructure側の設定

Oracle Cloudは他クラウドに比べると情報が少ないので、クラウド操作部分も少し丁寧に書きます

エンドポイントは`https://{バケット名前空間}.compat.objectstorage.{リージョン}.oraclecloud.com`になるため、まずはそれぞれの情報を取得します

## リージョンの確認

ここから自分の利用リージョンに対応する文字列を探してください

https://docs.oracle.com/ja-jp/iaas/Content/General/Concepts/regions.htm#top

例えば日本リージョンは大阪が`ap-osaka-1`、東京が`ap-tokyo-1`です
**後でこの値は`AWS_DEFAULT_REGION`として利用します**

## バケットの作成、バケット名前空間の確認

左上のハンバーガーメニューから、ストレージ>バケットを選択

<!--
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303575/86de7192-4d1b-ccd5-f91c-6a662cb8573e.png)
-->

コンパートメントを指定して、バケット名をつけて作成

<!--
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303575/605c71cf-cac2-d256-12bf-932e23285d79.png)
-->

作成した**バケットの詳細のネームスペースがバケット名前空間になるので、覚えておきましょう**
**後でこの値は`AWS_ENDPOINT`の一部として利用します**

また、**付けたバケット名は`AWS_BUCKET`として後で利用します**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303575/263066dd-8586-cce9-089b-e94d44ce16ba.png)


## 権限を分けたアカウントを作成(Optional)

**権限のスコープは必要最小にすべきです、必ず設定しましょう**

ただし、テストなので管理者アカウントのままでも構わないという人は、<a href="#顧客認証キーの発行">#顧客認証キーの発行</a> まで読み飛ばしてください
また本筋と関係ないのでそこまで詳しく紹介しません

<details><summary>作成方法</summary>

### グループを作成

アイデンティティとセキュリティ>ドメイン>グループ
仮の名前として`Laravel manager`として作成します

### ユーザを追加

アイデンティティとセキュリティ>ドメイン>ユーザー
作成時、先に作ったグループに所属させます

### ポリシを追加

アイデンティティとセキュリティ>ポリシー

ポリシーでは一行ごとに以下のような構文で権限を割り当てます
`Allow group [group_name] to [verb] [resource-type] in compartment [compartment_name] where [condition]`

バケットの読み取りとオブジェクト作成と取得の権限を`Laravel manager`に与える例
```
Allow group 'Default'/'Laravel manager' to read buckets in tenancy
Allow group 'Default'/'Laravel manager' to manage objects in tenancy where any {request.permission='OBJECT_CREATE', request.permission='OBJECT_INSPECT'}
```
適当に作ったのでテナンシに対して有効になっちゃってます
自分に合ったポリシを作成してください

</details>


## 顧客認証キーの発行

### ログインしているアカウントで発行する場合

右上のアカウント画像から、

ユーザープロファイル>顧客秘密キー

で秘密キーの生成を行います

### 作成した別のアカウントで発行する場合

ハンバーガーメニューから、

アイデンティティとセキュリティ>ドメイン>ユーザ>作成したアカウント>顧客秘密キー

で秘密キーの生成を行います
ドメインは多分Defaultになってるはず

### 顧客秘密キーの生成

#### アクセスキーシークレットの取得

以前はS3互換APIキーと呼ばれていたようです

適当な識別用の名前を付けて生成すると、アクセスキーのシークレットが表示されます
一度閉じると二度と表示されないので、必ずメモしましょう（画像のトークンは無効化済みです）
**この値は`AWS_SECRET_ACCESS_KEY`として利用します**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303575/4a6512a4-7030-cc42-343e-a82981c9ba02.png)

#### アクセスキーの取得

その後顧客秘密キーの一覧に登録されているので、アクセスキーをコピーします
**この値は`AWS_ACCESS_KEY_ID`として利用します**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303575/49f86b6d-7b6b-ccce-2946-2c2adb3b4cf8.png)

# Laravelの設定

## S3パッケージをインストール

```
$ composer require league/flysystem-aws-s3-v3
$ composer install
```


## ファイルの追加

追加・変更するファイル一覧

```
/
|- app
|  |- Http
|  |  \- Controllers
|  |     \- UploadController.php
|  \- Providers
|     \- OciObjectStorageServiceProvider.php
|- config
|  |- app.php
|  \- filesystems.php
|- routes
|  \- web.php
|- resources
|  \- views
|     \- upload.blade.php
\- .env
```

### .env

<details><summary>コード内容</summary>

`FILESYSTEM_DISK`を`local`->`oci`に変更

`AWS_*`には上で設定した情報を追記

```dot:.env
FILESYSTEM_DISK=oci
# ...
AWS_ACCESS_KEY_ID="<key-id>"
AWS_SECRET_ACCESS_KEY="<key-secret>"
AWS_DEFAULT_REGION="<cloud-region>"
AWS_BUCKET="<bucket-name>"
AWS_ENDPOINT="https://<bucket-namespace>.compat.objectstorage.${AWS_BUCKET}.oraclecloud.com"
AWS_USE_PATH_STYLE_ENDPOINT=true
```

</summary>

### app/Providers/OciObjectStorageServiceProvider.php

OCI登録するサービスプロバイダを作成

<details><summary>コード内容</summary>

```php:app/Providers/OciObjectStorageServiceProvider.php
<?php

namespace App\Providers;

use Aws\S3\S3Client;
use Illuminate\Support\ServiceProvider;
use Illuminate\Filesystem\AwsS3V3Adapter;
use Illuminate\Support\Arr;
use Storage;

use League\Flysystem\AwsS3V3\AwsS3V3Adapter as S3Adapter;
use \League\Flysystem\Filesystem as Flysystem;

class OciObjectStorageServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap services.
     */

    public function boot()
        {
            if (config('filesystems.default') != 'oci') {
                return;
            }
            Storage::extend('s3', function($app, $config) {

                // illuminateのAwsS3V3AdapterでOCIのbucket名をパスのprefixに使うための設定
                $config['directory_separator'] = '/';
                $config['prefix'] = $config['prefix'] ?? null;
                if($config['use_path_style_endpoint']) {
                    $config['prefix'] = $config['bucket']. $config['prefix'];
                }
                
                $client = new S3Client([
                    'credentials' => [
                        'key'    => $config['key'],
                        'secret' => $config['secret'],
                    ],
                    'region' => $config['region'],
                    'version' => '2006-03-01',
                    'bucket_endpoint' => true,
                    'use_path_style_endpoint' => true,
                    'endpoint' => $config['endpoint'],
                ]);
                
                // S3クライアント
                $s3_adapter = new S3Adapter($client, $config['bucket'], $config['prefix']);

                // \Illuminate\Filesystem\FilesystemManager.php createflysystemのパクリ
                $flysystem_adapter = new Flysystem($s3_adapter, Arr::only($config, [
                    'directory_visibility',
                    'disable_asserts',
                    'temporary_url',
                    'url',
                    'visibility',
                ]));

                // IlluminateのAwsS3V3Adapter
                $adapter = new AwsS3V3Adapter($flysystem_adapter, $s3_adapter, $config, $client);
                
                return $adapter;
            });
        }
}
```

</details>

### app\Http\Controllers\UploadController.php

コードはこちらのサイトを参考に一部を変更して使用しています

https://www.geekfeed.co.jp/geekblog/laravel_s3

<details><summary>コード内容</summary>

```php:app\Http\Controllers\UploadController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\UploadedFile;
use Storage;

use Illuminate\Support\Facades\Log;
use Illuminate\Http\Request;

class UploadController extends Controller
{
    //
    public function index()
    {
        return view('upload');
    }

    public function store(Request $request){
        // $request->file('file')->store(''); // storage/appに保存
    
        // アップロードされたファイルを変数に格納
        $upload_file = $request->file('file');
 
        // ファイルがアップロードされた場合
        if(!empty($upload_file)) {
 
            // アップロード先S3フォルダ名 
            $dir = 'test';
    
            // バケット内の指定フォルダへアップロード ※putFileはLaravel側でファイル名の一意のIDを自動的に生成してくれます。
            $s3_upload = Storage::disk('s3')->putFile('/'.$dir, $upload_file);
    
            // ※オプション（ファイルダウンロード、削除時に使用するS3でのファイル保存名、アップロード先のパスを取得します。）
            // アップロードファイルurlを取得
            $s3_url = Storage::disk('s3')->url($s3_upload);
            
            // s3_urlからS3でのファイル保存名取得
            $s3_upload_file_name = explode("/", $s3_url)[4];
    
            // アップロード先パスを取得 ※ファイルダウンロード、削除で使用します。
            $s3_path = $dir.'/'.$s3_upload_file_name;

            return $s3_url;
        }

    }
}
```

</details>

### config/app.php

作成したサービスプロバイダを登録

<details><summary>コード内容</summary>

```php:config/app.php
<?php
return [
    # ...
    'providers' => ServiceProvider::defaultProviders()->merge([
        # ...
        App\Providers\OciObjectStorageServiceProvider::class,
        # ...
    ])->toArray(),
    # ...
```

</details>

### config/filesystem.php

ここはデフォルトのままですが、一応

```php:config/filesystem.php
<?php
return [
    # ...
    'disks' => [
        # ...
        's3' => [
            'driver' => 's3',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_DEFAULT_REGION'),
            'bucket' => env('AWS_BUCKET'),
            'url' => env('AWS_URL'),
            'endpoint' => env('AWS_ENDPOINT'),
            'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
            'throw' => false,
        ],
    ],
    # ...
```

</details>

### resources/views/upload.blade.php

アップロードテスト用のページ

<details><summary>コード内容</summary>

```php:resources/views/upload.blade.php
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <form method="POST" action="{{ route('upload') }}" enctype="multipart/form-data">
      {{ csrf_field() }}

      <input type="file" id="file" name="file" class="form-control" />

      <button type="submit">アップロード</button>
    </form>
  </body>
</html>
```

</details>

### routes/web.php

<details><summary>コード内容</summary>

```php:routes/web.php
# 追記
Route::resource('upload', App\Http\Controllers\UploadController::class)->name('index', 'upload');
```

</details>

これで設定は完了です

# テスト

[http://localhost/upload](http://localhost/upload)にアクセスしてテストします
簡易アップロードformがあるので適当なファイルを送信します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303575/d91b75b8-572a-986f-82b3-d819ca2de404.png)
送信に成功すると保存先のURLを表示します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303575/88ac5e27-4aa4-5218-33d2-df9cb93c2479.png)
バケットの詳細からアップロードされたのが確認出来たら成功です

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303575/a6f398fa-04a1-df5b-53ed-6376d8f9f14b.png)

# 参考

https://docs.oracle.com/ja-jp/iaas/Content/Object/Tasks/s3compatibleapi.htm