はじめに
Laravel のファイルストレージでは AWS の S3 を指定して保存することができます。
S3を指定する方法は公式ドキュメントを参照してもらうとして、保存は下記のように store
メソッドを利用します。
<?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 ファサードを使っていません。
もちろんファサードを使って保存することも可能で、
$path = Storage::putFile('avatars', $request->file('avatar'));
このように公式ドキュメントには書いてあります。
で、やりたいのはドキュメントに ファイル視認性
と書かれていますが、
$visibility = Storage::getVisibility('file.jpg');
Storage::setVisibility('file.jpg', 'public')
ファイルが既に保存されている場合、上記の設定でズバッと設定をしてしまいたい。デフォルトで private になっているところを公開の public にしたい場合はどうしたら良いのか、です。
つまり store
の段階で最初からできてくれよってことですね。
で、例の如く...
ないんですよね、ドキュメント上に正解が。
ヒントはあって、第二引数で他のディスク(この例だとローカルストレージを利用中)に保存したい場合などは下記のように書かれています。
$path = $request->file('avatar')->store(
'avatars/'.$request->user()->id, 's3'
);
となると、もしかすると第3や第4の引数でなにかあるかもしれない。
ということでコアを追いかけることになります。
この場合の $request
は Illuminate\Http\Request
です。
追跡していくと、
Illuminate\Http\Request
↓
Illuminate\Http\Concerns\InteractsWithInput
↓
Illuminate\Http\UploadedFile
と、辿れます。どうやって辿るかは正直、勘です。
エスパー並の感度で遡った結果、
store メソッドを見つけました。
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
のメソッドも登場してくるわけですが、
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 を探します。
このファイルの先頭付近で
use League\Flysystem\AwsS3v3\AwsS3Adapter;
と書かれているので、この辺りがとても怪しい。
本体は Laravel のコアから外れて /vendor/league
の下までいくとようやく出てきます。
このクラスを眺めていると、ようやく AWS SDK のオプションが出てきました。
この辺は AWS SDK for PHP のドキュメントを見ると、ファイルアップロード関数の putObject と一致しているのがわかります。
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 というキーに突っ込みたいわけです。
というわけで、それっぽい関数をみてみると、
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 に配列で渡していけば良いことがわかりました。
$path = $request->file('avatar')->store('avatars', ['disk' => 's3', 'ACL' => 'public-read']);
ということで、このように第二引数に配列で渡しましょう。その際に disk のキーを渡すのもお忘れなく。