Introduction
By default, all objects in an S3 bucket are private, meaning only the bucket account owner initially has access to the objects. However, the object owner can optionally share objects with others by creating a presigned URL, using their own security credentials, to grant time-limited permission to download the objects. This is particularly useful because it means that you don’t have to provide bucket access to other users and restrict their access with IAM policies.
Laravel offers a nice and easy way to generate temporary urls to AWS S3 files using the Storage
facade and the temporaryUrl
method.
The Storage
Facade points to an instance of the Illuminate\Filesystem\FilesystemManager
class. Whenever you get a disk using Storage::disk('s3')
, it returns an instance of Illuminate\Filesystem\FilesystemAdapter
. This is a generic class that implements the Adapter pattern and lets you work with different storage classes using a unified interface.
In Laravel, the S3 Driver offers a temporaryUrl
method for this purpose. The signature accepts a path and a DateTime
instance specifying when the URL should expire.
Temporary Upload Urls
As we said, AWS also provides the means to upload files to an S3 bucket using a presigned URL. The expiration time defines the time when the upload has to be started, after which access is denied. If the action consists of multiple steps, such as a multipart upload, all steps must be started before the expiration.
Unfortunately Laravel does not implement natively a method to generate such presigned URL, however it’s not hard to implement such method ourselves. The FilesystemAdapter
class is Macroable, which means that it can be extended at runtime.
To get an inspiration let’s check the code inside the temporaryUrl
method:
<?php
/**
* Get a temporary URL for the file at the given path.
*
* @param string $path
* @param \DateTimeInterface $expiration
* @param array $options
* @return string
*
* @throws \RuntimeException
*/
public function temporaryUrl($path, $expiration, array $options = [])
{
$adapter = $this->driver->getAdapter();
if ($adapter instanceof CachedAdapter) {
$adapter = $adapter->getAdapter();
}
if (method_exists($adapter, 'getTemporaryUrl')) {
return $adapter->getTemporaryUrl($path, $expiration, $options);
}
if ($this->temporaryUrlCallback) {
return $this->temporaryUrlCallback->bindTo($this, static::class)(
$path, $expiration, $options
);
}
if ($adapter instanceof AwsS3Adapter) {
return $this->getAwsTemporaryUrl($adapter, $path, $expiration, $options);
}
throw new RuntimeException('This driver does not support creating temporary URLs.');
}
As you can see, if the adapter is an instance of the AwsS3Adapter
, the getAwsTemporaryUrl
method is invoked otherwise a RuntimeException
is thrown if the adapter does not support creating temporary URLs.
The getAwsTemporaryUrl
is the following:
<?php
/**
* Get a temporary URL for the file at the given path.
*
* @param \League\Flysystem\AwsS3v3\AwsS3Adapter $adapter
* @param string $path
* @param \DateTimeInterface $expiration
* @param array $options
* @return string
*/
public function getAwsTemporaryUrl($adapter, $path, $expiration, $options)
{
$client = $adapter->getClient();
$command = $client->getCommand('GetObject', array_merge([
'Bucket' => $adapter->getBucket(),
'Key' => $adapter->getPathPrefix().$path,
], $options));
$uri = $client->createPresignedRequest(
$command, $expiration
)->getUri();
// If an explicit base URL has been set on the disk configuration then we will use
// it as the base URL instead of the default path. This allows the developer to
// have full control over the base path for this filesystem's generated URLs.
if (! is_null($url = $this->driver->getConfig()->get('temporary_url'))) {
$uri = $this->replaceBaseUrl($uri, $url);
}
return (string) $uri;
}
Nothing special here. The AWS client is used to create the presigned request using the GetObject
command. The logic to generate a presigned upload URL is very similar, we just have to replace the GetObject
command with PutObject
. Here’s the full implementation of the macro, which should be define inside a Service Provider:
<?php
FilesystemAdapter::macro('temporaryUploadUrl', function ($path, $expiration, array $options = [])
{
$adapter = $this->driver->getAdapter();
if ($adapter instanceof AwsS3Adapter) {
$client = $adapter->getClient();
$command = $client->getCommand('PutObject', array_merge([
'Bucket' => $adapter->getBucket(),
'Key' => $adapter->getPathPrefix().$path
], $options));
$uri = $client->createPresignedRequest(
$command, $expiration
)->getUri();
// If an explicit base URL has been set on the disk configuration then we will use
// it as the base URL instead of the default path. This allows the developer to
// have full control over the base path for this filesystem's generated URLs.
/** @phpstan-ignore-next-line */
if (! is_null($url = $this->driver->getConfig()->get('temporary_url'))) {
/** @phpstan-ignore-next-line */
$uri = $this->replaceBaseUrl($uri, $url);
}
return (string) $uri;
}
throw new RuntimeException('This driver does not support creating temporary upload URLs.');
});
That’s it!