Using Minio on Laravel + Vapor Locally

Published in Laravel NovaLaravel on Nov 19, 2020

Testing Laravel Vapor application with file uploads locally can be tricky. It is possible to setup S3 bucket to receive file from localhost by adding required CORS configuration but now you have the alternative of using Minio.

Minio S3 Compatibility

Prequisite

  • Setup laravel/vapor-core using Composer.
  • Setup laravel-vapor using NPM to allow file uploading.

Setup Minio

You can setup Minio locally using Docker or Homestead, however for this demonstration I will be using Takeout. Remember that you need to update config/filesystems.php to include minio disk configuration:

        'minio' => [
            'driver' => 's3',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_DEFAULT_REGION'),
            'bucket' => env('AWS_BUCKET'),
            'endpoint' => env('AWS_ENDPOINT'),
            'url' => env('AWS_URL'),
            'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', true),
        ],

Also don't forget to update .env:

FILESYSTEM_DRIVER=minio
FILESYSTEM_CLOUD=minio

AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=minioadmin
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=local
AWS_ENDPOINT="http://127.0.0.1:9000"
AWS_USE_PATH_STYLE_ENDPOINT=(true)

Setup Laravel

Next we need to setup Laravel to authorize file uploads. You can refer to Storage > Authorization documentation. For testing you can just do the following on AppServiceProvider:

if ($this->app->isLocal()) {
    Gate::define('uploadFiles', fn ($user) => true);
}

Finally you need to map SignedStorageUrlController with the following files:

use App\Http\Controllers\SignedStorageUrlController;
use Laravel\Vapor\Contracts\SignedStorageUrlController as SignedStorageUrlControllerContract;

if ($this->app->isLocal()) {
    $this->app->singleton(
        SignedStorageUrlControllerContract::class, 
        SignedStorageUrlController::class
    );
}

And the following Gist should be published to app/Http/Controller/SignedStorageUrlController.php:

<?php
namespace App\Http\Controllers;
use Aws\S3\S3Client;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Laravel\Vapor\Contracts\SignedStorageUrlController as SignedStorageUrlControllerContract;
class SignedStorageUrlController extends \Illuminate\Routing\Controller implements SignedStorageUrlControllerContract
{
/**
* Create a new signed URL.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
Gate::authorize('uploadFiles', [
$request->user(),
$bucket = $request->input('bucket') ?: config('filesystems.disks.minio.bucket'),
]);
$client = $this->storageClient();
$uuid = (string) Str::uuid();
$signedRequest = $client->createPresignedRequest(
$this->createCommand($request, $client, $bucket, $key = ('tmp/'.$uuid)),
'+10 minutes'
);
return response()->json([
'uuid' => $uuid,
'bucket' => $bucket,
'key' => $key,
'url' => (string) $signedRequest->getUri(),
'headers' => $this->headers($request, $signedRequest),
], 201);
}
/**
* Create a command for the PUT operation.
*
* @param \Illuminate\Http\Request $request
* @param \Aws\S3\S3Client $client
* @param string $bucket
* @param string $key
* @return \Aws\Command
*/
protected function createCommand(Request $request, S3Client $client, $bucket, $key)
{
return $client->getCommand('putObject', array_filter([
'Bucket' => $bucket,
'Key' => $key,
]));
}
/**
* Get the headers that should be used when making the signed request.
*
* @param \Illuminate\Http\Request $request
* @param \GuzzleHttp\Psr7\Request
* @return array
*/
protected function headers(Request $request, $signedRequest)
{
return array_merge(
$signedRequest->getHeaders(),
[
'Content-Type' => $request->input('content_type') ?: 'application/octet-stream',
]
);
}
/**
* Get the S3 storage client instance.
*
* @return \Aws\S3\S3Client
*/
protected function storageClient()
{
$config = [
'region' => config('filesystems.disks.minio.region'),
'version' => 'latest',
'use_path_style_endpoint' => true,
'url' => config('filesystems.disks.minio.endpoint'),
'endpoint' => config('filesystems.disks.minio.endpoint'),
'credentials' => [
'key' => config('filesystems.disks.minio.key'),
'secret' => config('filesystems.disks.minio.secret'),
],
];
return S3Client::factory($config);
}
}