Skip to content

Commit

Permalink
[FEATURE] added temporary URLs.
Browse files Browse the repository at this point in the history
  • Loading branch information
frankdejonge committed Oct 20, 2022
1 parent 5d4e7d5 commit 13a3fef
Show file tree
Hide file tree
Showing 20 changed files with 214 additions and 20 deletions.
25 changes: 25 additions & 0 deletions src/AdapterTestUtilities/FilesystemAdapterTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace League\Flysystem\AdapterTestUtilities;

use DateInterval;
use DateTime;
use DateTimeImmutable;
use Generator;
use League\Flysystem\ChecksumProvider;
use League\Flysystem\Config;
Expand All @@ -17,12 +20,14 @@
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\UnableToSetVisibility;
use League\Flysystem\UrlGeneration\PublicUrlGenerator;
use League\Flysystem\UrlGeneration\TemporaryUrlGenerator;
use League\Flysystem\Visibility;
use PHPUnit\Framework\TestCase;
use Throwable;
use function file_get_contents;
use function is_resource;
use function iterator_to_array;
use function var_dump;
use const PHP_EOL;

/**
Expand Down Expand Up @@ -793,6 +798,26 @@ public function generating_a_public_url(): void
self::assertEquals('public contents', $contents);
}

/**
* @test
*/
public function generating_a_temporary_url(): void
{
$adapter = $this->adapter();

if ( ! $adapter instanceof TemporaryUrlGenerator) {
$this->markTestSkipped('Adapter does not supply temporary URls');
}

$adapter->write('some/private.txt', 'public contents', new Config(['visibility' => 'private']));

$expiresAt = (new DateTimeImmutable())->add(DateInterval::createFromDateString('1 minute'));
$url = $adapter->temporaryUrl('some/private.txt', $expiresAt, new Config());
$contents = file_get_contents($url);

self::assertEquals('public contents', $contents);
}

/**
* @test
*/
Expand Down
12 changes: 11 additions & 1 deletion src/AsyncAwsS3/AsyncAwsS3Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use AsyncAws\S3\ValueObject\CommonPrefix;
use AsyncAws\S3\ValueObject\ObjectIdentifier;
use AsyncAws\SimpleS3\SimpleS3Client;
use DateTimeImmutable;
use DateTimeInterface;
use Generator;
use League\Flysystem\ChecksumAlgoIsNotSupported;
use League\Flysystem\ChecksumProvider;
Expand All @@ -32,13 +34,14 @@
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\UnableToSetVisibility;
use League\Flysystem\UrlGeneration\PublicUrlGenerator;
use League\Flysystem\UrlGeneration\TemporaryUrlGenerator;
use League\Flysystem\Visibility;
use League\MimeTypeDetection\FinfoMimeTypeDetector;
use League\MimeTypeDetection\MimeTypeDetector;
use Throwable;
use function trim;

class AsyncAwsS3Adapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider
class AsyncAwsS3Adapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider, TemporaryUrlGenerator
{
/**
* @var string[]
Expand Down Expand Up @@ -530,4 +533,11 @@ public function checksum(string $path, Config $config): string

return trim($metadata['ETag'], '"');
}

public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string
{
$location = $this->prefixer->prefixPath($path);

return $this->client->getPresignedUrl($this->bucket, $location, DateTimeImmutable::createFromInterface($expiresAt));
}
}
5 changes: 3 additions & 2 deletions src/AsyncAwsS3/AsyncAwsS3AdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
use League\Flysystem\UnableToCheckFileExistence;
use League\Flysystem\UnableToDeleteFile;
use League\Flysystem\UnableToMoveFile;
use League\Flysystem\UnableToProvideChecksum;
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\Visibility;

Expand Down Expand Up @@ -98,11 +97,13 @@ private static function s3Client(): S3Client
self::markTestSkipped('No AWS credentials present for testing.');
}

return static::$s3Client = new SimpleS3Client([
static::$s3Client = new SimpleS3Client([
'accessKeyId' => $key,
'accessKeySecret' => $secret,
'region' => $region,
]);

return static::$s3Client;
}


Expand Down
6 changes: 6 additions & 0 deletions src/AsyncAwsS3/S3ClientStub.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use AsyncAws\S3\Result\PutObjectOutput;
use AsyncAws\S3\S3Client;
use AsyncAws\SimpleS3\SimpleS3Client;
use DateTimeImmutable;
use Symfony\Component\HttpClient\MockHttpClient;

/**
Expand Down Expand Up @@ -179,4 +180,9 @@ public function getUrl(string $bucket, string $key): string
{
return $this->actualClient->getUrl($bucket, $key);
}

public function getPresignedUrl(string $bucket, string $key, ?DateTimeImmutable $expires = null): string
{
return $this->actualClient->getPresignedUrl($bucket, $key, $expires);
}
}
2 changes: 1 addition & 1 deletion src/AsyncAwsS3/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"require": {
"php": "^8.0.2",
"league/flysystem": "^3.8.0",
"league/flysystem": "^3.10.0",
"league/mime-type-detection": "^1.0.0",
"async-aws/s3": "^1.5"
},
Expand Down
18 changes: 17 additions & 1 deletion src/AwsS3V3/AwsS3V3Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Aws\Api\DateTimeResult;
use Aws\S3\S3ClientInterface;
use DateTimeInterface;
use Generator;
use League\Flysystem\ChecksumAlgoIsNotSupported;
use League\Flysystem\ChecksumProvider;
Expand All @@ -29,14 +30,15 @@
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 League\MimeTypeDetection\MimeTypeDetector;
use Psr\Http\Message\StreamInterface;
use Throwable;
use function trim;

class AwsS3V3Adapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider
class AwsS3V3Adapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider, TemporaryUrlGenerator
{
/**
* @var string[]
Expand Down Expand Up @@ -537,4 +539,18 @@ public function checksum(string $path, Config $config): string

return trim($metadata['ETag'], '"');
}

public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string
{
$options = $config->get('get_object_options', []);
$command = $this->client->getCommand('GetObject', [
'Bucket' => $this->bucket,
'Key' => $this->prefixer->prefixPath($path),
] + $options);

$presignedRequestOptions = $config->get('presigned_request_options', []);
$request = $this->client->createPresignedRequest($command, $expiresAt, $presignedRequestOptions);

return (string) $request->getUri();
}
}
2 changes: 1 addition & 1 deletion src/AwsS3V3/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"require": {
"php": "^8.0.2",
"league/flysystem": "^3.8.0",
"league/flysystem": "^3.10.0",
"league/mime-type-detection": "^1.0.0",
"aws/aws-sdk-php": "^3.132.4"
},
Expand Down
48 changes: 42 additions & 6 deletions src/AzureBlobStorage/AzureBlobStorageAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace League\Flysystem\AzureBlobStorage;

use DateTime;
use DateTimeInterface;
use League\Flysystem\ChecksumAlgoIsNotSupported;
use League\Flysystem\ChecksumProvider;
use League\Flysystem\Config;
Expand All @@ -16,27 +18,32 @@
use League\Flysystem\UnableToCopyFile;
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\MimeTypeDetection\FinfoMimeTypeDetector;
use League\MimeTypeDetection\MimeTypeDetector;
use MicrosoftAzure\Storage\Blob\BlobRestProxy;
use MicrosoftAzure\Storage\Blob\BlobSharedAccessSignatureHelper;
use MicrosoftAzure\Storage\Blob\Models\BlobProperties;
use MicrosoftAzure\Storage\Blob\Models\CreateBlockBlobOptions;
use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions;
use MicrosoftAzure\Storage\Common\Exceptions\ServiceException;
use MicrosoftAzure\Storage\Common\Internal\Resources;
use MicrosoftAzure\Storage\Common\Internal\StorageServiceSettings;
use MicrosoftAzure\Storage\Common\Models\ContinuationToken;
use Throwable;
use function base64_decode;
use function bin2hex;
use function stream_get_contents;

class AzureBlobStorageAdapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider
class AzureBlobStorageAdapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider, TemporaryUrlGenerator
{
/** @var string[] */
private const META_OPTIONS = [
Expand All @@ -50,16 +57,12 @@ class AzureBlobStorageAdapter implements FilesystemAdapter, PublicUrlGenerator,
const ON_VISIBILITY_IGNORE = 'ignore';

private BlobRestProxy $client;

private MimeTypeDetector $mimeTypeDetector;

private int $maxResultsForContentsListing;

private string $container;

private PathPrefixer $prefixer;

private string $visibilityHandling;
private ?StorageServiceSettings $serviceSettings;

public function __construct(
BlobRestProxy $client,
Expand All @@ -68,13 +71,15 @@ public function __construct(
MimeTypeDetector $mimeTypeDetector = null,
int $maxResultsForContentsListing = 5000,
string $visibilityHandling = self::ON_VISIBILITY_THROW_ERROR,
StorageServiceSettings $serviceSettings = null,
) {
$this->client = $client;
$this->container = $container;
$this->prefixer = new PathPrefixer($prefix);
$this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector();
$this->maxResultsForContentsListing = $maxResultsForContentsListing;
$this->visibilityHandling = $visibilityHandling;
$this->serviceSettings = $serviceSettings;
}

public function copy(string $source, string $destination, Config $config): void
Expand Down Expand Up @@ -373,4 +378,35 @@ public function checksum(string $path, Config $config): string

return bin2hex(base64_decode($checksum));
}

public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string
{
if ( ! $this->serviceSettings instanceof StorageServiceSettings) {
throw UnableToGenerateTemporaryUrl::noGeneratorConfigured(
$path,
'The $serviceSettings constructor parameter must be set to generate temporary URLs.',
);
}
$sas = new BlobSharedAccessSignatureHelper($this->serviceSettings->getName(), $this->serviceSettings->getKey());
$baseUrl = $this->publicUrl($path, $config);
$resourceName = $this->container . '/' . ltrim($this->prefixer->prefixPath($path), '/');
$token = $sas->generateBlobServiceSharedAccessSignatureToken(
Resources::RESOURCE_TYPE_BLOB,
$resourceName,
'r', // read
DateTime::createFromInterface($expiresAt),
$config->get('signed_start', ''),
$config->get('signed_ip', ''),
$config->get('signed_protocol', 'https'),
$config->get('signed_identifier', ''),
$config->get('cache_control', ''),
$config->get('content_deposition', ''),
$config->get('content_encoding', ''),
$config->get('content_language', ''),
$config->get('content_type', ''),
);

return "$baseUrl?$token";

}
}
10 changes: 9 additions & 1 deletion src/AzureBlobStorage/AzureBlobStorageAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use League\Flysystem\UnableToSetVisibility;
use League\Flysystem\Visibility;
use MicrosoftAzure\Storage\Blob\BlobRestProxy;
use MicrosoftAzure\Storage\Common\Internal\StorageServiceSettings;
use function getenv;

/**
Expand All @@ -22,14 +23,21 @@ class AzureBlobStorageAdapterTest extends TestCase
protected static function createFilesystemAdapter(): FilesystemAdapter
{
$dsn = getenv('FLYSYSTEM_AZURE_DSN');
$dsn = 'DefaultEndpointsProtocol=https;AccountName=notpublicflysystemtests;AccountKey=B+Dm9IKyFvzr5TztsHcJqhF0jMpm2gR1v8OEsS0kgfftq6gnqgxcVJxF8peagofUwC/mOGbAgBSQ+AStnIXXPg==;EndpointSuffix=core.windows.net';

if (empty($dsn)) {
self::markTestSkipped('FLYSYSTEM_AZURE_DSN is not provided.');
}

$client = BlobRestProxy::createBlobService($dsn);
$serviceSettings = StorageServiceSettings::createFromConnectionString($dsn);

return new AzureBlobStorageAdapter($client, self::CONTAINER_NAME, 'ci');
return new AzureBlobStorageAdapter(
$client,
self::CONTAINER_NAME,
'ci',
serviceSettings: $serviceSettings,
);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/AzureBlobStorage/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"require": {
"php": "^8.0.2",
"league/flysystem": "^3.8.0",
"league/flysystem": "^3.10.0",
"microsoft/azure-storage-blob": "^1.1"
},
"license": "MIT",
Expand Down
14 changes: 14 additions & 0 deletions src/Filesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

namespace League\Flysystem;

use DateTimeInterface;
use Generator;
use League\Flysystem\UrlGeneration\ShardedPrefixPublicUrlGenerator;
use League\Flysystem\UrlGeneration\PrefixPublicUrlGenerator;
use League\Flysystem\UrlGeneration\PublicUrlGenerator;
use League\Flysystem\UrlGeneration\TemporaryUrlGenerator;
use Throwable;

use function is_array;
Expand All @@ -20,6 +22,7 @@ class Filesystem implements FilesystemOperator
private Config $config;
private PathNormalizer $pathNormalizer;
private ?PublicUrlGenerator $publicUrlGenerator;
private ?TemporaryUrlGenerator $temporaryUrlGenerator;

public function __construct(
FilesystemAdapter $adapter,
Expand Down Expand Up @@ -170,6 +173,17 @@ public function publicUrl(string $path, array $config = []): string
return $this->publicUrlGenerator->publicUrl($path, $config);
}

public function temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []): string
{
$generator = $this->temporaryUrlGenerator ?: $this->adapter;

if ($generator instanceof TemporaryUrlGenerator) {
return $this->temporaryUrlGenerator->temporaryUrl($path, $expiresAt, $this->config->extend($config));
}

throw UnableToGenerateTemporaryUrl::noGeneratorConfigured($path);
}

public function checksum(string $path, array $config = []): string
{
$config = $this->config->extend($config);
Expand Down
3 changes: 3 additions & 0 deletions src/FilesystemReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

namespace League\Flysystem;

use DateTimeInterface;

/**
* This interface contains everything to read from and inspect
* a filesystem. All methods containing are non-destructive.
*
* @method string publicUrl(string $path, array $config = []) Will be added in 4.0
* @method string temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []) Will be added in 4.0
* @method string checksum(string $path, array $config = []) Will be added in 4.0
*/
interface FilesystemReader
Expand Down
Loading

0 comments on commit 13a3fef

Please sign in to comment.