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
20 changes: 15 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
## v0.5.0 (unreleased)

### 💥 Breaking changes

* 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

Expand All @@ -17,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

Expand All @@ -39,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)

Expand Down
69 changes: 58 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
8 changes: 7 additions & 1 deletion src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@

use League\Flysystem\Config as BaseConfig;

class Config extends BaseConfig
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';
}
70 changes: 68 additions & 2 deletions src/OpenStackSwiftAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,26 @@
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;
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;

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,
) {
}

Expand All @@ -48,6 +51,7 @@ private function getContainer(): Container
return $this->container;
}

#[\Override]
public function fileExists(string $path): bool
{
try {
Expand All @@ -57,6 +61,7 @@ public function fileExists(string $path): bool
}
}

#[\Override]
public function directoryExists(string $path): bool
{
try {
Expand All @@ -69,6 +74,7 @@ public function directoryExists(string $path): bool
}
}

#[\Override]
public function write(string $path, string $contents, BaseConfig $config): void
{
$data = [
Expand All @@ -83,6 +89,7 @@ public function write(string $path, string $contents, BaseConfig $config): void
}
}

#[\Override]
public function writeStream(string $path, $contents, BaseConfig $config): void
{
$data = [
Expand Down Expand Up @@ -111,6 +118,7 @@ public function writeStream(string $path, $contents, BaseConfig $config): void
}
}

#[\Override]
public function read(string $path): string
{
$object = $this->getContainer()->getObject($path);
Expand All @@ -128,6 +136,7 @@ public function read(string $path): string
}
}

#[\Override]
public function readStream(string $path)
{
$object = $this->getContainer()->getObject($path);
Expand All @@ -149,6 +158,7 @@ public function readStream(string $path)
}
}

#[\Override]
public function delete(string $path): void
{
$object = $this->getContainer()->getObject($path);
Expand All @@ -164,6 +174,7 @@ public function delete(string $path): void
}
}

#[\Override]
public function deleteDirectory(string $path): void
{
// Make sure a slash is added to the end.
Expand All @@ -189,21 +200,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);
Expand All @@ -223,6 +238,7 @@ public function mimeType(string $path): FileAttributes
return $fileAttributes;
}

#[\Override]
public function lastModified(string $path): FileAttributes
{
$object = $this->getContainer()->getObject($path);
Expand All @@ -242,6 +258,7 @@ public function lastModified(string $path): FileAttributes
return $fileAttributes;
}

#[\Override]
public function fileSize(string $path): FileAttributes
{
$object = $this->getContainer()->getObject($path);
Expand All @@ -261,6 +278,7 @@ public function fileSize(string $path): FileAttributes
return $fileAttributes;
}

#[\Override]
public function listContents(string $path, bool $deep): iterable
{
$path = trim($path, '/');
Expand Down Expand Up @@ -298,6 +316,7 @@ public function listContents(string $path, bool $deep): iterable
}
}

#[\Override]
public function move(string $source, string $destination, BaseConfig $config): void
{
if ($source === $destination) {
Expand All @@ -312,6 +331,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);
Expand Down Expand Up @@ -355,4 +375,50 @@ 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
{
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 = [
'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);

/** @var string|null $fileName */
$fileName = $config->get(Config::OPTION_FILE_NAME);
if (is_string($fileName)) {
$queryParams['filename'] = $fileName;
}

return sprintf(
"{$url}?%s",
join('&', array_map(fn ($k, $v) => "{$k}={$v}", array_keys($queryParams), $queryParams))
);
}
}
Loading