Skip to content

Commit

Permalink
Added support for temporary signed urls on S3
Browse files Browse the repository at this point in the history
  • Loading branch information
frasmage committed Oct 10, 2020
1 parent a07627f commit 9217176
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 6 deletions.
43 changes: 38 additions & 5 deletions docs/source/media.rst
Expand Up @@ -3,10 +3,10 @@ Using Media

.. highlight:: php

Media Paths & URLs
Media Paths
---------------------

``Media`` records keep track of the location of their file and are able to generate a number of paths and URLs relative to the file. Consider the following example, given a ``Media`` instance with the following attributes:
``Media`` records keep track of the location of their file and are able to generate a number of paths relative to the file. Consider the following example, given a ``Media`` instance with the following attributes:


::
Expand All @@ -27,9 +27,6 @@ The following attributes and methods would be exposed:
$media->getAbsolutePath();
// /var/www/site/public/uploads/foo/bar/picture.jpg

$media->getUrl();
// http://localhost/uploads/foo/bar/picture.jpg

$media->getDiskPath();
// foo/bar/picture.jpg

Expand All @@ -45,8 +42,44 @@ The following attributes and methods would be exposed:
$media->extension;
// jpg

URLs and Downloads
---------------------

URLs can be generated for Media stored on a public disk and set to public visibility.

::

$media->getUrl();
// http://localhost/uploads/foo/bar/picture.jpg

`$media->getUrl()` will throw an exception if the file or its disk has its visibility set to private. You can check if it is safe to generate a url for a record with the `$media->isPubliclyAccessible()` method.

For private files stored on an Amazon S3 disk, it is possible to generate a temporary signed URL to allow authorized users the ability to download the file for a specified period of time.

::

<?php
$media->getTemporaryUrl(Carbon::now->addMinutes(5));

For private files, it is possible to expose them to authorized users by streaming the file from the server.

::

<?php
return response()->streamDownload(
function() use ($media) {
$stream = $media->stream();
while($bytes = $stream->read(1024)) {
echo $bytes;
}
},
$media->basename,
[
'Content-Type' => $media->mime_type,
'Content-Length' => $media->size
]
);

Querying Media
---------------------

Expand Down
5 changes: 5 additions & 0 deletions src/Exceptions/MediaUrlException.php
Expand Up @@ -19,4 +19,9 @@ public static function invalidGenerator(string $class): self
{
return new static("Could not set UrlGenerator, class `{$class}` does not extend `Plank\Mediable\UrlGenerators\UrlGenerator`");
}

public static function temporaryUrlsNotSupported(string $disk): self
{
return new static("Temporary URLs are not supported for files on disk '{$disk}'");
}
}
11 changes: 11 additions & 0 deletions src/Media.php
Expand Up @@ -16,6 +16,7 @@
use Plank\Mediable\Exceptions\MediaMoveException;
use Plank\Mediable\Exceptions\MediaUrlException;
use Plank\Mediable\Helpers\File;
use Plank\Mediable\UrlGenerators\TemporaryUrlGeneratorInterface;
use Plank\Mediable\UrlGenerators\UrlGeneratorInterface;
use Psr\Http\Message\StreamInterface;

Expand Down Expand Up @@ -342,6 +343,16 @@ public function getUrl(): string
return $this->getUrlGenerator()->getUrl();
}

public function getTemporaryUrl(\DateTimeInterface $expiry): string
{
$generator = $this->getUrlGenerator();
if ($generator instanceof TemporaryUrlGeneratorInterface) {
return $generator->getTemporaryUrl($expiry);
}

throw MediaUrlException::temporaryUrlsNotSupported($this->disk);
}

/**
* Check if the file exists on disk.
* @return bool
Expand Down
19 changes: 18 additions & 1 deletion src/UrlGenerators/S3UrlGenerator.php
Expand Up @@ -7,7 +7,7 @@
use Illuminate\Contracts\Filesystem\Cloud;
use Illuminate\Filesystem\FilesystemManager;

class S3UrlGenerator extends BaseUrlGenerator
class S3UrlGenerator extends BaseUrlGenerator implements TemporaryUrlGeneratorInterface
{
/**
* Filesystem Manager.
Expand Down Expand Up @@ -43,4 +43,21 @@ public function getUrl(): string
$filesystem = $this->filesystem->disk($this->media->disk);
return $filesystem->url($this->media->getDiskPath());
}

public function getTemporaryUrl(\DateTimeInterface $expiry): string
{
$adapter = $this->filesystem->disk($this->media->disk)->getDriver()->getAdapter();
$command = $adapter->getClient()->getCommand(
'GetObject',
[
'Bucket' => $adapter->getBucket(),
'Key' => $this->media->getDiskPath(),
]
);

return (string)$adapter->getClient()->createPresignedRequest(
$command,
$expiry
)->getUri();
}
}
8 changes: 8 additions & 0 deletions src/UrlGenerators/TemporaryUrlGeneratorInterface.php
@@ -0,0 +1,8 @@
<?php

namespace Plank\Mediable\UrlGenerators;

interface TemporaryUrlGeneratorInterface extends UrlGeneratorInterface
{
public function getTemporaryUrl(\DateTimeInterface $expiry): string;
}
41 changes: 41 additions & 0 deletions tests/Integration/MediaTest.php
Expand Up @@ -2,15 +2,20 @@

namespace Plank\Mediable\Tests\Integration;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\DB;
use Plank\Mediable\Exceptions\MediaMoveException;
use Plank\Mediable\Exceptions\MediaUrlException;
use Plank\Mediable\Media;
use Plank\Mediable\Tests\Mocks\MediaSoftDelete;
use Plank\Mediable\Tests\Mocks\SampleMediable;
use Plank\Mediable\Tests\Mocks\SampleMediableSoftDelete;
use Plank\Mediable\Tests\TestCase;
use Plank\Mediable\UrlGenerators\TemporaryUrlGeneratorInterface;
use Plank\Mediable\UrlGenerators\UrlGeneratorFactory;
use Plank\Mediable\UrlGenerators\UrlGeneratorInterface;

class MediaTest extends TestCase
{
Expand Down Expand Up @@ -761,4 +766,40 @@ public function test_it_can_find_other_variants()
$this->assertEquals($all, $media2->getAllVariantsAndSelf());
$this->assertEquals($all, $media3->getAllVariantsAndSelf());
}

public function test_it_generates_temporary_urls()
{
$media = $this->makeMedia();
$expiry = Carbon::now();

$generator = $this->createMock(
TemporaryUrlGeneratorInterface::class
);
$generator->expects($this->once())
->method('getTemporaryUrl')
->with($expiry)
->willReturn($url = 'https://example.com/path');
$factory = $this->createMock(UrlGeneratorFactory::class);
$factory->expects($this->once())
->method('create')
->willReturn($generator);
app()->instance('mediable.url.factory', $factory);

$this->assertEquals($url, $media->getTemporaryUrl($expiry));
}

public function test_it_throws_for_unsupported_temporary_urls()
{
$this->expectException(MediaUrlException::class);
$media = $this->makeMedia();

$generator = $this->createMock(UrlGeneratorInterface::class);
$factory = $this->createMock(UrlGeneratorFactory::class);
$factory->expects($this->once())
->method('create')
->willReturn($generator);
app()->instance('mediable.url.factory', $factory);

$media->getTemporaryUrl(Carbon::now());
}
}
35 changes: 35 additions & 0 deletions tests/Integration/UrlGenerators/S3UrlGeneratorTest.php
Expand Up @@ -2,11 +2,14 @@

namespace Plank\Mediable\Tests\Integration\UrlGenerators;

use Carbon\Carbon;
use Illuminate\Filesystem\FilesystemManager;
use Plank\Mediable\Media;
use Plank\Mediable\Tests\TestCase;
use Plank\Mediable\UrlGenerators\S3UrlGenerator;

use function GuzzleHttp\Psr7\parse_query;

class S3UrlGeneratorTest extends TestCase
{
public function setUp(): void
Expand Down Expand Up @@ -52,6 +55,38 @@ public function test_it_generates_url()
);
}

public function test_it_generates_temporary_url()
{
$generator = $this->setupGenerator();
$url = $generator->getTemporaryUrl(Carbon::now()->addDay());
[$uri, $queryString] = explode('?', $url);
$this->assertEquals(
sprintf(
'https://%s.s3.%s.amazonaws.com/foo/bar.jpg',
env('S3_BUCKET'),
env('S3_REGION')
),
$uri
);
parse_str($queryString, $queryParams);
$this->assertArrayHasKey(
'X-Amz-Credential',
$queryParams
);
$this->assertArrayHasKey(
'X-Amz-Expires',
$queryParams
);
$this->assertArrayHasKey(
'X-Amz-Algorithm',
$queryParams
);
$this->assertArrayHasKey(
'X-Amz-Signature',
$queryParams
);
}

protected function setupGenerator()
{
$media = $this->getMedia();
Expand Down

0 comments on commit 9217176

Please sign in to comment.