From e3c05fe48bb8be9baf04026b500ed7828e36de4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Wed, 10 Sep 2025 16:20:32 +0200 Subject: [PATCH 1/6] Make classes final and add `Override` attribute where needed --- CHANGELOG.md | 6 ++++++ src/Config.php | 2 +- src/OpenStackSwiftAdapter.php | 19 ++++++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 127e963..aa7734f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v0.5.0 (unreleased) + +### 💥 Breaking changes + +* Make classes final + ## v0.4.0 (February 8, 2025) ### 💥 Breaking changes diff --git a/src/Config.php b/src/Config.php index 382cdd2..9d94411 100644 --- a/src/Config.php +++ b/src/Config.php @@ -6,7 +6,7 @@ use League\Flysystem\Config as BaseConfig; -class Config extends BaseConfig +final class Config extends BaseConfig { public const OPTION_SEGMENT_SIZE = 'segment_size'; public const OPTION_SEGMENT_CONTAINER = 'segment_container'; diff --git a/src/OpenStackSwiftAdapter.php b/src/OpenStackSwiftAdapter.php index 702564c..45580be 100644 --- a/src/OpenStackSwiftAdapter.php +++ b/src/OpenStackSwiftAdapter.php @@ -26,7 +26,7 @@ use OpenStack\ObjectStore\v1\Models\StorageObject; use OpenStack\OpenStack; -class OpenStackSwiftAdapter implements FilesystemAdapter +final class OpenStackSwiftAdapter implements FilesystemAdapter { private ?Container $container = null; @@ -48,6 +48,7 @@ private function getContainer(): Container return $this->container; } + #[\Override] public function fileExists(string $path): bool { try { @@ -57,6 +58,7 @@ public function fileExists(string $path): bool } } + #[\Override] public function directoryExists(string $path): bool { try { @@ -69,6 +71,7 @@ public function directoryExists(string $path): bool } } + #[\Override] public function write(string $path, string $contents, BaseConfig $config): void { $data = [ @@ -83,6 +86,7 @@ public function write(string $path, string $contents, BaseConfig $config): void } } + #[\Override] public function writeStream(string $path, $contents, BaseConfig $config): void { $data = [ @@ -111,6 +115,7 @@ public function writeStream(string $path, $contents, BaseConfig $config): void } } + #[\Override] public function read(string $path): string { $object = $this->getContainer()->getObject($path); @@ -128,6 +133,7 @@ public function read(string $path): string } } + #[\Override] public function readStream(string $path) { $object = $this->getContainer()->getObject($path); @@ -149,6 +155,7 @@ public function readStream(string $path) } } + #[\Override] public function delete(string $path): void { $object = $this->getContainer()->getObject($path); @@ -164,6 +171,7 @@ public function delete(string $path): void } } + #[\Override] public function deleteDirectory(string $path): void { // Make sure a slash is added to the end. @@ -189,21 +197,25 @@ public function deleteDirectory(string $path): void } } + #[\Override] public function createDirectory(string $path, BaseConfig $config): void { // TODO add option in constructor to enable creating empty files to simulate directories } + #[\Override] public function setVisibility(string $path, string $visibility): void { throw UnableToSetVisibility::atLocation($path, 'OpenStack Swift does not support per-file visibility.'); } + #[\Override] public function visibility(string $path): FileAttributes { throw UnableToRetrieveMetadata::visibility($path, 'OpenStack Swift does not support per-file visibility.'); } + #[\Override] public function mimeType(string $path): FileAttributes { $object = $this->getContainer()->getObject($path); @@ -223,6 +235,7 @@ public function mimeType(string $path): FileAttributes return $fileAttributes; } + #[\Override] public function lastModified(string $path): FileAttributes { $object = $this->getContainer()->getObject($path); @@ -242,6 +255,7 @@ public function lastModified(string $path): FileAttributes return $fileAttributes; } + #[\Override] public function fileSize(string $path): FileAttributes { $object = $this->getContainer()->getObject($path); @@ -261,6 +275,7 @@ public function fileSize(string $path): FileAttributes return $fileAttributes; } + #[\Override] public function listContents(string $path, bool $deep): iterable { $path = trim($path, '/'); @@ -298,6 +313,7 @@ public function listContents(string $path, bool $deep): iterable } } + #[\Override] public function move(string $source, string $destination, BaseConfig $config): void { if ($source === $destination) { @@ -312,6 +328,7 @@ public function move(string $source, string $destination, BaseConfig $config): v } } + #[\Override] public function copy(string $source, string $destination, BaseConfig $config): void { $object = $this->getContainer()->getObject($source); From 1e898b319840e704e691fcb646d219af7981a808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Thu, 11 Sep 2025 10:58:31 +0200 Subject: [PATCH 2/6] Implements `TemporaryUrlGenerator` --- CHANGELOG.md | 14 ++++++--- src/Config.php | 6 ++++ src/OpenStackSwiftAdapter.php | 46 +++++++++++++++++++++++++-- tests/OpenStackSwiftAdapterTest.php | 49 ++++++++++++++++++++++++++++- 4 files changed, 107 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa7734f..c87db0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,19 @@ * Make classes final +### ✨ New features + +* Add support for temporary URLs + ## v0.4.0 (February 8, 2025) ### 💥 Breaking changes -* Drop support of PHP 8.0 ([#7](https://github.com/webalternatif/flysystem-dsn/pull/7)) +* Drop support for PHP 8.0 ([#7](https://github.com/webalternatif/flysystem-dsn/pull/7)) ### ✨ New features -* Add support of PHP 8.4 ([#7](https://github.com/webalternatif/flysystem-dsn/pull/7)) +* Add support for PHP 8.4 ([#7](https://github.com/webalternatif/flysystem-dsn/pull/7)) ### 🐛 Bug fixes @@ -23,13 +27,13 @@ ### ✨ New features -* Add support of PHP 8.3 ([#6](https://github.com/webalternatif/flysystem-dsn/pull/6)) +* Add support for PHP 8.3 ([#6](https://github.com/webalternatif/flysystem-dsn/pull/6)) ## v0.3.1 (December 15, 2022) ### ✨ New features -* Add support of PHP 8.2 ([#2](https://github.com/webalternatif/flysystem-openstack-swift/pull/2)) +* Add support for PHP 8.2 ([#2](https://github.com/webalternatif/flysystem-openstack-swift/pull/2)) ### 🐛 Bug fixes @@ -45,7 +49,7 @@ ### ✨ New features -* Add support of PHP 8.1 ([78c83c5](https://github.com/webalternatif/flysystem-openstack-swift/commit/78c83c525f0d1f42ffa8ac954a6efb11d261df5a)) +* Add support for PHP 8.1 ([78c83c5](https://github.com/webalternatif/flysystem-openstack-swift/commit/78c83c525f0d1f42ffa8ac954a6efb11d261df5a)) ## v0.2.0 (August 30, 2021) diff --git a/src/Config.php b/src/Config.php index 9d94411..7825877 100644 --- a/src/Config.php +++ b/src/Config.php @@ -8,6 +8,12 @@ final class Config extends BaseConfig { + // Options used by OpenStackSwiftAdapter::writeStream() public const OPTION_SEGMENT_SIZE = 'segment_size'; public const OPTION_SEGMENT_CONTAINER = 'segment_container'; + + // Options used by OpenStackSwiftAdapter::createFileAttributesFrom() + public const OPTION_DIGEST = 'digest'; + public const OPTION_FILE_NAME = 'file_name'; + public const OPTION_PREFIX = 'prefix'; } diff --git a/src/OpenStackSwiftAdapter.php b/src/OpenStackSwiftAdapter.php index 45580be..58d1384 100644 --- a/src/OpenStackSwiftAdapter.php +++ b/src/OpenStackSwiftAdapter.php @@ -21,18 +21,20 @@ use League\Flysystem\UnableToRetrieveMetadata; use League\Flysystem\UnableToSetVisibility; use League\Flysystem\UnableToWriteFile; +use League\Flysystem\UrlGeneration\TemporaryUrlGenerator; use OpenStack\Common\Error\BadResponseError; use OpenStack\ObjectStore\v1\Models\Container; use OpenStack\ObjectStore\v1\Models\StorageObject; use OpenStack\OpenStack; -final class OpenStackSwiftAdapter implements FilesystemAdapter +final class OpenStackSwiftAdapter implements FilesystemAdapter, TemporaryUrlGenerator { private ?Container $container = null; public function __construct( private OpenStack $openStack, - private string $containerName + private string $containerName, + private ?string $tempUrlKey = null, ) { } @@ -372,4 +374,44 @@ private function createFileAttributesFrom(StorageObject $object): FileAttributes $mimeType ); } + + /** + * Available options in {@param $config} are: + * - {@see Config::OPTION_DIGEST}: the digest algorithm to use for the HMAC cryptographic signature (given as first + * parameter of {@see hash_hmac}), default: sha256. + * - {@see Config::OPTION_FILE_NAME}: a string to override the default file name (which is based on the object name). + * - {@see Config::OPTION_PREFIX}: if `true`, a prefix-based temporary URL will be generated, default: `false`. + * + * For more information, see {@see https://docs.openstack.org/swift/latest/api/temporary_url_middleware.html}. + */ + #[\Override] + public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, BaseConfig $config): string + { + $expires = $expiresAt->getTimestamp(); + + $queryParams = [ + 'temp_url_expires' => $expires, + ]; + + $url = $this->getContainer()->getObject($path)->getPublicUri()->__toString(); + $hmacPath = preg_replace('#(.*)v1#U', '/v1', $url, 1); + + if (true === $config->get(Config::OPTION_PREFIX)) { + $hmacPath = "prefix:{$hmacPath}"; + $queryParams['temp_url_prefix'] = $path; + } + + $hmacBody = "GET\n{$expires}\n{$hmacPath}"; + $digest = (string) $config->get(Config::OPTION_DIGEST, 'sha256'); + $queryParams['temp_url_sig'] = hash_hmac($digest, $hmacBody, $this->tempUrlKey ?? ''); + + if (null !== ($fileName = $config->get(Config::OPTION_FILE_NAME))) { + $queryParams['filename'] = $fileName; + } + + return sprintf( + "{$url}?%s", + join('&', array_map(fn ($k, $v) => "{$k}={$v}", array_keys($queryParams), $queryParams)) + ); + } } diff --git a/tests/OpenStackSwiftAdapterTest.php b/tests/OpenStackSwiftAdapterTest.php index d662a45..9884641 100644 --- a/tests/OpenStackSwiftAdapterTest.php +++ b/tests/OpenStackSwiftAdapterTest.php @@ -39,7 +39,21 @@ protected static function createFilesystemAdapter(): FilesystemAdapter 'name' => self::$containerName, ]); - return new OpenStackSwiftAdapter($openstack, self::$containerName); + self::$container->execute( + [ + 'method' => 'POST', + 'path' => self::$containerName, + 'params' => [ + 'tempUrlKey' => [ + 'location' => 'header', + 'sentAs' => 'X-Container-Meta-Temp-URL-Key', + ], + ], + ], + ['tempUrlKey' => $containerTempUrlKey = uniqid()] + ); + + return new OpenStackSwiftAdapter($openstack, self::$containerName, $containerTempUrlKey); } public static function setUpBeforeClass(): void @@ -271,4 +285,37 @@ public function test_listing_slash_is_equivalent_to_listing_empty_string(): void ); }); } + + public function test_temporary_url_cannot_be_used_to_access_another_file(): void + { + $adapter = $this->adapter(); + + $adapter->write('some/file1.txt', 'file 1 contents', new Config(['visibility' => 'private'])); + $adapter->write('some/file2.txt', 'file 2 contents', new Config(['visibility' => 'private'])); + + $expiresAt = (new \DateTimeImmutable())->add(\DateInterval::createFromDateString('1 minute')); + $url = $adapter->temporaryUrl('some/file1.txt', $expiresAt, new Config()); + $contents = file_get_contents($url); + self::assertEquals('file 1 contents', $contents); + + $url = str_replace('file1.txt', 'file2.txt', $url); + self::assertFalse(@file_get_contents($url)); + } + + public function test_generating_prefixed_temporary_url(): void + { + $adapter = $this->adapter(); + + $adapter->write('some/file1.txt', 'file 1 contents', new Config(['visibility' => 'private'])); + $adapter->write('some/file2.txt', 'file 2 contents', new Config(['visibility' => 'private'])); + + $expiresAt = (new \DateTimeImmutable())->add(\DateInterval::createFromDateString('1 minute')); + $url = $adapter->temporaryUrl('some', $expiresAt, new Config(['prefix' => true])); + + $contents = file_get_contents(str_replace('/some?', '/some/file1.txt?', $url)); + self::assertEquals('file 1 contents', $contents); + + $contents = file_get_contents(str_replace('/some?', '/some/file2.txt?', $url)); + self::assertEquals('file 2 contents', $contents); + } } From 1f3e4f2dd5fde4c563d5124fa1aa7e1861052069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Thu, 11 Sep 2025 10:59:48 +0200 Subject: [PATCH 3/6] PHP-CS-Fixer --- composer.json | 4 +++- tests/OpenStackSwiftAdapterTest.php | 15 ++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index cac5e73..cd68846 100644 --- a/composer.json +++ b/composer.json @@ -24,9 +24,11 @@ "vimeo/psalm": "^6.5" }, "scripts": { + "cs-check": "php-cs-fixer fix --dry-run", + "cs-fix": "php-cs-fixer fix", "phpunit": "phpunit", "psalm": "psalm --threads=$(nproc) --no-cache", - "test": ["@psalm", "@phpunit"] + "test": ["@psalm", "@phpunit", "@cs-check"] }, "autoload": { "psr-4": { diff --git a/tests/OpenStackSwiftAdapterTest.php b/tests/OpenStackSwiftAdapterTest.php index 9884641..9888106 100644 --- a/tests/OpenStackSwiftAdapterTest.php +++ b/tests/OpenStackSwiftAdapterTest.php @@ -15,6 +15,7 @@ /** * @internal + * * @covers \Webf\Flysystem\OpenStackSwift\OpenStackSwiftAdapter */ class OpenStackSwiftAdapterTest extends FilesystemAdapterTestCase @@ -109,8 +110,8 @@ public function overwriting_a_file(): void $contents = $adapter->read('path.txt'); $this->assertEquals('new contents', $contents); -// $visibility = $adapter->visibility('path.txt')->visibility(); -// $this->assertEquals(Visibility::PRIVATE, $visibility); + // $visibility = $adapter->visibility('path.txt')->visibility(); + // $this->assertEquals(Visibility::PRIVATE, $visibility); }); } @@ -134,7 +135,7 @@ public function listing_contents_recursive(): void $listing = $adapter->listContents('', true); /** @var StorageAttributes[] $items */ $items = iterator_to_array($listing); -// $this->assertCount(2, $items, $this->formatIncorrectListingCount($items)); + // $this->assertCount(2, $items, $this->formatIncorrectListingCount($items)); $paths = array_map( fn (StorageAttributes $item) => $item->path(), @@ -180,7 +181,7 @@ public function copying_a_file(): void $this->assertTrue($adapter->fileExists('source.txt')); $this->assertTrue($adapter->fileExists('destination.txt')); -// $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('destination.txt')->visibility()); + // $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('destination.txt')->visibility()); $this->assertEquals('contents to be copied', $adapter->read('destination.txt')); }); } @@ -205,7 +206,7 @@ public function copying_a_file_again(): void $this->assertTrue($adapter->fileExists('source.txt')); $this->assertTrue($adapter->fileExists('destination.txt')); -// $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('destination.txt')->visibility()); + // $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('destination.txt')->visibility()); $this->assertEquals('contents to be copied', $adapter->read('destination.txt')); }); } @@ -234,7 +235,7 @@ public function moving_a_file(): void $adapter->fileExists('destination.txt'), 'After moving, a file should be present at the new location.' ); -// $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('destination.txt')->visibility()); + // $this->assertEquals(Visibility::PUBLIC, $adapter->visibility('destination.txt')->visibility()); $this->assertEquals('contents to be copied', $adapter->read('destination.txt')); }); } @@ -262,7 +263,7 @@ public function file_exists_on_directory_is_false(): void $this->assertFalse($adapter->directoryExists('test')); $adapter->createDirectory('test', new Config()); -// $this->assertTrue($adapter->directoryExists('test')); + // $this->assertTrue($adapter->directoryExists('test')); $this->assertFalse($adapter->fileExists('test')); }); } From 708bb5e9ea906683df2e684274f23760fb0e8c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Thu, 11 Sep 2025 11:38:23 +0200 Subject: [PATCH 4/6] Add documentation in README --- README.md | 69 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 07145f5..a44fb05 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,9 @@ $flysystem = new Filesystem($adapter); ### Uploading large objects -In order to use the `createLargeObject` method of the underlying OpenStack -library to upload [large objects][4] (which is mandatory for files over 5 GB), -you must use the `writeStream` method and define the `segment_size` config -option. +To use the `createLargeObject` method of the underlying OpenStack library to +upload [large objects][4] (which is mandatory for files over 5 GB), you must use +the `writeStream` method and define the `segment_size` config option. The `segment_container` option is also available if you want to upload segments in another container. @@ -56,16 +55,60 @@ in another container. ```php use Webf\Flysystem\OpenStackSwift\Config; -$flysystem->writeStream($path, $content, new Config([ +$flysystem->writeStream($path, $content, ([ Config::OPTION_SEGMENT_SIZE => 52428800, // 50 MiB Config::OPTION_SEGMENT_CONTAINER => 'test_segments', -])); +]); +``` + +### Generating temporary URLs + +This adapter supports generating temporary URLs as described in +[Flysystem's documentation][5]. + +To do so, you must : +- set a secret key at the _account_ or _container_ level of your OpenStack Swift + instance (see details in the [OpenStack documentation][6]), +- provide this secret key as third argument (`$tempUrlKey`) when creating the + adapter. + +#### Available options + +When calling `Filesystem::temporaryUrl()`, you can pass the following options as +third argument (`$config`): + +| Option key | Description | Type | Default value | +|-------------|----------------------------------------------------------------------------------------------------------------|----------|---------------| +| `digest` | The digest algorithm to use for the HMAC cryptographic signature (given as first parameter of [hash_hmac][7]). | `string` | `'sha256'` | +| `file_name` | A string to override the default file name (which is based on the object name) when the file is downloaded. | `string` | `null` | +| `prefix` | If `true`, a prefix-based temporary URL will be generated. | `bool` | `false` | + +Those option keys are available as public constants in the +`Webf\Flysystem\OpenStackSwift\Config` class. + +More information about those options can be found in the +[OpenStack documentation][8]. + +#### Example + +```php +use League\Flysystem\Filesystem; +use Webf\Flysystem\OpenStackSwift\OpenStackSwiftAdapter; + +// ... (see above) + +$adapter = new OpenStackSwiftAdapter($openstack, '{containerName}', '{tempUrlKey}'); +$flysystem = new Filesystem($adapter); + +$flysystem->temporaryUrl($path, new DateTime('+1 hour'), [ + // options... +]); ``` ## Tests This library uses the `FilesystemAdapterTestCase` provided by -[`league/flysystem-adapter-test-utilities`][5], so it performs integration tests +[`league/flysystem-adapter-test-utilities`][9], so it performs integration tests that need a real OpenStack Swift container. To run tests, duplicate the `phpunit.xml.dist` file into `phpunit.xml` and fill @@ -75,7 +118,7 @@ all the environment variables, then run: $ composer test ``` -This will run [Psalm][6] and [PHPUnit][7], but you can run them individually +This will run [Psalm][10] and [PHPUnit][11], but you can run them individually like this: ```bash @@ -87,6 +130,10 @@ $ composer phpunit [2]: https://github.com/php-opencloud/openstack [3]: https://github.com/chrisnharvey/flysystem-openstack-swift [4]: https://php-openstack-sdk.readthedocs.io/en/latest/services/object-store/v1/objects.html#create-a-large-object-over-5gb -[5]: https://github.com/thephpleague/flysystem-adapter-test-utilities -[6]: https://psalm.dev -[7]: https://phpunit.de +[5]: https://flysystem.thephpleague.com/docs/usage/temporary-urls +[6]: https://docs.openstack.org/swift/latest/api/temporary_url_middleware.html#secret-keys +[7]: https://www.php.net/manual/en/function.hash-hmac.php +[8]: https://docs.openstack.org/swift/latest/api/temporary_url_middleware.html +[9]: https://github.com/thephpleague/flysystem-adapter-test-utilities +[10]: https://psalm.dev +[11]: https://phpunit.de From ece34b69d6046662cd6ac1540fb9cda9a912d13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Thu, 11 Sep 2025 12:10:52 +0200 Subject: [PATCH 5/6] Fix typing --- src/OpenStackSwiftAdapter.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/OpenStackSwiftAdapter.php b/src/OpenStackSwiftAdapter.php index 58d1384..3cf1982 100644 --- a/src/OpenStackSwiftAdapter.php +++ b/src/OpenStackSwiftAdapter.php @@ -405,7 +405,9 @@ public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, BaseCo $digest = (string) $config->get(Config::OPTION_DIGEST, 'sha256'); $queryParams['temp_url_sig'] = hash_hmac($digest, $hmacBody, $this->tempUrlKey ?? ''); - if (null !== ($fileName = $config->get(Config::OPTION_FILE_NAME))) { + /** @var string|null $fileName */ + $fileName = $config->get(Config::OPTION_FILE_NAME); + if (is_string($fileName)) { $queryParams['filename'] = $fileName; } From 198df9c860a0a04c150dea87ecec0c723c17dace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Fri, 12 Sep 2025 10:51:48 +0200 Subject: [PATCH 6/6] Trigger exception when temporary url secret key is missing --- src/OpenStackSwiftAdapter.php | 7 ++++++- tests/OpenStackSwiftAdapterTest.php | 20 ++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/OpenStackSwiftAdapter.php b/src/OpenStackSwiftAdapter.php index 3cf1982..8e034ce 100644 --- a/src/OpenStackSwiftAdapter.php +++ b/src/OpenStackSwiftAdapter.php @@ -16,6 +16,7 @@ use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToDeleteDirectory; use League\Flysystem\UnableToDeleteFile; +use League\Flysystem\UnableToGenerateTemporaryUrl; use League\Flysystem\UnableToMoveFile; use League\Flysystem\UnableToReadFile; use League\Flysystem\UnableToRetrieveMetadata; @@ -387,6 +388,10 @@ private function createFileAttributesFrom(StorageObject $object): FileAttributes #[\Override] public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, BaseConfig $config): string { + if (null === $this->tempUrlKey) { + throw new UnableToGenerateTemporaryUrl(sprintf('The `$tempUrlKey` argument must be provided to %s\'s constructor in order to generate temporary URLs.', self::class), $path); + } + $expires = $expiresAt->getTimestamp(); $queryParams = [ @@ -403,7 +408,7 @@ public function temporaryUrl(string $path, \DateTimeInterface $expiresAt, BaseCo $hmacBody = "GET\n{$expires}\n{$hmacPath}"; $digest = (string) $config->get(Config::OPTION_DIGEST, 'sha256'); - $queryParams['temp_url_sig'] = hash_hmac($digest, $hmacBody, $this->tempUrlKey ?? ''); + $queryParams['temp_url_sig'] = hash_hmac($digest, $hmacBody, $this->tempUrlKey); /** @var string|null $fileName */ $fileName = $config->get(Config::OPTION_FILE_NAME); diff --git a/tests/OpenStackSwiftAdapterTest.php b/tests/OpenStackSwiftAdapterTest.php index 9888106..f7ba9fe 100644 --- a/tests/OpenStackSwiftAdapterTest.php +++ b/tests/OpenStackSwiftAdapterTest.php @@ -6,6 +6,7 @@ use League\Flysystem\Config; use League\Flysystem\FilesystemAdapter; use League\Flysystem\StorageAttributes; +use League\Flysystem\UnableToGenerateTemporaryUrl; use League\Flysystem\Visibility; use OpenStack\Common\Error\BadResponseError; use OpenStack\ObjectStore\v1\Models\Container; @@ -23,9 +24,9 @@ class OpenStackSwiftAdapterTest extends FilesystemAdapterTestCase private static ?Container $container = null; private static ?string $containerName = null; - protected static function createFilesystemAdapter(): FilesystemAdapter + private static function createOpenStack(): OpenStack { - $openstack = new OpenStack([ + return new OpenStack([ 'authUrl' => $_ENV['OPENSTACK_AUTH_URL'], 'region' => $_ENV['OPENSTACK_REGION'], 'user' => [ @@ -35,6 +36,11 @@ protected static function createFilesystemAdapter(): FilesystemAdapter ], 'scope' => ['project' => ['id' => $_ENV['OPENSTACK_PROJECT_ID']]], ]); + } + + protected static function createFilesystemAdapter(): FilesystemAdapter + { + $openstack = self::createOpenStack(); self::$container = $openstack->objectStoreV1()->createContainer([ 'name' => self::$containerName, @@ -319,4 +325,14 @@ public function test_generating_prefixed_temporary_url(): void $contents = file_get_contents(str_replace('/some?', '/some/file2.txt?', $url)); self::assertEquals('file 2 contents', $contents); } + + public function test_temporary_url_generation_needs_secret_key(): void + { + $openstack = self::createOpenStack(); + + $adapter = new OpenStackSwiftAdapter($openstack, self::$containerName); + + $this->expectException(UnableToGenerateTemporaryUrl::class); + $adapter->temporaryUrl('some/file.txt', new \DateTimeImmutable(), new Config()); + } }