Laravel で S3 にファイルを上げるタイミングで公開権限を設定する

### はじめに

Laravel のファイルストレージでは AWS の S３ を指定して保存することができます。
S3を指定する方法は公式ドキュメントを参照してもらうとして、保存は下記のように `store` メソッドを利用します。

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class UserAvatarController extends Controller
{
    public function update(Request $request)
    {
        $path = $request->file('avatar')->store('avatars');

        return $path;
    }
}
```

### 重要なポイント

保存をするとき、上記の例では Storage ファサードを使っていません。
もちろんファサードを使って保存することも可能で、

```php
$path = Storage::putFile('avatars', $request->file('avatar'));
```

このように公式ドキュメントには書いてあります。
で、やりたいのはドキュメントに `ファイル視認性` と書かれていますが、

```php
$visibility = Storage::getVisibility('file.jpg');
Storage::setVisibility('file.jpg', 'public')
```

ファイルが既に保存されている場合、上記の設定でズバッと設定をしてしまいたい。デフォルトで private になっているところを公開の public にしたい場合はどうしたら良いのか、です。
つまり `store` の段階で最初からできてくれよってことですね。

### で、例の如く...

ないんですよね、ドキュメント上に正解が。

ヒントはあって、第二引数で他のディスク（この例だとローカルストレージを利用中）に保存したい場合などは下記のように書かれています。


```php
$path = $request->file('avatar')->store(
    'avatars/'.$request->user()->id, 's3'
);
```

となると、もしかすると第３や第４の引数でなにかあるかもしれない。
ということでコアを追いかけることになります。

この場合の `$request` は `Illuminate\Http\Request`です。
追跡していくと、

```
Illuminate\Http\Request
　↓
Illuminate\Http\Concerns\InteractsWithInput
　↓
Illuminate\Http\UploadedFile
```

と、辿れます。どうやって辿るかは正直、勘です。
エスパー並の感度で遡った結果、

store メソッドを見つけました。

```php:Illuminate\Http\UploadedFile
    public function store($path, $options = [])
    {
        return $this->storeAs($path, $this->hashName(), $this->parseOptions($options));
    }

    public function storeAs($path, $name, $options = [])
    {
        $options = $this->parseOptions($options);

        $disk = Arr::pull($options, 'disk');

        return Container::getInstance()->make(FilesystemFactory::class)->disk($disk)->putFileAs(
            $path, $this, $name, $options
        );
    }

    protected function parseOptions($options)
    {
        if (is_string($options)) {
            $options = ['disk' => $options];
        }

        return $options;
    }
```

`parseOptions` を見ると、文字列を与えると自動的に配列で `disk` の key/val 型になるように整形して `storeAs` に渡しているのがみて取れますね。

で、最終的には `putFileAs` で保存しているのが分かるわけですが、`FilesystemFactory` でいよいよ Factory の文字が見えてまいりました。

これの実体は `Illuminate\Contracts\Filesystem\Factory` です。
こいつは interface なので、基底にもつクラスがいるはずで、それは何かというと `Illuminate\Filesystem\FilesystemAdapter` になります。
このなかに `putFileAs` のメソッドも登場してくるわけですが、

```php:Illuminate\Filesystem\FilesystemAdapter
    public function putFileAs($path, $file, $name, $options = [])
    {
        $stream = fopen($file->getRealPath(), 'r');

        $result = $this->put(
            $path = trim($path.'/'.$name, '/'), $stream, $options
        );

        if (is_resource($stream)) {
            fclose($stream);
        }

        return $result ? $path : false;
    }

    public function put($path, $contents, $options = [])
    {
        $options = is_string($options)
                     ? ['visibility' => $options]
                     : (array) $options;

        if ($contents instanceof File ||
            $contents instanceof UploadedFile) {
            return $this->putFile($path, $contents, $options);
        }

        return is_resource($contents)
                ? $this->driver->putStream($path, $contents, $options)
                : $this->driver->put($path, $contents, $options);
    }
```
putFileAs は put への橋渡しをしていますね。
で、put のところで `visibility` が登場しており、この辺りがかなりそれっぽい。
で、最終的に `$this->driver` で保存してそうだということが return からわかります。

ここで使われている drive は当然ながら s3 のはずなので、driver を探します。
このファイルの先頭付近で

```php
use League\Flysystem\AwsS3v3\AwsS3Adapter;
```
と書かれているので、この辺りがとても怪しい。
本体は Laravel のコアから外れて `/vendor/league` の下までいくとようやく出てきます。
このクラスを眺めていると、ようやく AWS SDK のオプションが出てきました。

この辺は [AWS SDK for PHP のドキュメント](https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html#putobject)を見ると、ファイルアップロード関数の putObject と一致しているのがわかります。


```php:League\Flysystem\AwsS3v3\AwsS3Adapter

    protected static $metaOptions = [
        'ACL',
        'CacheControl',
        'ContentDisposition',
        'ContentEncoding',
        'ContentLength',
        'ContentType',
        'Expires',
        'GrantFullControl',
        'GrantRead',
        'GrantReadACP',
        'GrantWriteACP',
        'Metadata',
        'RequestPayer',
        'SSECustomerAlgorithm',
        'SSECustomerKey',
        'SSECustomerKeyMD5',
        'SSEKMSKeyId',
        'ServerSideEncryption',
        'StorageClass',
        'Tagging',
        'WebsiteRedirectLocation',
    ];
```

この場合、ACL が今回の対象になるポイントで、Laravelがファイル視認性とか言ってますが、最終的に渡したいのは
`private|public-read|public-read-write|authenticated-read|aws-exec-read|bucket-owner-read|bucket-owner-full-control` のどれかをこのアダプターの ACL というキーに突っ込みたいわけです。
というわけで、それっぽい関数をみてみると、

```php:League\Flysystem\AwsS3v3\AwsS3Adapter

    protected function upload($path, $body, Config $config)
    {
        $key = $this->applyPathPrefix($path);
        $options = $this->getOptionsFromConfig($config);
        $acl = array_key_exists('ACL', $options) ? $options['ACL'] : 'private';

        if (!$this->isOnlyDir($path)) {
            if ( ! isset($options['ContentType'])) {
                $options['ContentType'] = Util::guessMimeType($path, $body);
            }

            if ( ! isset($options['ContentLength'])) {
                $options['ContentLength'] = is_resource($body) ? Util::getStreamSize($body) : Util::contentSize($body);
            }

            if ($options['ContentLength'] === null) {
                unset($options['ContentLength']);
            }
        }

        try {
            $this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
        } catch (S3MultipartUploadException $multipartUploadException) {
            return false;
        }

        return $this->normalizeResponse($options, $path);
    }
```

upload のメソッドに `$acl = array_key_exists('ACL', $options) ? $options['ACL'] : 'private';`が出てきますね。
ACLという配列キーが出てこなければ private にせよ、と書いてあるので逆に ACL のキーで渡せば良いことがわかります。

### というわけで解決方法

長くなりましたが $options に配列で渡していけば良いことがわかりました。

```php
$path = $request->file('avatar')->store('avatars', ['disk' => 's3', 'ACL' => 'public-read']);
```

ということで、このように第二引数に配列で渡しましょう。その際に disk のキーを渡すのもお忘れなく。