Re-internalize Flysystem extensions.

This commit is contained in:
Buster Neece 2022-11-13 02:00:56 -06:00
parent 0ba1556a95
commit c69ed7570e
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
38 changed files with 895 additions and 120 deletions

View File

@ -28,7 +28,6 @@
"ext-xmlwriter": "*",
"azuracast/doctrine-batch-utilities": "dev-main",
"azuracast/doctrine-entity-normalizer": "dev-main",
"azuracast/flysystem-v2-extensions": "dev-main",
"azuracast/nowplaying": "dev-main",
"azuracast/slim-callable-eventdispatcher": "dev-main",
"bacon/bacon-qr-code": "^2.0",

83
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1cbe0493b1bb2b62ab667d12fec4d738",
"content-hash": "418f33f513ae89bfcf89c29142408445",
"packages": [
{
"name": "aws/aws-crt-php",
@ -264,86 +264,6 @@
],
"time": "2022-06-12T14:29:46+00:00"
},
{
"name": "azuracast/flysystem-v2-extensions",
"version": "dev-main",
"source": {
"type": "git",
"url": "https://github.com/AzuraCast/flysystem-v2-extensions.git",
"reference": "44915dea8668d120e540bd1acfdd4d3bd40eee20"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/AzuraCast/flysystem-v2-extensions/zipball/44915dea8668d120e540bd1acfdd4d3bd40eee20",
"reference": "44915dea8668d120e540bd1acfdd4d3bd40eee20",
"shasum": ""
},
"require": {
"league/flysystem": "^3.1",
"php": "^8.0"
},
"require-dev": {
"league/flysystem-aws-s3-v3": "^3.0",
"league/flysystem-sftp-v3": "^3.0",
"php-parallel-lint/php-console-highlighter": "^0.5.0",
"php-parallel-lint/php-parallel-lint": "^1.3",
"phpstan/phpstan": "^1.0",
"roave/security-advisories": "dev-latest",
"spatie/flysystem-dropbox": ">2.0.5"
},
"suggest": {
"league/flysystem-aws-s3-v3": "AWS S3 API version 3 adapter for Flysystem V3.",
"league/flysystem-sftp-v3": "SFTP adapter for Flysystem V3",
"spatie/flysystem-dropbox": "Dropbox adapter for Flysystem V3."
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"Azura\\Files\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Buster Neece",
"email": "buster@busterneece.com"
}
],
"description": "Extensions to Flysystem V2 functionality.",
"keywords": [
"aws",
"cloud",
"file",
"files",
"filesystem",
"filesystems",
"s3",
"storage"
],
"support": {
"issues": "https://github.com/AzuraCast/flysystem-v2-extensions/issues",
"source": "https://github.com/AzuraCast/flysystem-v2-extensions/tree/main"
},
"funding": [
{
"url": "https://github.com/AzuraCast",
"type": "github"
},
{
"url": "https://opencollective.com/azuracast",
"type": "open_collective"
},
{
"url": "https://www.patreon.com/AzuraCast",
"type": "patreon"
}
],
"time": "2022-10-08T22:14:27+00:00"
},
{
"name": "azuracast/nowplaying",
"version": "dev-main",
@ -13978,7 +13898,6 @@
"stability-flags": {
"azuracast/doctrine-batch-utilities": 20,
"azuracast/doctrine-entity-normalizer": 20,
"azuracast/flysystem-v2-extensions": 20,
"azuracast/nowplaying": 20,
"azuracast/slim-callable-eventdispatcher": 20,
"lstrojny/fxmlrpc": 20,

View File

@ -7,7 +7,7 @@ namespace App\Controller\Api\Admin\Backups;
use App\Entity;
use App\Http\Response;
use App\Http\ServerRequest;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\ExtendedFilesystemInterface;
use Psr\Http\Message\ResponseInterface;
final class DeleteAction extends AbstractFileAction

View File

@ -6,7 +6,7 @@ namespace App\Controller\Api\Admin\Backups;
use App\Http\Response;
use App\Http\ServerRequest;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\ExtendedFilesystemInterface;
use Psr\Http\Message\ResponseInterface;
final class DownloadAction extends AbstractFileAction

View File

@ -8,7 +8,7 @@ use App\Entity;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Paginator;
use Azura\Files\Attributes\FileAttributes;
use App\Flysystem\Attributes\FileAttributes;
use League\Flysystem\StorageAttributes;
use Psr\Http\Message\ResponseInterface;

View File

@ -9,7 +9,7 @@ use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use App\OpenApi;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\ExtendedFilesystemInterface;
use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface;

View File

@ -18,7 +18,7 @@ use App\Radio\Backend\Liquidsoap;
use App\Radio\Enums\BackendAdapters;
use App\Radio\Enums\LiquidsoapQueues;
use App\Utilities\File;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\ExtendedFilesystemInterface;
use Exception;
use InvalidArgumentException;
use League\Flysystem\StorageAttributes;

View File

@ -11,7 +11,7 @@ use App\Exception\InvalidPodcastMediaFileException;
use App\Exception\StorageLocationFullException;
use App\Media\AlbumArt;
use App\Media\MetadataManager;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\ExtendedFilesystemInterface;
use League\Flysystem\UnableToDeleteFile;
use League\Flysystem\UnableToRetrieveMetadata;

View File

@ -9,7 +9,7 @@ use App\Doctrine\Repository;
use App\Entity;
use App\Exception\StorageLocationFullException;
use App\Media\AlbumArt;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\ExtendedFilesystemInterface;
use League\Flysystem\UnableToDeleteFile;
use League\Flysystem\UnableToRetrieveMetadata;

View File

@ -13,7 +13,7 @@ use App\Media\MetadataManager;
use App\Media\RemoteAlbumArt;
use App\Service\AudioWaveform;
use App\Utilities\Logger;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\ExtendedFilesystemInterface;
use Exception;
use Generator;
use League\Flysystem\FilesystemException;

View File

@ -7,7 +7,7 @@ namespace App\Entity\Repository;
use App\Entity;
use App\Flysystem\StationFilesystems;
use App\Service\Flow\UploadedFile;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\ExtendedFilesystemInterface;
/**
* @extends AbstractStationBasedRepository<Entity\StationMount>

View File

@ -8,10 +8,10 @@ use App\Assets\AlbumArtCustomAsset;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Doctrine\Repository;
use App\Entity;
use App\Flysystem\ExtendedFilesystemInterface;
use App\Flysystem\StationFilesystems;
use App\Radio\Enums\StreamFormats;
use App\Service\Flow\UploadedFile;
use Azura\Files\ExtendedFilesystemInterface;
use Closure;
use Psr\Http\Message\UriInterface;

View File

@ -11,8 +11,8 @@ use App\Entity\StorageLocationAdapter\StorageLocationAdapterInterface;
use App\Exception\StorageLocationFullException;
use App\Radio\Quota;
use App\Validator\Constraints as AppAssert;
use Azura\Files\Adapter\ExtendedAdapterInterface;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\Adapter\ExtendedAdapterInterface;
use App\Flysystem\ExtendedFilesystemInterface;
use Brick\Math\BigInteger;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

View File

@ -5,8 +5,8 @@ declare(strict_types=1);
namespace App\Entity\StorageLocationAdapter;
use App\Entity\StorageLocation;
use Azura\Files\ExtendedFilesystemInterface;
use Azura\Files\RemoteFilesystem;
use App\Flysystem\ExtendedFilesystemInterface;
use App\Flysystem\RemoteFilesystem;
abstract class AbstractStorageLocationLocationAdapter implements StorageLocationAdapterInterface
{

View File

@ -5,8 +5,8 @@ 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 App\Flysystem\Adapter\DropboxAdapter;
use App\Flysystem\Adapter\ExtendedAdapterInterface;
use Spatie\Dropbox\Client;
final class DropboxStorageLocationAdapter extends AbstractStorageLocationLocationAdapter

View File

@ -5,10 +5,10 @@ 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;
use App\Flysystem\Adapter\LocalAdapterInterface;
use App\Flysystem\Adapter\LocalFilesystemAdapter;
use App\Flysystem\ExtendedFilesystemInterface;
use App\Flysystem\LocalFilesystem;
final class LocalStorageLocationAdapter extends AbstractStorageLocationLocationAdapter
{

View File

@ -5,9 +5,9 @@ declare(strict_types=1);
namespace App\Entity\StorageLocationAdapter;
use App\Entity\Enums\StorageLocationAdapters;
use App\Flysystem\Adapter\AwsS3Adapter;
use App\Flysystem\Adapter\ExtendedAdapterInterface;
use Aws\S3\S3Client;
use Azura\Files\Adapter\AwsS3\AwsS3Adapter;
use Azura\Files\Adapter\ExtendedAdapterInterface;
use InvalidArgumentException;
final class S3StorageLocationAdapter extends AbstractStorageLocationLocationAdapter

View File

@ -5,8 +5,8 @@ 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 App\Flysystem\Adapter\ExtendedAdapterInterface;
use App\Flysystem\Adapter\SftpAdapter;
use League\Flysystem\PhpseclibV3\SftpConnectionProvider;
final class SftpStorageLocationAdapter extends AbstractStorageLocationLocationAdapter

View File

@ -5,8 +5,8 @@ declare(strict_types=1);
namespace App\Entity\StorageLocationAdapter;
use App\Entity\Enums\StorageLocationAdapters;
use Azura\Files\Adapter\ExtendedAdapterInterface;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\Adapter\ExtendedAdapterInterface;
use App\Flysystem\ExtendedFilesystemInterface;
interface StorageLocationAdapterInterface
{

View File

@ -0,0 +1,61 @@
<?php
namespace App\Flysystem;
use App\Flysystem\Adapter\ExtendedAdapterInterface;
use App\Flysystem\Normalizer\WhitespacePathNormalizer;
use League\Flysystem\Filesystem;
use League\Flysystem\PathNormalizer;
use League\Flysystem\StorageAttributes;
use League\Flysystem\UnableToRetrieveMetadata;
abstract class AbstractFilesystem extends Filesystem implements ExtendedFilesystemInterface
{
protected ExtendedAdapterInterface $adapter;
public function __construct(
ExtendedAdapterInterface $adapter,
array $config = [],
PathNormalizer $pathNormalizer = null
) {
$this->adapter = $adapter;
$pathNormalizer = $pathNormalizer ?: new WhitespacePathNormalizer();
parent::__construct($adapter, $config, $pathNormalizer);
}
public function getAdapter(): ExtendedAdapterInterface
{
return $this->adapter;
}
public function getMetadata(string $path): StorageAttributes
{
return $this->adapter->getMetadata($path);
}
public function isDir(string $path): bool
{
try {
return $this->getMetadata($path)->isDir();
} catch (UnableToRetrieveMetadata $e) {
return false;
}
}
public function isFile(string $path): bool
{
try {
return $this->getMetadata($path)->isFile();
} catch (UnableToRetrieveMetadata $e) {
return false;
}
}
public function uploadAndDeleteOriginal(string $localPath, string $to): void
{
$this->upload($localPath, $to);
@unlink($localPath);
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Flysystem\Adapter;
use App\Flysystem\Attributes\DirectoryAttributes;
use App\Flysystem\Attributes\FileAttributes;
use Aws\Api\DateTimeResult;
use Aws\S3\S3ClientInterface;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
use League\Flysystem\AwsS3V3\VisibilityConverter;
use League\Flysystem\PathPrefixer;
use League\Flysystem\StorageAttributes;
use League\Flysystem\UnableToRetrieveMetadata;
use League\MimeTypeDetection\MimeTypeDetector;
use Throwable;
final class AwsS3Adapter extends AwsS3V3Adapter implements ExtendedAdapterInterface
{
private readonly PathPrefixer $prefixer;
public function __construct(
private readonly S3ClientInterface $client,
private readonly string $bucket,
string $prefix = '',
VisibilityConverter $visibility = null,
MimeTypeDetector $mimeTypeDetector = null,
array $options = [],
bool $streamReads = true
) {
$this->prefixer = new PathPrefixer($prefix);
parent::__construct($client, $bucket, $prefix, $visibility, $mimeTypeDetector, $options, $streamReads);
}
/** @inheritDoc */
public function getMetadata(string $path): StorageAttributes
{
$arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];
$command = $this->client->getCommand('HeadObject', $arguments);
try {
$metadata = $this->client->execute($command);
} catch (Throwable $exception) {
throw UnableToRetrieveMetadata::create($path, 'metadata', '', $exception);
}
if (substr($path, -1) === '/') {
return new DirectoryAttributes(rtrim($path, '/'));
}
$mimetype = $metadata['ContentType'] ?? null;
$fileSize = $metadata['ContentLength'] ?? $metadata['Size'] ?? null;
$fileSize = $fileSize === null ? null : (int)$fileSize;
$dateTime = $metadata['LastModified'] ?? null;
$lastModified = $dateTime instanceof DateTimeResult ? $dateTime->getTimeStamp() : null;
$visibility = function ($path) {
return $this->visibility($path)->visibility();
};
return new FileAttributes(
$path,
$fileSize,
$visibility,
$lastModified,
$mimetype
);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Flysystem\Adapter;
use League\Flysystem\StorageAttributes;
use League\Flysystem\UnableToRetrieveMetadata;
use Spatie\Dropbox\Exceptions\BadRequest;
use Spatie\FlysystemDropbox\DropboxAdapter as SpatieDropboxAdapter;
final class DropboxAdapter extends SpatieDropboxAdapter implements ExtendedAdapterInterface
{
/** @inheritDoc */
public function getMetadata(string $path): StorageAttributes
{
$location = $this->applyPathPrefix($path);
try {
$response = $this->client->getMetadata($location);
} catch (BadRequest $e) {
throw UnableToRetrieveMetadata::create($location, 'metadata', $e->getMessage());
}
return $this->normalizeResponse($response);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Flysystem\Adapter;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\FilesystemException;
use League\Flysystem\StorageAttributes;
use League\Flysystem\UnableToRetrieveMetadata;
interface ExtendedAdapterInterface extends FilesystemAdapter
{
/**
* @throws UnableToRetrieveMetadata
* @throws FilesystemException
*/
public function getMetadata(string $path): StorageAttributes;
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Flysystem\Adapter;
interface LocalAdapterInterface extends ExtendedAdapterInterface
{
public function getLocalPath(string $path): string;
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Flysystem\Adapter;
use App\Flysystem\Attributes\DirectoryAttributes;
use App\Flysystem\Attributes\FileAttributes;
use League\Flysystem\Local\LocalFilesystemAdapter as LeagueLocalFilesystemAdapter;
use League\Flysystem\PathPrefixer;
use League\Flysystem\StorageAttributes;
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use League\Flysystem\UnixVisibility\VisibilityConverter;
use League\MimeTypeDetection\MimeTypeDetector;
final class LocalFilesystemAdapter extends LeagueLocalFilesystemAdapter implements LocalAdapterInterface
{
private readonly PathPrefixer $pathPrefixer;
private readonly VisibilityConverter $visibility;
public function __construct(
string $location,
VisibilityConverter $visibility = null,
int $writeFlags = LOCK_EX,
int $linkHandling = self::DISALLOW_LINKS,
MimeTypeDetector $mimeTypeDetector = null
) {
$this->pathPrefixer = new PathPrefixer($location, DIRECTORY_SEPARATOR);
$this->visibility = $visibility ?: new PortableVisibilityConverter();
parent::__construct($location, $visibility, $writeFlags, $linkHandling, $mimeTypeDetector);
}
public function getLocalPath(string $path): string
{
return $this->pathPrefixer->prefixPath($path);
}
/** @inheritDoc */
public function getMetadata(string $path): StorageAttributes
{
$location = $this->pathPrefixer->prefixPath($path);
if (!file_exists($location)) {
throw UnableToRetrieveMetadata::create($location, 'metadata', 'File not found');
}
$fileInfo = new \SplFileInfo($location);
$lastModified = $fileInfo->getMTime();
$isDirectory = $fileInfo->isDir();
$permissions = $fileInfo->getPerms();
$visibility = $isDirectory
? $this->visibility->inverseForDirectory($permissions)
: $this->visibility->inverseForFile($permissions);
return $isDirectory
? new DirectoryAttributes($path, $visibility, $lastModified)
: new FileAttributes(
$path,
$fileInfo->getSize(),
$visibility,
$lastModified,
fn() => $this->mimeType($path)->mimeType()
);
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Flysystem\Adapter;
use App\Flysystem\Attributes\DirectoryAttributes;
use App\Flysystem\Attributes\FileAttributes;
use League\Flysystem\PathPrefixer;
use League\Flysystem\PhpseclibV3\ConnectionProvider;
use League\Flysystem\PhpseclibV3\SftpAdapter as LeagueSftpAdapter;
use League\Flysystem\StorageAttributes;
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use League\Flysystem\UnixVisibility\VisibilityConverter;
use League\MimeTypeDetection\FinfoMimeTypeDetector;
use League\MimeTypeDetection\MimeTypeDetector;
final class SftpAdapter extends LeagueSftpAdapter implements ExtendedAdapterInterface
{
private const NET_SFTP_TYPE_DIRECTORY = 2;
private readonly VisibilityConverter $visibilityConverter;
private readonly PathPrefixer $prefixer;
public function __construct(
private readonly ConnectionProvider $connectionProvider,
string $root,
VisibilityConverter $visibilityConverter = null,
MimeTypeDetector $mimeTypeDetector = null
) {
$this->visibilityConverter = $visibilityConverter ?: new PortableVisibilityConverter();
$this->prefixer = new PathPrefixer($root);
$mimeTypeDetector ??= new FinfoMimeTypeDetector();
parent::__construct($connectionProvider, $root, $visibilityConverter, $mimeTypeDetector);
}
/** @inheritDoc */
public function getMetadata(string $path): StorageAttributes
{
$location = $this->prefixer->prefixPath($path);
$connection = $this->connectionProvider->provideConnection();
$stat = $connection->stat($location);
if (!is_array($stat)) {
throw UnableToRetrieveMetadata::create($path, 'metadata');
}
$attributes = $this->convertListingToAttributes($path, $stat);
if (!$attributes instanceof FileAttributes) {
throw UnableToRetrieveMetadata::create($path, 'metadata', 'path is not a file');
}
return $attributes;
}
private function convertListingToAttributes(string $path, array $attributes): StorageAttributes
{
$permissions = $attributes['mode'] & 0777;
$lastModified = $attributes['mtime'] ?? null;
if ($attributes['type'] === self::NET_SFTP_TYPE_DIRECTORY) {
return new DirectoryAttributes(
ltrim($path, '/'),
$this->visibilityConverter->inverseForDirectory($permissions),
$lastModified
);
}
return new FileAttributes(
$path,
$attributes['size'],
$this->visibilityConverter->inverseForFile($permissions),
$lastModified
);
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Flysystem\Attributes;
use League\Flysystem\ProxyArrayAccessToProperties;
use League\Flysystem\StorageAttributes;
use League\Flysystem\UnableToRetrieveMetadata;
abstract class AbstractAttributes implements StorageAttributes
{
use ProxyArrayAccessToProperties;
protected string $type;
/**
* @param string $path
* @param string|callable|null $visibility
* @param int|callable|null $lastModified
* @param array $extraMetadata
*/
public function __construct(
protected string $path,
protected $visibility = null,
protected $lastModified = null,
protected array $extraMetadata = []
) {
}
public function path(): string
{
return $this->path;
}
public function type(): string
{
return $this->type;
}
public function visibility(): ?string
{
$visibility = (is_callable($this->visibility))
? ($this->visibility)($this->path)
: $this->visibility;
return $visibility;
}
public function lastModified(): ?int
{
$lastModified = is_callable($this->lastModified)
? ($this->lastModified)($this->path)
: $this->lastModified;
if (null === $lastModified) {
throw UnableToRetrieveMetadata::lastModified($this->path);
}
return $lastModified;
}
public function extraMetadata(): array
{
return $this->extraMetadata;
}
public function isFile(): bool
{
return (StorageAttributes::TYPE_FILE === $this->type);
}
public function isDir(): bool
{
return (StorageAttributes::TYPE_DIRECTORY === $this->type);
}
public function withPath(string $path): StorageAttributes
{
$clone = clone $this;
$clone->path = $path;
return $clone;
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Flysystem\Attributes;
use League\Flysystem\StorageAttributes;
final class DirectoryAttributes extends AbstractAttributes
{
/**
* @param string $path
* @param string|callable|null $visibility
* @param int|callable|null $lastModified
* @param array $extraMetadata
*/
public function __construct(string $path, $visibility = null, $lastModified = null, array $extraMetadata = [])
{
$this->type = StorageAttributes::TYPE_DIRECTORY;
parent::__construct($path, $visibility, $lastModified, $extraMetadata);
}
public static function fromArray(array $attributes): self
{
return new self(
$attributes[StorageAttributes::ATTRIBUTE_PATH],
$attributes[StorageAttributes::ATTRIBUTE_VISIBILITY] ?? null,
$attributes[StorageAttributes::ATTRIBUTE_LAST_MODIFIED] ?? null,
$attributes[StorageAttributes::ATTRIBUTE_EXTRA_METADATA] ?? [],
);
}
/**
* @inheritDoc
*/
public function jsonSerialize(): array
{
return [
StorageAttributes::ATTRIBUTE_TYPE => $this->type,
StorageAttributes::ATTRIBUTE_PATH => $this->path,
StorageAttributes::ATTRIBUTE_VISIBILITY => $this->visibility,
StorageAttributes::ATTRIBUTE_LAST_MODIFIED => $this->lastModified,
StorageAttributes::ATTRIBUTE_EXTRA_METADATA => $this->extraMetadata,
];
}
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace App\Flysystem\Attributes;
use League\Flysystem\StorageAttributes;
use League\Flysystem\UnableToRetrieveMetadata;
final class FileAttributes extends AbstractAttributes
{
/**
* @param string $path
* @param int|callable|null $fileSize
* @param string|callable|null $visibility
* @param int|callable|null $lastModified
* @param string|callable|null $mimeType
* @param array $extraMetadata
*/
public function __construct(
string $path,
private $fileSize = null,
$visibility = null,
$lastModified = null,
private $mimeType = null,
array $extraMetadata = []
) {
$this->type = StorageAttributes::TYPE_FILE;
parent::__construct($path, $visibility, $lastModified, $extraMetadata);
}
public function fileSize(): ?int
{
$fileSize = is_callable($this->fileSize)
? ($this->fileSize)($this->path)
: $this->fileSize;
if (null === $fileSize) {
throw UnableToRetrieveMetadata::fileSize($this->path);
}
return $fileSize;
}
public function mimeType(): ?string
{
$mimeType = is_callable($this->mimeType)
? ($this->mimeType)($this->path)
: $this->mimeType;
if (null === $mimeType) {
throw UnableToRetrieveMetadata::mimeType($this->path);
}
return $mimeType;
}
public static function fromArray(array $attributes): self
{
return new self(
$attributes[StorageAttributes::ATTRIBUTE_PATH],
$attributes[StorageAttributes::ATTRIBUTE_FILE_SIZE] ?? null,
$attributes[StorageAttributes::ATTRIBUTE_VISIBILITY] ?? null,
$attributes[StorageAttributes::ATTRIBUTE_LAST_MODIFIED] ?? null,
$attributes[StorageAttributes::ATTRIBUTE_MIME_TYPE] ?? null,
$attributes[StorageAttributes::ATTRIBUTE_EXTRA_METADATA] ?? []
);
}
public function jsonSerialize(): array
{
return [
StorageAttributes::ATTRIBUTE_TYPE => self::TYPE_FILE,
StorageAttributes::ATTRIBUTE_PATH => $this->path,
StorageAttributes::ATTRIBUTE_FILE_SIZE => $this->fileSize,
StorageAttributes::ATTRIBUTE_VISIBILITY => $this->visibility,
StorageAttributes::ATTRIBUTE_LAST_MODIFIED => $this->lastModified,
StorageAttributes::ATTRIBUTE_MIME_TYPE => $this->mimeType,
StorageAttributes::ATTRIBUTE_EXTRA_METADATA => $this->extraMetadata,
];
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Flysystem;
use App\Flysystem\Adapter\ExtendedAdapterInterface;
use League\Flysystem\FilesystemOperator;
use League\Flysystem\StorageAttributes;
interface ExtendedFilesystemInterface extends FilesystemOperator
{
/**
* @return ExtendedAdapterInterface The underlying filesystem adapter.
*/
public function getAdapter(): ExtendedAdapterInterface;
/**
* @return bool Whether this filesystem is directly located on disk.
*/
public function isLocal(): bool;
/**
* @param string $path The original path of the file on the filesystem.
*
* @return string A path that will be guaranteed to be local to the filesystem.
*/
public function getLocalPath(string $path): string;
/**
* @param string $path
*
* @return StorageAttributes Metadata for the specified path.
*/
public function getMetadata(string $path): StorageAttributes;
public function isDir(string $path): bool;
public function isFile(string $path): bool;
/**
* Call a callable function with a path that is guaranteed to be a local path, even if
* this filesystem is a remote one, by copying to a temporary directory first in the
* case of remote filesystems.
*
* @param string $path
* @param callable $function
*
* @return mixed
*/
public function withLocalFile(string $path, callable $function);
/**
* @param string $localPath
* @param string $to
*/
public function uploadAndDeleteOriginal(string $localPath, string $to): void;
/**
* @param string $localPath
* @param string $to
*/
public function upload(string $localPath, string $to): void;
/**
* @param string $from
* @param string $localPath
*/
public function download(string $from, string $localPath): void;
}

View File

@ -0,0 +1,99 @@
<?php
namespace App\Flysystem;
use App\Flysystem\Adapter\LocalAdapterInterface;
use League\Flysystem\PathNormalizer;
use League\Flysystem\UnableToCopyFile;
use League\Flysystem\UnableToCreateDirectory;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use League\Flysystem\UnixVisibility\VisibilityConverter;
final class LocalFilesystem extends AbstractFilesystem
{
private readonly LocalAdapterInterface $localAdapter;
private readonly VisibilityConverter $visibilityConverter;
public function __construct(
LocalAdapterInterface $adapter,
array $config = [],
PathNormalizer $pathNormalizer = null,
VisibilityConverter $visibilityConverter = null
) {
$this->localAdapter = $adapter;
$this->visibilityConverter = $visibilityConverter ?? new PortableVisibilityConverter();
parent::__construct($adapter, $config, $pathNormalizer);
}
/** @inheritDoc */
public function isLocal(): bool
{
return true;
}
/** @inheritDoc */
public function getLocalPath(string $path): string
{
return $this->localAdapter->getLocalPath($path);
}
/** @inheritDoc */
public function upload(string $localPath, string $to): void
{
$destPath = $this->getLocalPath($to);
$this->ensureDirectoryExists(
dirname($destPath),
$this->visibilityConverter->defaultForDirectories()
);
if (!@copy($localPath, $destPath)) {
throw UnableToCopyFile::fromLocationTo($localPath, $destPath);
}
}
/** @inheritDoc */
public function download(string $from, string $localPath): void
{
$sourcePath = $this->getLocalPath($from);
$this->ensureDirectoryExists(
dirname($localPath),
$this->visibilityConverter->defaultForDirectories()
);
if (!@copy($sourcePath, $localPath)) {
throw UnableToCopyFile::fromLocationTo($sourcePath, $localPath);
}
}
/** @inheritDoc */
public function withLocalFile(string $path, callable $function)
{
$localPath = $this->getLocalPath($path);
return $function($localPath);
}
private function ensureDirectoryExists(string $dirname, int $visibility): void
{
if (is_dir($dirname)) {
return;
}
error_clear_last();
if (!@mkdir($dirname, $visibility, true)) {
$mkdirError = error_get_last();
}
clearstatcache(false, $dirname);
if (!is_dir($dirname)) {
$errorMessage = $mkdirError['message'] ?? '';
throw UnableToCreateDirectory::atLocation($dirname, $errorMessage);
}
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Flysystem\Normalizer;
use League\Flysystem\PathNormalizer;
use League\Flysystem\PathTraversalDetected;
final class WhitespacePathNormalizer implements PathNormalizer
{
public function normalizePath(string $path): string
{
$path = str_replace('\\', '/', $path);
$path = $this->removeFunkyWhiteSpace($path);
return $this->normalizeRelativePath($path);
}
private function removeFunkyWhiteSpace(string $path): string
{
// Remove unprintable characters and invalid unicode characters.
// We do this check in a loop, since removing invalid unicode characters
// can lead to new characters being created.
//
// Customized regex for zero-width chars
// @see https://github.com/thephpleague/flysystem/issues/1157
while (preg_match('#\p{C}-[\x{200C}-\x{200D}]+|^\./#u', $path)) {
$path = (string) preg_replace('#\p{C}-[\x{200C}-\x{200D}]+|^\./#u', '', $path);
}
return $path;
}
private function normalizeRelativePath(string $path): string
{
$parts = [];
foreach (explode('/', $path) as $part) {
switch ($part) {
case '':
case '.':
break;
case '..':
if (empty($parts)) {
throw PathTraversalDetected::forPath($path);
}
array_pop($parts);
break;
default:
$parts[] = $part;
break;
}
}
return implode('/', $parts);
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Flysystem;
use App\Flysystem\Adapter\ExtendedAdapterInterface;
use League\Flysystem\PathNormalizer;
use League\Flysystem\PathPrefixer;
final class RemoteFilesystem extends AbstractFilesystem
{
private readonly PathPrefixer $localPath;
public function __construct(
ExtendedAdapterInterface $remoteAdapter,
string $localPath = null,
array $config = [],
PathNormalizer $pathNormalizer = null
) {
$this->localPath = new PathPrefixer($localPath ?? sys_get_temp_dir());
parent::__construct($remoteAdapter, $config, $pathNormalizer);
}
/** @inheritDoc */
public function isLocal(): bool
{
return false;
}
/** @inheritDoc */
public function getLocalPath(string $path): string
{
$tempLocalPath = $this->localPath->prefixPath(
substr(md5($path), 0, 10) . '_' . basename($path),
);
$this->download($path, $tempLocalPath);
return $tempLocalPath;
}
/** @inheritDoc */
public function withLocalFile(string $path, callable $function)
{
$localPath = $this->getLocalPath($path);
try {
$returnVal = $function($localPath);
} finally {
unlink($localPath);
}
return $returnVal;
}
/** @inheritDoc */
public function upload(string $localPath, string $to): void
{
if (!is_file($localPath)) {
throw new \RuntimeException(sprintf('Source upload file not found at path: %s', $localPath));
}
$stream = fopen($localPath, 'rb');
try {
$this->writeStream($to, $stream);
} finally {
if (is_resource($stream)) {
fclose($stream);
}
}
}
/** @inheritDoc */
public function download(string $from, string $localPath): void
{
if (is_file($localPath)) {
if (filemtime($localPath) >= $this->lastModified($from)) {
touch($localPath);
return;
}
unlink($localPath);
}
$stream = $this->readStream($from);
file_put_contents($localPath, $stream);
if (is_resource($stream)) {
fclose($stream);
}
}
}

View File

@ -5,11 +5,8 @@ declare(strict_types=1);
namespace App\Flysystem;
use App\Entity;
use Azura\Files\Adapter\Local\LocalFilesystemAdapter;
use Azura\Files\Adapter\LocalAdapterInterface;
use Azura\Files\ExtendedFilesystemInterface;
use Azura\Files\LocalFilesystem;
use Azura\Files\RemoteFilesystem;
use App\Flysystem\Adapter\LocalAdapterInterface;
use App\Flysystem\Adapter\LocalFilesystemAdapter;
final class StationFilesystems
{

View File

@ -5,8 +5,8 @@ declare(strict_types=1);
namespace App\Http;
use App\Nginx\CustomUrls;
use Azura\Files\Adapter\LocalAdapterInterface;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\Adapter\LocalAdapterInterface;
use App\Flysystem\ExtendedFilesystemInterface;
use InvalidArgumentException;
use League\Flysystem\FileAttributes;
use Psr\Http\Message\ResponseInterface;

View File

@ -7,7 +7,7 @@ namespace App\Media;
use App\Entity;
use App\Utilities\File;
use Azura\DoctrineBatchUtils\ReadWriteBatchIteratorAggregate;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\ExtendedFilesystemInterface;
use Doctrine\ORM\EntityManagerInterface;
use Throwable;

View File

@ -7,7 +7,7 @@ namespace App\Sync\Task;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity;
use App\Flysystem\StationFilesystems;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\ExtendedFilesystemInterface;
use Doctrine\ORM\Query;
use Psr\Log\LoggerInterface;

View File

@ -12,8 +12,8 @@ use App\Message\ProcessCoverArtMessage;
use App\Message\ReprocessMediaMessage;
use App\MessageQueue\QueueManagerInterface;
use App\Radio\Quota;
use Azura\Files\Attributes\FileAttributes;
use Azura\Files\ExtendedFilesystemInterface;
use App\Flysystem\Attributes\FileAttributes;
use App\Flysystem\ExtendedFilesystemInterface;
use Brick\Math\BigInteger;
use Doctrine\ORM\AbstractQuery;
use League\Flysystem\FilesystemException;