Heads up: this article is over a year old. Some information might be out of date, as I don't always update older articles.

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!

comments powered by Disqus