[AWS SDK for PHP]署名付きPOSTを使い、直接クライアント側からAWS S3にファイルアップロード(PostObjectV4インスタンスを使用)

以下の流れで実装しました。

- [PostObjectV4](https://docs.aws.amazon.com/ja_jp/sdk-for-php/v3/developer-guide/s3-presigned-post.html) のインスタンスを使い、署名付きPOSTに必要な情報を発行
- Vue.jsで、アプリケーションサーバを介さず、クライアント側から直接S3にファイルをアップロード


画面はこんな感じで、簡単にアップロード画面を作ってます。
![スクリーンショット 2019-11-18 23.51.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/247004/5ff21d2a-b6bf-915c-2e07-0c8b3aef33d7.png)


## 準備
### AWS
S3でバケットを作成してください。
※IAMの設定は省略します

### S3 CORSの設定
CORSを設定しておかなければなりません。

```
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
```

### AWS-SDK-PHPのインストール
```
$ composer require aws/aws-sdk-php
```

## 実装
署名付きPOSTに必要なデータを発行するPHPの処理と、ファイルアップロードするVue.jsの処理を記載します。
実装した処理は以下の通りです。

- 署名付きPOSTに必要なデータ形式のjsonを返すAPI
- Vue.jsから↑のAPIを叩き、レスポンスを取得
- レスポンスを利用し、ファイルアップロード


### API(PHP)

```php

<?php
    // laravel apiのcontrollerの処理を一部抜粋
    public function getPresignedUrl(Request $request)
    {
        $s3Client = new \Aws\S3\S3Client([
            'credentials' => [
                'key' => env('AWS_ACCESS_KEY_ID'),
                'secret' => env('AWS_SECRET_ACCESS_KEY'),
            ],
            'region' => env('AWS_DEFAULT_REGION'),
            'version' => 'latest'
        ]);

        $bucket = env('AWS_BUCKET');
        $requestData = $request->all();
        $formInputs = [
            'acl' => 'public-read',
            'key' => 'hoge/' . $requestData['filename'] . '.' . $requestData['fileext'],
        ];
        $options = [
            ['acl' => 'public-read'],
            ['bucket' => $bucket],
            ['starts-with', '$key', 'hoge/'],
        ];
        $expires = '+20 minutes';
        $postObject = new \Aws\S3\PostObjectV4(
            $s3Client,
            $bucket,
            $formInputs,
            $options,
            $expires
        );

        $formAttributes = $postObject->getFormAttributes();
        $formInputs = $postObject->getFormInputs();

        return response()
            ->json([
                'url' => $formAttributes['action'],
                'fields' => $formInputs
            ]);
    }
```


### Client側(Vue.js)
```vuejs
<script>
    export default {
        name: 'AwsS3Upload',
        methods: {
            async upload() { 
                const upload_files = document.getElementById('upload-file');
                const upload_file = upload_files.files[0];
                // 署名付きPOSTのAPI叩く
                let preSignedUrl = await this.getPresignedUrl();
                // S3へアップロード
                let uploadS3Path = await this.uploadS3(preSignedUrl, upload_file);
            },
            async getPresignedUrl() {
                // ↓ここのファイル名は仮置きで適当になってますｗ
                let filename = 'fuga';
                let filetype = 'image/jpeg'
                let fileext = 'jpg'
                try {
                    const url = '/api/get-presigned-url?filename=' +  filename + '&filetype=' + filetype + '&fileext=' + fileext;
                    let response = await axios.get(url);
                    console.log('S3署名付きURL取得 成功');

                    return response;
                } catch (error) {
                    console.log('S3 署名付きURL取得 失敗');
                }
            },
            async uploadS3(presignedUrl, up_file) {
                let data = presignedUrl.data;
                try {
                    var formdata = new FormData();
                    for (let key in data.fields) {
                        formdata.append(key, data.fields[key]);
                    }
                    formdata.append("file", up_file);
                    const headers = {
                        "content-type": "multipart/form-data",
                    }
                    console.log('S3 アップロード 開始');
                    let response = await axios.post(
                        data.url,
                        formdata,
                        {
                            headers: headers,
                        }
                    );
                    console.log('S3 アップロード 成功');
                    return data.url + '/' + data.fields.key;
                } catch (error) {
                    console.log('S3 アップロード エラー');
                }
            },
        }
    }
</script>

```

## 結論
署名付きPOSTで、S3へ直接クライアントサイドからファイルアップロードができました。
また、アプリケーションサーバを介さずにファイルをアップロードできるため、ファイルサイズが大きい場合においてもサーバに負荷をかけずに済みます。 (20191129追記)

## 参考
[【AWS S3】S3 Presigned URLの仕組みを調べてみた](https://qiita.com/tmiki/items/87697d3d3d5330c6fc08)
[CORS(Cross-Origin Resource Sharing)について整理してみた](https://dev.classmethod.jp/etc/about-cors/)
[PresignedPost.php](https://github.com/awsdocs/aws-doc-sdk-examples/blob/master/php/example_code/s3/PresignedPost.php)
[ブラウザからS3へのダイレクトアップロード](https://cloudpack.media/47912)