Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions src/BunnyCDNAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PlatformCommunity\Flysystem\BunnyCDN;

use DateTimeInterface;
use Exception;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Pool;
Expand All @@ -20,25 +21,30 @@
use League\Flysystem\UnableToCreateDirectory;
use League\Flysystem\UnableToDeleteDirectory;
use League\Flysystem\UnableToDeleteFile;
use League\Flysystem\UnableToGenerateTemporaryUrl;
use League\Flysystem\UnableToMoveFile;
use League\Flysystem\UnableToProvideChecksum;
use League\Flysystem\UnableToReadFile;
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\UnableToSetVisibility;
use League\Flysystem\UnableToWriteFile;
use League\Flysystem\UrlGeneration\PublicUrlGenerator;
use League\Flysystem\UrlGeneration\TemporaryUrlGenerator;
use League\Flysystem\Visibility;
use League\MimeTypeDetection\FinfoMimeTypeDetector;
use PlatformCommunity\Flysystem\BunnyCDN\Exceptions\NotFoundException;
use RuntimeException;
use TypeError;

class BunnyCDNAdapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider
class BunnyCDNAdapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider, TemporaryUrlGenerator
{
use CalculateChecksumFromStream;

public function __construct(private BunnyCDNClient $client, private string $pullzone_url = '')
{
public function __construct(
private BunnyCDNClient $client,
private string $pullzone_url = '',
private string $token_auth_key = ''
) {
if (\func_num_args() > 2 && (string) \func_get_arg(2) !== '') {
throw new \RuntimeException('PrefixPath is no longer supported directly. Use PathPrefixedAdapter instead: https://flysystem.thephpleague.com/docs/adapter/path-prefixing/');
}
Expand Down Expand Up @@ -554,6 +560,47 @@ public function publicUrl(string $path, Config $config): string
return rtrim($this->pullzone_url, '/').'/'.ltrim($path, '/');
}

public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string
{
if ($this->token_auth_key === '') {
throw new UnableToGenerateTemporaryUrl('In order to generate temporary URLs for a BunnyCDN object, you must pass the "token_auth_key" parameter to the BunnyCDNAdapter.', $path);
}

// convert our expiration to a unix timestamp
$expiration = $expiresAt->getTimestamp();

// extract elements from our path
$parts = parse_url($path);
$path = $parts['path'];

// extract our query params
parse_str($parts['query'] ?? '', $params);
ksort($params);

// concatenate all of our data
return $path
.(str_contains($path, '?') ? '&' : '?')
.'token='.$this->buildSigningKey($path, $expiration, $params)
.'&expires='.$expiration
.($params ? '&'.http_build_query($params) : null);
}

private function buildSigningKey($path, int $expiration, array $params): string
{
// prefix our path
$path = str_starts_with($path, '/') ? $path : '/'.$path;

// process our query params
$query = implode('&', array_map(fn ($k, $v) => $k.'='.$v, array_keys($params), $params));

// now generate and hash our payload
$payload = $this->token_auth_key.$path.(string) $expiration.$query;
$hash = hash('sha256', $payload, true);

// sanitise and base64 encode it
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($hash));
}

private static function parse_bunny_timestamp(string $timestamp): int
{
return (date_create_from_format('Y-m-d\TH:i:s.u', $timestamp) ?: date_create_from_format('Y-m-d\TH:i:s', $timestamp))->getTimestamp();
Expand Down
17 changes: 16 additions & 1 deletion tests/FlysystemAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use League\Flysystem\FilesystemException;
use League\Flysystem\UnableToCopyFile;
use League\Flysystem\UnableToDeleteFile;
use League\Flysystem\UnableToGenerateTemporaryUrl;
use League\Flysystem\UnableToMoveFile;
use League\Flysystem\UnableToProvideChecksum;
use League\Flysystem\UnableToRetrieveMetadata;
Expand Down Expand Up @@ -104,7 +105,21 @@ public function setting_visibility(): void

public function generating_a_temporary_url(): void
{
$this->markTestSkipped('No temporary URL support is provided for BunnyCDN');
$adapter = new BunnyCDNAdapter(self::bunnyCDNClient(), '', 'test-key');

$expiresAt = new \DateTimeImmutable('+1 hour');
$url = $adapter->temporaryUrl('path.txt', $expiresAt, new Config());

$this->assertStringContainsString('path.txt?token=', $url);
$this->assertStringContainsString('&expires=', $url);
}

public function test_temporary_url_throws_exception_if_not_configured(): void
{
$this->expectException(UnableToGenerateTemporaryUrl::class);
$this->expectExceptionMessage('In order to generate temporary URLs for a BunnyCDN object, you must pass the "token_auth_key" parameter to the BunnyCDNAdapter.');

$this->adapter()->temporaryUrl('path.txt', new \DateTimeImmutable('+1 hour'), new Config());
}

/**
Expand Down
9 changes: 8 additions & 1 deletion tests/PrefixTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ public function setting_visibility(): void

public function generating_a_temporary_url(): void
{
$this->markTestSkipped('No temporary URL support is provided for BunnyCDN');
$adapter = new BunnyCDNAdapter(self::bunnyCDNClient(), '', 'test-key');
$prefixAdapter = new PathPrefixedAdapter($adapter, self::PREFIX_PATH);

$expiresAt = new \DateTimeImmutable('+1 hour');
$url = $prefixAdapter->temporaryUrl('path.txt', $expiresAt, new Config());

$this->assertStringContainsString(self::PREFIX_PATH.'/path.txt?token=', $url);
$this->assertStringContainsString('&expires=', $url);
}

/**
Expand Down