Extend storage location adapters into separate classes.

This commit is contained in:
Buster Neece 2022-10-27 04:31:15 -05:00
parent 9b9a19aa7d
commit 6f6d5ab692
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
8 changed files with 315 additions and 145 deletions

View File

@ -4,6 +4,12 @@ declare(strict_types=1);
namespace App\Entity\Enums;
use App\Entity\StorageLocationAdapter\DropboxStorageLocationAdapter;
use App\Entity\StorageLocationAdapter\LocalStorageLocationAdapter;
use App\Entity\StorageLocationAdapter\S3StorageLocationAdapter;
use App\Entity\StorageLocationAdapter\SftpStorageLocationAdapter;
use App\Entity\StorageLocationAdapter\StorageLocationAdapterInterface;
enum StorageLocationAdapters: string
{
case Local = 'local';
@ -25,4 +31,17 @@ enum StorageLocationAdapters: string
self::Sftp => 'SFTP',
};
}
/**
* @return class-string<StorageLocationAdapterInterface>
*/
public function getAdapterClass(): string
{
return match ($this) {
self::Local => LocalStorageLocationAdapter::class,
self::S3 => S3StorageLocationAdapter::class,
self::Dropbox => DropboxStorageLocationAdapter::class,
self::Sftp => SftpStorageLocationAdapter::class
};
}
}

View File

@ -7,29 +7,16 @@ namespace App\Entity;
use App\Entity\Enums\StorageLocationAdapters;
use App\Entity\Enums\StorageLocationTypes;
use App\Entity\Interfaces\IdentifiableEntityInterface;
use App\Entity\StorageLocationAdapter\StorageLocationAdapterInterface;
use App\Exception\StorageLocationFullException;
use App\Radio\Quota;
use App\Validator\Constraints as AppAssert;
use Aws\S3\S3Client;
use Azura\Files\Adapter\AwsS3\AwsS3Adapter;
use Azura\Files\Adapter\Dropbox\DropboxAdapter;
use Azura\Files\Adapter\ExtendedAdapterInterface;
use Azura\Files\Adapter\Local\LocalFilesystemAdapter;
use Azura\Files\Adapter\LocalAdapterInterface;
use Azura\Files\Adapter\Sftp\SftpAdapter;
use Azura\Files\ExtendedFilesystemInterface;
use Azura\Files\LocalFilesystem;
use Azura\Files\RemoteFilesystem;
use Brick\Math\BigInteger;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException;
use League\Flysystem\PhpseclibV3\SftpConnectionProvider;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use League\Flysystem\Visibility;
use RuntimeException;
use Spatie\Dropbox\Client;
use Stringable;
#[
@ -152,23 +139,6 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
return $this->path;
}
public function getFilteredPath(): string
{
return match ($this->getAdapterEnum()) {
StorageLocationAdapters::S3, StorageLocationAdapters::Dropbox => trim($this->path, '/'),
default => rtrim($this->path, '/')
};
}
public function applyPath(?string $suffix = null): string
{
$suffix = (null !== $suffix)
? '/' . ltrim($suffix, '/')
: '';
return $this->path . $suffix;
}
public function setPath(string $path): void
{
$this->path = $this->truncateString($path);
@ -496,137 +466,35 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
return $this->media;
}
public function getUri(?string $suffix = null): string
public function getStorageLocationAdapter(): StorageLocationAdapterInterface
{
$path = $this->applyPath($suffix);
return match ($this->getAdapterEnum()) {
StorageLocationAdapters::S3 => $this->getS3ObjectUri($suffix),
StorageLocationAdapters::Dropbox => 'dropbox://' . $this->dropboxAuthToken . ltrim($path, '/'),
default => $path,
};
$adapterClass = $this->getAdapterEnum()->getAdapterClass();
return new $adapterClass($this);
}
protected function getS3ObjectUri(?string $suffix = null): string
public function getUri(?string $suffix = null): string
{
$path = $this->applyPath($suffix);
return $this->getStorageLocationAdapter()->getUri($suffix);
}
$bucket = $this->s3Bucket;
if (null === $bucket) {
return 'No S3 Bucket Specified';
}
try {
$client = $this->getS3Client();
if (empty($path)) {
$objectUrl = $client->getObjectUrl($bucket, '/');
return rtrim($objectUrl, '/');
}
return $client->getObjectUrl($bucket, ltrim($path, '/'));
} catch (InvalidArgumentException $e) {
return 'Invalid URI (' . $e->getMessage() . ')';
}
public function getFilteredPath(): string
{
return $this->getStorageLocationAdapter()::filterPath($this->path);
}
public function validate(): void
{
if (StorageLocationAdapters::S3 === $this->getAdapterEnum()) {
$client = $this->getS3Client();
$client->listObjectsV2(
[
'Bucket' => $this->s3Bucket,
'max-keys' => 1,
]
);
}
$adapter = $this->getStorageAdapter();
$adapter->fileExists('/test');
$this->getStorageLocationAdapter()->validate();
}
public function getStorageAdapter(): ExtendedAdapterInterface
{
$filteredPath = $this->getFilteredPath();
switch ($this->getAdapterEnum()) {
case StorageLocationAdapters::S3:
$bucket = $this->s3Bucket;
if (null === $bucket) {
throw new RuntimeException('Amazon S3 bucket is empty.');
}
return new AwsS3Adapter($this->getS3Client(), $bucket, $filteredPath);
case StorageLocationAdapters::Dropbox:
return new DropboxAdapter($this->getDropboxClient(), $filteredPath);
case StorageLocationAdapters::Sftp:
return new SftpAdapter($this->getSftpConnectionProvider(), $filteredPath);
default:
return new LocalFilesystemAdapter($filteredPath);
}
}
protected function getS3Client(): S3Client
{
if (StorageLocationAdapters::S3 !== $this->getAdapterEnum()) {
throw new InvalidArgumentException('This storage location is not using the S3 adapter.');
}
$s3Options = array_filter(
[
'credentials' => [
'key' => $this->s3CredentialKey,
'secret' => $this->s3CredentialSecret,
],
'region' => $this->s3Region,
'version' => $this->s3Version,
'endpoint' => $this->s3Endpoint,
]
);
return new S3Client($s3Options);
}
protected function getDropboxClient(): Client
{
if (StorageLocationAdapters::Dropbox !== $this->getAdapterEnum()) {
throw new InvalidArgumentException('This storage location is not using the Dropbox adapter.');
}
$creds = (!empty($this->dropboxAppKey) && !empty($this->dropboxAppSecret))
? [$this->dropboxAppKey, $this->dropboxAppSecret]
: $this->dropboxAuthToken;
return new Client($creds);
}
protected function getSftpConnectionProvider(): SftpConnectionProvider
{
if (StorageLocationAdapters::Sftp !== $this->getAdapterEnum()) {
throw new InvalidArgumentException('This storage location is not using the SFTP adapter.');
}
return new SftpConnectionProvider(
$this->sftpHost ?? '',
$this->sftpUsername ?? '',
$this->sftpPassword,
$this->sftpPrivateKey,
$this->sftpPrivateKeyPassPhrase,
$this->sftpPort ?? 22
);
return $this->getStorageLocationAdapter()->getStorageAdapter();
}
public function getFilesystem(): ExtendedFilesystemInterface
{
$adapter = $this->getStorageAdapter();
return ($adapter instanceof LocalAdapterInterface)
? new LocalFilesystem(
adapter: $adapter,
visibilityConverter: new PortableVisibilityConverter(defaultForDirectories: Visibility::PUBLIC)
)
: new RemoteFilesystem($adapter);
return $this->getStorageLocationAdapter()->getFilesystem();
}
public function __toString(): string

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Entity\StorageLocationAdapter;
use App\Entity\StorageLocation;
use Azura\Files\ExtendedFilesystemInterface;
use Azura\Files\RemoteFilesystem;
abstract class AbstractStorageLocationLocationAdapter implements StorageLocationAdapterInterface
{
public function __construct(
protected readonly StorageLocation $storageLocation
) {
if ($this->getType() !== $storageLocation->getAdapterEnum()) {
throw new \InvalidArgumentException('This storage location is not using the specified adapter.');
}
}
public static function filterPath(string $path): string
{
return rtrim($path, '/');
}
public function getUri(?string $suffix = null): string
{
return $this->applyPath($suffix);
}
protected function applyPath(?string $suffix = null): string
{
$suffix = (null !== $suffix)
? '/' . ltrim($suffix, '/')
: '';
return $this->storageLocation->getPath() . $suffix;
}
public function getFilesystem(): ExtendedFilesystemInterface
{
return new RemoteFilesystem($this->getStorageAdapter());
}
public function validate(): void
{
$adapter = $this->getStorageAdapter();
$adapter->fileExists('/test');
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Entity\StorageLocationAdapter;
use App\Entity\Enums\StorageLocationAdapters;
use Azura\Files\Adapter\Dropbox\DropboxAdapter;
use Azura\Files\Adapter\ExtendedAdapterInterface;
use Spatie\Dropbox\Client;
final class DropboxStorageLocationAdapter extends AbstractStorageLocationLocationAdapter
{
public function getType(): StorageLocationAdapters
{
return StorageLocationAdapters::Dropbox;
}
public static function filterPath(string $path): string
{
return trim($path, '/');
}
public function getUri(?string $suffix = null): string
{
$path = $this->applyPath($suffix);
$appKey = $this->storageLocation->getDropboxAppKey();
$uriPrefix = (!empty($appKey))
? $appKey
: $this->storageLocation->getDropboxAuthToken();
return 'dropbox://' . $uriPrefix . '/' . ltrim($path, '/');
}
public function getStorageAdapter(): ExtendedAdapterInterface
{
$filteredPath = self::filterPath($this->storageLocation->getPath());
return new DropboxAdapter($this->getClient(), $filteredPath);
}
private function getClient(): Client
{
$appKey = $this->storageLocation->getDropboxAppKey();
$appSecret = $this->storageLocation->getDropboxAppSecret();
$authToken = $this->storageLocation->getDropboxAuthToken();
$creds = (!empty($appKey) && !empty($appSecret))
? [$appKey, $appSecret]
: $authToken;
return new Client($creds);
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Entity\StorageLocationAdapter;
use App\Entity\Enums\StorageLocationAdapters;
use Azura\Files\Adapter\Local\LocalFilesystemAdapter;
use Azura\Files\Adapter\LocalAdapterInterface;
use Azura\Files\ExtendedFilesystemInterface;
use Azura\Files\LocalFilesystem;
final class LocalStorageLocationAdapter extends AbstractStorageLocationLocationAdapter
{
public function getType(): StorageLocationAdapters
{
return StorageLocationAdapters::Local;
}
public function getStorageAdapter(): LocalAdapterInterface
{
$filteredPath = self::filterPath($this->storageLocation->getPath());
return new LocalFilesystemAdapter($filteredPath);
}
public function getFilesystem(): ExtendedFilesystemInterface
{
return new LocalFilesystem($this->getStorageAdapter());
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Entity\StorageLocationAdapter;
use App\Entity\Enums\StorageLocationAdapters;
use Aws\S3\S3Client;
use Azura\Files\Adapter\AwsS3\AwsS3Adapter;
use Azura\Files\Adapter\ExtendedAdapterInterface;
use InvalidArgumentException;
final class S3StorageLocationAdapter extends AbstractStorageLocationLocationAdapter
{
public function getType(): StorageLocationAdapters
{
return StorageLocationAdapters::S3;
}
public static function filterPath(string $path): string
{
return trim($path, '/');
}
public function getUri(?string $suffix = null): string
{
$path = $this->applyPath($suffix);
$bucket = $this->storageLocation->getS3Bucket();
if (null === $bucket) {
return 'No S3 Bucket Specified';
}
try {
$client = $this->getClient();
if (empty($path)) {
$objectUrl = $client->getObjectUrl($bucket, '/');
return rtrim($objectUrl, '/');
}
return $client->getObjectUrl($bucket, ltrim($path, '/'));
} catch (InvalidArgumentException $e) {
return 'Invalid URI (' . $e->getMessage() . ')';
}
}
public function getStorageAdapter(): ExtendedAdapterInterface
{
$filteredPath = self::filterPath($this->storageLocation->getPath());
$bucket = $this->storageLocation->getS3Bucket();
if (null === $bucket) {
throw new \RuntimeException('Amazon S3 bucket is empty.');
}
return new AwsS3Adapter($this->getClient(), $bucket, $filteredPath);
}
public function validate(): void
{
$client = $this->getClient();
$client->listObjectsV2(
[
'Bucket' => $this->storageLocation->getS3Bucket(),
'max-keys' => 1,
]
);
parent::validate();
}
private function getClient(): S3Client
{
$s3Options = array_filter(
[
'credentials' => [
'key' => $this->storageLocation->getS3CredentialKey(),
'secret' => $this->storageLocation->getS3CredentialSecret(),
],
'region' => $this->storageLocation->getS3Region(),
'version' => $this->storageLocation->getS3Version(),
'endpoint' => $this->storageLocation->getS3Endpoint(),
]
);
return new S3Client($s3Options);
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Entity\StorageLocationAdapter;
use App\Entity\Enums\StorageLocationAdapters;
use Azura\Files\Adapter\ExtendedAdapterInterface;
use Azura\Files\Adapter\Sftp\SftpAdapter;
use League\Flysystem\PhpseclibV3\SftpConnectionProvider;
final class SftpStorageLocationAdapter extends AbstractStorageLocationLocationAdapter
{
public function getType(): StorageLocationAdapters
{
return StorageLocationAdapters::Sftp;
}
public function getStorageAdapter(): ExtendedAdapterInterface
{
$filteredPath = self::filterPath($this->storageLocation->getPath());
return new SftpAdapter($this->getSftpConnectionProvider(), $filteredPath);
}
private function getSftpConnectionProvider(): SftpConnectionProvider
{
return new SftpConnectionProvider(
$this->storageLocation->getSftpHost() ?? '',
$this->storageLocation->getSftpUsername() ?? '',
$this->storageLocation->getSftpPassword(),
$this->storageLocation->getSftpPrivateKey(),
$this->storageLocation->getSftpPrivateKeyPassPhrase(),
$this->storageLocation->getSftpPort() ?? 22
);
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Entity\StorageLocationAdapter;
use App\Entity\Enums\StorageLocationAdapters;
use Azura\Files\Adapter\ExtendedAdapterInterface;
use Azura\Files\ExtendedFilesystemInterface;
interface StorageLocationAdapterInterface
{
public function getType(): StorageLocationAdapters;
public function getUri(?string $suffix = null): string;
public function getStorageAdapter(): ExtendedAdapterInterface;
public function getFilesystem(): ExtendedFilesystemInterface;
public function validate(): void;
public static function filterPath(string $path): string;
}