Update to Flysystem V2 (#3956)

This commit is contained in:
Buster "Silver Eagle" Neece 2021-03-31 11:42:24 -05:00 committed by GitHub
parent 3419e58d83
commit c5352c42be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1026 additions and 1217 deletions

View File

@ -4,7 +4,7 @@
"license": "Apache-2.0",
"type": "project",
"require": {
"php": ">=7.4",
"php": ">=7.4 || ^8.0",
"ext-PDO": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
@ -34,13 +34,11 @@
"http-interop/http-factory-guzzle": "^1.0",
"intervention/image": "^2.5",
"james-heinrich/getid3": "^1.9.9",
"jhofm/flysystem-iterator": "^2.1",
"laminas/laminas-config": "^3.3",
"league/csv": "^9.6",
"league/flysystem": "^1.0",
"league/flysystem-aws-s3-v3": "^1.0",
"league/flysystem-cached-adapter": "^1.0",
"league/mime-type-detection": "^1.5",
"league/flysystem": "^2.0",
"league/flysystem-aws-s3-v3": "^2.0",
"league/mime-type-detection": "^1.7",
"league/plates": "^3.1",
"lstrojny/fxmlrpc": "dev-master",
"matomo/device-detector": "^4.0",
@ -62,7 +60,7 @@
"rlanvin/php-ip": "^2.0",
"slim/http": "^1.1",
"slim/slim": "^4.2",
"spatie/flysystem-dropbox": "^1.2",
"spatie/flysystem-dropbox": "^2",
"spomky-labs/otphp": "^10.0",
"supervisorphp/supervisor": "dev-main",
"symfony/cache": "^5.2",

302
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": "40a0455319934ddab8203b2b0712bfa3",
"content-hash": "c27d2c8a4e5bf6def0fc196f5da7b50a",
"packages": [
{
"name": "aws/aws-sdk-php",
@ -2672,65 +2672,18 @@
},
"time": "2020-06-30T18:43:34+00:00"
},
{
"name": "jhofm/flysystem-iterator",
"version": "v2.2.1",
"source": {
"type": "git",
"url": "https://github.com/jhofm/flysystem-iterator.git",
"reference": "b0ead46b255a8be14bdb2e2b4a871be63c395985"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jhofm/flysystem-iterator/zipball/b0ead46b255a8be14bdb2e2b4a871be63c395985",
"reference": "b0ead46b255a8be14bdb2e2b4a871be63c395985",
"shasum": ""
},
"require": {
"ext-json": "*",
"league/flysystem": "^1.0",
"php": "^7.0"
},
"require-dev": {
"league/flysystem-memory": "^1.0",
"phpunit/phpunit": "^6.0|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Jhofm\\FlysystemIterator\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Johannes Hofmann",
"email": "hofmann.johannes@gmail.com"
}
],
"description": "Iterator plugin for league/flysystem",
"support": {
"issues": "https://github.com/jhofm/flysystem-iterator/issues",
"source": "https://github.com/jhofm/flysystem-iterator/tree/v2.2.1"
},
"abandoned": "league/flysystem",
"time": "2020-12-25T18:00:39+00:00"
},
{
"name": "laminas/laminas-code",
"version": "4.0.0",
"version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-code.git",
"reference": "28a6d70ea8b8bca687d7163300e611ae33baf82a"
"reference": "5b553c274b94af3f880cbaaf8fbab047f279a31c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laminas/laminas-code/zipball/28a6d70ea8b8bca687d7163300e611ae33baf82a",
"reference": "28a6d70ea8b8bca687d7163300e611ae33baf82a",
"url": "https://api.github.com/repos/laminas/laminas-code/zipball/5b553c274b94af3f880cbaaf8fbab047f279a31c",
"reference": "5b553c274b94af3f880cbaaf8fbab047f279a31c",
"shasum": ""
},
"require": {
@ -2788,7 +2741,7 @@
"type": "community_bridge"
}
],
"time": "2020-12-30T16:16:14+00:00"
"time": "2021-03-27T13:55:31+00:00"
},
{
"name": "laminas/laminas-config",
@ -3050,22 +3003,22 @@
},
{
"name": "league/csv",
"version": "9.6.2",
"version": "9.7.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/csv.git",
"reference": "f28da6e483bf979bac10e2add384c90ae9983e4e"
"reference": "4cacd9c72c4aa8bdbef43315b2ca25c46a0f833f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/csv/zipball/f28da6e483bf979bac10e2add384c90ae9983e4e",
"reference": "f28da6e483bf979bac10e2add384c90ae9983e4e",
"url": "https://api.github.com/repos/thephpleague/csv/zipball/4cacd9c72c4aa8bdbef43315b2ca25c46a0f833f",
"reference": "4cacd9c72c4aa8bdbef43315b2ca25c46a0f833f",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": ">=7.2.5"
"php": "^7.3 || ^8.0"
},
"require-dev": {
"ext-curl": "*",
@ -3074,7 +3027,7 @@
"phpstan/phpstan": "^0.12.0",
"phpstan/phpstan-phpunit": "^0.12.0",
"phpstan/phpstan-strict-rules": "^0.12.0",
"phpunit/phpunit": "^8.5"
"phpunit/phpunit": "^9.5"
},
"suggest": {
"ext-dom": "Required to use the XMLConverter and or the HTMLConverter classes",
@ -3130,59 +3083,46 @@
"type": "github"
}
],
"time": "2020-12-10T19:40:30+00:00"
"time": "2021-03-26T22:08:10+00:00"
},
{
"name": "league/flysystem",
"version": "1.1.3",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "9be3b16c877d477357c015cec057548cf9b2a14a"
"reference": "7cbbb7222e8d8a34e71273d243dfcf383ed6779f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/9be3b16c877d477357c015cec057548cf9b2a14a",
"reference": "9be3b16c877d477357c015cec057548cf9b2a14a",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/7cbbb7222e8d8a34e71273d243dfcf383ed6779f",
"reference": "7cbbb7222e8d8a34e71273d243dfcf383ed6779f",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
"league/mime-type-detection": "^1.3",
"php": "^7.2.5 || ^8.0"
"ext-json": "*",
"league/mime-type-detection": "^1.0.0",
"php": "^7.2 || ^8.0"
},
"conflict": {
"league/flysystem-sftp": "<1.0.6"
"guzzlehttp/ringphp": "<1.1.1"
},
"require-dev": {
"phpspec/prophecy": "^1.11.1",
"phpunit/phpunit": "^8.5.8"
},
"suggest": {
"ext-fileinfo": "Required for MimeType",
"ext-ftp": "Allows you to use FTP server storage",
"ext-openssl": "Allows you to use FTPS server storage",
"league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2",
"league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3",
"league/flysystem-azure": "Allows you to use Windows Azure Blob storage",
"league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching",
"league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem",
"league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files",
"league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib",
"league/flysystem-webdav": "Allows you to use WebDAV storage",
"league/flysystem-ziparchive": "Allows you to use ZipArchive adapter",
"spatie/flysystem-dropbox": "Allows you to use Dropbox storage",
"srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications"
"async-aws/s3": "^1.5",
"async-aws/simple-s3": "^1.0",
"aws/aws-sdk-php": "^3.132.4",
"composer/semver": "^3.0",
"ext-fileinfo": "*",
"friendsofphp/php-cs-fixer": "^2.16",
"google/cloud-storage": "^1.23",
"phpseclib/phpseclib": "^2.0",
"phpstan/phpstan": "^0.12.26",
"phpunit/phpunit": "^8.5 || ^9.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
}
},
"autoload": {
"psr-4": {
"League\\Flysystem\\": "src/"
"League\\Flysystem\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@ -3192,73 +3132,70 @@
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frenky.net"
"email": "info@frankdejonge.nl"
}
],
"description": "Filesystem abstraction: Many filesystems, one API.",
"description": "File storage abstraction for PHP",
"keywords": [
"Cloud Files",
"WebDAV",
"abstraction",
"aws",
"cloud",
"copy.com",
"dropbox",
"file systems",
"file",
"files",
"filesystem",
"filesystems",
"ftp",
"rackspace",
"remote",
"s3",
"sftp",
"storage"
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
"source": "https://github.com/thephpleague/flysystem/tree/1.x"
"source": "https://github.com/thephpleague/flysystem/tree/2.0.4"
},
"funding": [
{
"url": "https://offset.earth/frankdejonge",
"type": "other"
"type": "custom"
},
{
"url": "https://github.com/frankdejonge",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/flysystem",
"type": "tidelift"
}
],
"time": "2020-08-23T07:39:11+00:00"
"time": "2021-02-12T19:37:50+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
"version": "1.0.29",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
"reference": "4e25cc0582a36a786c31115e419c6e40498f6972"
"reference": "c89931e9c4b294493234798564cc814ef478fbc6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/4e25cc0582a36a786c31115e419c6e40498f6972",
"reference": "4e25cc0582a36a786c31115e419c6e40498f6972",
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/c89931e9c4b294493234798564cc814ef478fbc6",
"reference": "c89931e9c4b294493234798564cc814ef478fbc6",
"shasum": ""
},
"require": {
"aws/aws-sdk-php": "^3.20.0",
"league/flysystem": "^1.0.40",
"php": ">=5.5.0"
"aws/aws-sdk-php": "^3.132.4",
"league/flysystem": "^2.0.0",
"league/mime-type-detection": "^1.0.0",
"php": "^7.2 || ^8.0"
},
"require-dev": {
"henrikbjorn/phpspec-code-coverage": "~1.0.1",
"phpspec/phpspec": "^2.0.0"
"conflict": {
"guzzlehttp/ringphp": "<1.1.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"League\\Flysystem\\AwsS3v3\\": "src/"
"League\\Flysystem\\AwsS3V3\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
@ -3268,66 +3205,24 @@
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frenky.net"
"email": "info@frankdejonge.nl"
}
],
"description": "Flysystem adapter for the AWS S3 SDK v3.x",
"description": "AWS S3 filesystem adapter for Flysystem.",
"keywords": [
"Flysystem",
"aws",
"file",
"files",
"filesystem",
"s3",
"storage"
],
"support": {
"issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues",
"source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/1.0.29"
"source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/2.0.4"
},
"time": "2020-10-08T18:58:37+00:00"
},
{
"name": "league/flysystem-cached-adapter",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-cached-adapter.git",
"reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-cached-adapter/zipball/d1925efb2207ac4be3ad0c40b8277175f99ffaff",
"reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff",
"shasum": ""
},
"require": {
"league/flysystem": "~1.0",
"psr/cache": "^1.0.0"
},
"require-dev": {
"mockery/mockery": "~0.9",
"phpspec/phpspec": "^3.4",
"phpunit/phpunit": "^5.7",
"predis/predis": "~1.0",
"tedivm/stash": "~0.12"
},
"suggest": {
"ext-phpredis": "Pure C implemented extension for PHP"
},
"type": "library",
"autoload": {
"psr-4": {
"League\\Flysystem\\Cached\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "frankdejonge",
"email": "info@frenky.net"
}
],
"description": "An adapter decorator to enable meta-data caching.",
"support": {
"issues": "https://github.com/thephpleague/flysystem-cached-adapter/issues",
"source": "https://github.com/thephpleague/flysystem-cached-adapter/tree/master"
},
"time": "2020-07-25T15:56:04+00:00"
"time": "2021-02-09T21:10:56+00:00"
},
{
"name": "league/mime-type-detection",
@ -5996,25 +5891,26 @@
},
{
"name": "spatie/flysystem-dropbox",
"version": "1.2.3",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/flysystem-dropbox.git",
"reference": "8b6b072f217343b875316ca6a4203dd59f04207a"
"reference": "0bb9f5485d7ac61665ffbcf7dbca38bf2be3174f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/flysystem-dropbox/zipball/8b6b072f217343b875316ca6a4203dd59f04207a",
"reference": "8b6b072f217343b875316ca6a4203dd59f04207a",
"url": "https://api.github.com/repos/spatie/flysystem-dropbox/zipball/0bb9f5485d7ac61665ffbcf7dbca38bf2be3174f",
"reference": "0bb9f5485d7ac61665ffbcf7dbca38bf2be3174f",
"shasum": ""
},
"require": {
"league/flysystem": "^1.0.20",
"php": "^7.0 || ^8.0",
"spatie/dropbox-api": "^1.1.0"
"league/flysystem": "^2.0.4",
"php": ">7.2 || ^8.0",
"spatie/dropbox-api": "^1.17.1"
},
"require-dev": {
"phpunit/phpunit": "^6.5.14 || ^7.5.20 || ^8.5.11 || ^9.4.3"
"phpspec/prophecy-phpunit": "^2.0.1",
"phpunit/phpunit": "^9.5.4"
},
"type": "library",
"autoload": {
@ -6046,9 +5942,19 @@
],
"support": {
"issues": "https://github.com/spatie/flysystem-dropbox/issues",
"source": "https://github.com/spatie/flysystem-dropbox/tree/1.2.3"
"source": "https://github.com/spatie/flysystem-dropbox/tree/2.0.1"
},
"time": "2020-11-28T22:17:09+00:00"
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
},
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2021-03-31T07:44:17+00:00"
},
{
"name": "spomky-labs/otphp",
@ -9148,16 +9054,16 @@
},
{
"name": "codeception/codeception",
"version": "4.1.18",
"version": "4.1.19",
"source": {
"type": "git",
"url": "https://github.com/Codeception/Codeception.git",
"reference": "f47547bac347dfb5ea5351ff91148cbcc08e6818"
"reference": "138dc9345a81ec994dcd6b9680c501a752a37b00"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Codeception/Codeception/zipball/f47547bac347dfb5ea5351ff91148cbcc08e6818",
"reference": "f47547bac347dfb5ea5351ff91148cbcc08e6818",
"url": "https://api.github.com/repos/Codeception/Codeception/zipball/138dc9345a81ec994dcd6b9680c501a752a37b00",
"reference": "138dc9345a81ec994dcd6b9680c501a752a37b00",
"shasum": ""
},
"require": {
@ -9231,7 +9137,7 @@
],
"support": {
"issues": "https://github.com/Codeception/Codeception/issues",
"source": "https://github.com/Codeception/Codeception/tree/4.1.18"
"source": "https://github.com/Codeception/Codeception/tree/4.1.19"
},
"funding": [
{
@ -9239,7 +9145,7 @@
"type": "open_collective"
}
],
"time": "2021-02-23T17:11:42+00:00"
"time": "2021-03-28T13:26:08+00:00"
},
{
"name": "codeception/lib-asserts",
@ -10619,16 +10525,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.5",
"version": "9.2.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1"
"reference": "f6293e1b30a2354e8428e004689671b83871edde"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f3e026641cc91909d421802dd3ac7827ebfd97e1",
"reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde",
"reference": "f6293e1b30a2354e8428e004689671b83871edde",
"shasum": ""
},
"require": {
@ -10684,7 +10590,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.5"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6"
},
"funding": [
{
@ -10692,7 +10598,7 @@
"type": "github"
}
],
"time": "2020-11-28T06:44:49+00:00"
"time": "2021-03-28T07:26:59+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -12941,7 +12847,7 @@
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": ">=7.4",
"php": ">=7.4 || ^8.0",
"ext-pdo": "*",
"ext-fileinfo": "*",
"ext-gd": "*",

View File

@ -167,7 +167,7 @@ class BackupCommand extends CommandAbstract
if (null !== $storageLocation) {
$fs = $storageLocation->getFilesystem();
$fs->putFromLocal($tmpPath, $path);
$fs->uploadAndDeleteOriginal($tmpPath, $path);
}
$io->newLine();

View File

@ -5,7 +5,6 @@ namespace App\Console\Command\Internal;
use App\Console\Application;
use App\Console\Command\CommandAbstract;
use App\Entity;
use App\Flysystem\FilesystemManager;
use App\Message;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
@ -18,19 +17,15 @@ class SftpUploadCommand extends CommandAbstract
protected LoggerInterface $logger;
protected FilesystemManager $filesystem;
public function __construct(
Application $application,
MessageBus $messageBus,
LoggerInterface $logger,
FilesystemManager $filesystem
LoggerInterface $logger
) {
parent::__construct($application);
$this->messageBus = $messageBus;
$this->logger = $logger;
$this->filesystem = $filesystem;
}
public function __invoke(
@ -70,18 +65,9 @@ class SftpUploadCommand extends CommandAbstract
return 1;
}
$this->flushCache($storageLocation);
return $this->handleNewUpload($storageLocation, $path);
}
protected function flushCache(Entity\StorageLocation $storageLocation): void
{
$adapter = $storageLocation->getStorageAdapter();
$fs = $this->filesystem->getFilesystemForAdapter($adapter, true);
$fs->clearCache(false);
}
protected function handleNewUpload(Entity\StorageLocation $storageLocation, $path): int
{
$relativePath = str_replace($storageLocation->getPath() . '/', '', $path);

View File

@ -6,7 +6,7 @@ use App\Config;
use App\Controller\AbstractLogViewerController;
use App\Entity;
use App\Exception\NotFoundException;
use App\Flysystem\Filesystem;
use App\Flysystem\FilesystemInterface;
use App\Form\BackupSettingsForm;
use App\Form\Form;
use App\Http\Response;
@ -15,6 +15,7 @@ use App\Message\BackupMessage;
use App\Session\Flash;
use App\Sync\Task\RunBackupTask;
use App\Utilities\File;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Messenger\MessageBus;
@ -167,7 +168,7 @@ class BackupsController extends AbstractLogViewerController
): ResponseInterface {
[$path, $fs] = $this->getFile($path);
/** @var Filesystem $fs */
/** @var FilesystemInterface $fs */
return $fs->streamToResponse($response->withNoCache(), $path);
}
@ -177,7 +178,7 @@ class BackupsController extends AbstractLogViewerController
[$path, $fs] = $this->getFile($path);
/** @var Filesystem $fs */
/** @var FilesystemInterface $fs */
$fs->delete($path);
$request->getFlash()->addMessage('<b>' . __('Backup deleted.') . '</b>', Flash::SUCCESS);
@ -187,8 +188,7 @@ class BackupsController extends AbstractLogViewerController
/**
* @param string $rawPath
*
* @return array{0: string, 1: Filesystem}
* @throws NotFoundException
* @return array{0: string, 1: FilesystemInterface}
*/
protected function getFile(string $rawPath): array
{
@ -202,12 +202,12 @@ class BackupsController extends AbstractLogViewerController
if (!($storageLocation instanceof Entity\StorageLocation)) {
throw new \InvalidArgumentException('Invalid storage location.');
throw new InvalidArgumentException('Invalid storage location.');
}
$fs = $storageLocation->getFilesystem();
if (!$fs->has($path)) {
if (!$fs->fileExists($path)) {
throw new NotFoundException(__('Backup not found.'));
}

View File

@ -3,7 +3,7 @@
namespace App\Controller\Api\Stations\Art;
use App\Entity;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use OpenApi\Annotations as OA;
@ -31,7 +31,6 @@ class GetArtAction
*
* @param ServerRequest $request
* @param Response $response
* @param FilesystemManager $filesystem
* @param Entity\Repository\StationRepository $stationRepo
* @param Entity\Repository\StationMediaRepository $mediaRepo
* @param string $media_id
@ -39,17 +38,14 @@ class GetArtAction
public function __invoke(
ServerRequest $request,
Response $response,
FilesystemManager $filesystem,
Entity\Repository\StationRepository $stationRepo,
Entity\Repository\StationMediaRepository $mediaRepo,
string $media_id
): ResponseInterface {
$station = $request->getStation();
$fs = $filesystem->getPrefixedAdapterForStation(
$station,
FilesystemManager::PREFIX_MEDIA,
true
);
$fsStation = new StationFilesystems($station);
$fsMedia = $fsStation->getMediaFilesystem();
$defaultArtRedirect = $response->withRedirect($stationRepo->getDefaultAlbumArtUrl($station), 302);
@ -68,8 +64,8 @@ class GetArtAction
}
}
if ($fs->has($mediaPath)) {
return $fs->streamToResponse($response, $mediaPath, null, 'inline');
if ($fsMedia->fileExists($mediaPath)) {
return $fsMedia->streamToResponse($response, $mediaPath, null, 'inline');
}
return $defaultArtRedirect;

View File

@ -3,7 +3,6 @@
namespace App\Controller\Api\Stations\Art;
use App\Entity;
use App\Flysystem\FilesystemManager;
use App\Http\Response;
use App\Http\ServerRequest;
use Doctrine\ORM\EntityManagerInterface;
@ -15,7 +14,6 @@ class PostArtAction
public function __invoke(
ServerRequest $request,
Response $response,
FilesystemManager $filesystem,
Entity\Repository\StationMediaRepository $mediaRepo,
EntityManagerInterface $em,
$media_id

View File

@ -4,7 +4,8 @@ namespace App\Controller\Api\Stations\Files;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity;
use App\Flysystem\Filesystem;
use App\Flysystem\FilesystemInterface;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Message;
@ -15,6 +16,7 @@ use DoctrineBatchUtils\BatchProcessing\SimpleBatchIteratorAggregate;
use Exception;
use Jhofm\FlysystemIterator\Filter\FilterFactory;
use Jhofm\FlysystemIterator\Options\Options;
use League\Flysystem\StorageAttributes;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Messenger\MessageBus;
use Throwable;
@ -60,27 +62,29 @@ class BatchAction
): ResponseInterface {
$station = $request->getStation();
$storageLocation = $station->getMediaStorageLocation();
$fs = $storageLocation->getFilesystem();
$fsStation = new StationFilesystems($station);
$fsMedia = $fsStation->getMediaFilesystem();
switch ($request->getParam('do')) {
case 'delete':
$result = $this->doDelete($request, $station, $storageLocation, $fs);
$result = $this->doDelete($request, $station, $storageLocation, $fsMedia);
break;
case 'playlist':
$result = $this->doPlaylist($request, $station, $storageLocation, $fs);
$result = $this->doPlaylist($request, $station, $storageLocation, $fsMedia);
break;
case 'move':
$result = $this->doMove($request, $station, $storageLocation, $fs);
$result = $this->doMove($request, $station, $storageLocation, $fsMedia);
break;
case 'queue':
$result = $this->doQueue($request, $station, $storageLocation, $fs);
$result = $this->doQueue($request, $station, $storageLocation, $fsMedia);
break;
case 'reprocess':
$result = $this->doReprocess($request, $station, $storageLocation, $fs);
$result = $this->doReprocess($request, $station, $storageLocation, $fsMedia);
break;
default:
@ -102,7 +106,7 @@ class BatchAction
ServerRequest $request,
Entity\Station $station,
Entity\StorageLocation $storageLocation,
Filesystem $fs
FilesystemInterface $fs
): Entity\Api\BatchResult {
$result = $this->parseRequest($request, $fs, true);
@ -146,7 +150,7 @@ class BatchAction
}
try {
$fs->deleteDir($dir);
$fs->deleteDirectory($dir);
} catch (Throwable $e) {
$result->errors[] = $dir . ': ' . $e->getMessage();
}
@ -163,7 +167,7 @@ class BatchAction
ServerRequest $request,
Entity\Station $station,
Entity\StorageLocation $storageLocation,
Filesystem $fs
FilesystemInterface $fs
): Entity\Api\BatchResult {
$result = $this->parseRequest($request, $fs, true);
@ -253,7 +257,7 @@ class BatchAction
ServerRequest $request,
Entity\Station $station,
Entity\StorageLocation $storageLocation,
Filesystem $fs
FilesystemInterface $fs
): Entity\Api\BatchResult {
$result = $this->parseRequest($request, $fs, false);
@ -272,10 +276,9 @@ class BatchAction
$newPath = File::renameDirectoryInPath($oldPath, $from, $to);
try {
if ($fs->rename($oldPath, $newPath)) {
$record->setPath($newPath);
$this->em->persist($record);
}
$fs->move($oldPath, $newPath);
$record->setPath($newPath);
$this->em->persist($record);
} catch (Throwable $e) {
$result->errors[] = $oldPath . ': ' . $e->getMessage();
}
@ -284,25 +287,24 @@ class BatchAction
foreach ($result->directories as $dirPath) {
$newDirPath = File::renameDirectoryInPath($dirPath, $from, $to);
$fs->move($dirPath, $newDirPath);
if ($fs->rename($dirPath, $newDirPath)) {
$toMove = [
$this->iterateMediaInDirectory($storageLocation, $dirPath),
$this->iterateUnprocessableMediaInDirectory($storageLocation, $dirPath),
$this->iteratePlaylistFoldersInDirectory($station, $dirPath),
];
$toMove = [
$this->iterateMediaInDirectory($storageLocation, $dirPath),
$this->iterateUnprocessableMediaInDirectory($storageLocation, $dirPath),
$this->iteratePlaylistFoldersInDirectory($station, $dirPath),
];
foreach ($toMove as $iterator) {
foreach ($iterator as $record) {
/** @var Entity\PathAwareInterface $record */
try {
$record->setPath(
File::renameDirectoryInPath($record->getPath(), $from, $to)
);
$this->em->persist($record);
} catch (Throwable $e) {
$result->errors[] = $record->getPath() . ': ' . $e->getMessage();
}
foreach ($toMove as $iterator) {
foreach ($iterator as $record) {
/** @var Entity\PathAwareInterface $record */
try {
$record->setPath(
File::renameDirectoryInPath($record->getPath(), $from, $to)
);
$this->em->persist($record);
} catch (Throwable $e) {
$result->errors[] = $record->getPath() . ': ' . $e->getMessage();
}
}
}
@ -315,7 +317,7 @@ class BatchAction
ServerRequest $request,
Entity\Station $station,
Entity\StorageLocation $storageLocation,
Filesystem $fs
FilesystemInterface $fs
): Entity\Api\BatchResult {
$result = $this->parseRequest($request, $fs, true);
@ -340,7 +342,7 @@ class BatchAction
ServerRequest $request,
Entity\Station $station,
Entity\StorageLocation $storageLocation,
Filesystem $fs
FilesystemInterface $fs
): Entity\Api\BatchResult {
$result = $this->parseRequest($request, $fs, true);
@ -388,7 +390,7 @@ class BatchAction
protected function parseRequest(
ServerRequest $request,
Filesystem $fs,
FilesystemInterface $fs,
bool $recursive = false
): Entity\Api\BatchResult {
$files = array_values((array)$request->getParam('files', []));
@ -396,12 +398,10 @@ class BatchAction
if ($recursive) {
foreach ($directories as $dir) {
$dirIterator = $fs->createIterator(
$dir,
[
Options::OPTION_IS_RECURSIVE => true,
Options::OPTION_FILTER => FilterFactory::isFile(),
]
$dirIterator = $fs->listContents($dir, true)->filter(
function (StorageAttributes $attrs) {
return $attrs->isFile();
}
);
foreach ($dirIterator as $subDirMeta) {

View File

@ -3,7 +3,7 @@
namespace App\Controller\Api\Stations\Files;
use App\Entity\Api\Error;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@ -12,22 +12,21 @@ class DownloadAction
{
public function __invoke(
ServerRequest $request,
Response $response,
FilesystemManager $filesystem
Response $response
): ResponseInterface {
set_time_limit(600);
$station = $request->getStation();
$storageLocation = $station->getMediaStorageLocation();
$fs = $storageLocation->getFilesystem();
$fsStation = new StationFilesystems($station);
$fsMedia = $fsStation->getMediaFilesystem();
$path = $request->getParam('file');
if (!$fs->has($path)) {
if (!$fsMedia->fileExists($path)) {
return $response->withStatus(404)
->withJson(new Error(404, 'File not found.'));
}
return $fs->streamToResponse($response, $path);
return $fsMedia->streamToResponse($response, $path);
}
}

View File

@ -4,6 +4,7 @@ namespace App\Controller\Api\Stations\Files;
use App\Entity;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Paginator;
@ -13,6 +14,7 @@ use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\Expr;
use Jhofm\FlysystemIterator\Options\Options;
use League\Flysystem\StorageAttributes;
use Psr\Http\Message\ResponseInterface;
use Psr\SimpleCache\CacheInterface;
@ -23,19 +25,15 @@ class ListAction
Response $response,
EntityManagerInterface $em,
CacheInterface $cache,
FilesystemManager $filesystem,
Entity\Repository\StationRepository $stationRepo
): ResponseInterface {
$station = $request->getStation();
$router = $request->getRouter();
$station = $request->getStation();
$storageLocation = $station->getMediaStorageLocation();
$fs = $filesystem->getFilesystemForAdapter($storageLocation->getStorageAdapter(), true);
$flushCache = (bool)$request->getParam('flushCache', false);
if ($flushCache) {
$fs->clearCache();
}
$fsStation = new StationFilesystems($station);
$fs = $fsStation->getMediaFilesystem();
$currentDir = $request->getParam('currentDirectory', '');
@ -54,6 +52,7 @@ class ListAction
$cacheKey = implode('.', $cacheKeyParts);
$flushCache = (bool)$request->getParam('flushCache', false);
if (!$flushCache && $cache->has($cacheKey)) {
$result = $cache->get($cacheKey);
} else {
@ -244,34 +243,33 @@ class ListAction
$files = array_keys($mediaInDir);
}
} else {
$filesIterator = $fs->createIterator(
$currentDir,
[
Options::OPTION_IS_RECURSIVE => false,
]
);
$protectedPaths = [Entity\StationMedia::DIR_ALBUM_ART, Entity\StationMedia::DIR_WAVEFORMS];
$files = [];
foreach ($filesIterator as $fileRow) {
if ($currentDir === '' && in_array($fileRow['path'], $protectedPaths, true)) {
continue;
$files = $fs->listContents($currentDir, false)->filter(
function (StorageAttributes $attributes) use ($currentDir, $protectedPaths) {
return !($currentDir === '' && in_array($attributes->path(), $protectedPaths, true));
}
$files[] = $fileRow['path'];
}
);
}
foreach ($files as $path) {
$meta = $fs->getMetadata($path);
foreach ($files as $file) {
$row = new Entity\Api\FileList();
$row->path = $path;
if ($file instanceof StorageAttributes) {
$row->path = $file->path();
$row->timestamp = $file->lastModified() ?? 0;
$row->is_dir = $file->isDir();
} else {
$row->path = $file;
$row->timestamp = $fs->lastModified($file) ?? 0;
$row->is_dir = false;
}
$row->size = ($row->is_dir) ? 0 : $fs->fileSize($row->path);
$shortname = (!empty($searchPhrase))
? $path
: basename($path);
? $row->path
: basename($row->path);
$max_length = 60;
if (mb_strlen($shortname) > $max_length) {
@ -279,26 +277,22 @@ class ListAction
}
$row->path_short = $shortname;
$row->timestamp = $meta['timestamp'] ?? 0;
$row->size = $meta['size'];
$row->is_dir = ('dir' === $meta['type']);
$row->media = new Entity\Api\FileListMedia();
if (isset($mediaInDir[$path])) {
$row->media = $mediaInDir[$path]['media'];
if (isset($mediaInDir[$row->path])) {
$row->media = $mediaInDir[$row->path]['media'];
$row->text = $row->media->text;
$row->playlists = (array)$mediaInDir[$path]['playlists'];
} elseif ('dir' === $meta['type']) {
$row->playlists = (array)$mediaInDir[$row->path]['playlists'];
} elseif ($row->is_dir) {
$row->text = __('Directory');
if (isset($foldersInDir[$path])) {
$row->playlists = (array)$foldersInDir[$path]['playlists'];
if (isset($foldersInDir[$row->path])) {
$row->playlists = (array)$foldersInDir[$row->path]['playlists'];
}
} elseif (isset($unprocessableMedia[$path])) {
} elseif (isset($unprocessableMedia[$row->path])) {
$row->text = __(
'File Not Processed: %s',
Utilities\Strings::truncateText($unprocessableMedia[$path])
Utilities\Strings::truncateText($unprocessableMedia[$row->path])
);
} else {
$row->text = __('File Processing');
@ -308,12 +302,12 @@ class ListAction
'download' => (string)$router->named(
'api:stations:files:download',
['station_id' => $station->getId()],
['file' => $path]
['file' => $row->path]
),
'rename' => (string)$router->named(
'api:stations:files:rename',
['station_id' => $station->getId()],
['file' => $path]
['file' => $row->path]
),
];

View File

@ -3,49 +3,56 @@
namespace App\Controller\Api\Stations\Files;
use App\Entity;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use League\Flysystem\StorageAttributes;
use Psr\Http\Message\ResponseInterface;
class ListDirectoriesAction
{
public function __invoke(
ServerRequest $request,
Response $response,
FilesystemManager $filesystem
Response $response
): ResponseInterface {
$station = $request->getStation();
$fs = $filesystem->getPrefixedAdapterForStation($station, FilesystemManager::PREFIX_MEDIA, true);
$currentDir = $request->getParam('currentDirectory', '');
if (!empty($currentDir)) {
$dirMeta = $fs->getMetadata($currentDir);
if ('dir' !== $dirMeta['type']) {
return $response->withStatus(500)
->withJson(new Entity\Api\Error(500, __('Path "%s" is not a folder.', $currentDir)));
}
}
$fsStation = new StationFilesystems($station);
$fsMedia = $fsStation->getMediaFilesystem();
$protectedPaths = [Entity\StationMedia::DIR_ALBUM_ART, Entity\StationMedia::DIR_WAVEFORMS];
$directories = array_filter(array_map(function ($file) use ($protectedPaths) {
if ('dir' !== $file['type']) {
return null;
}
$directoriesRaw = $fsMedia->listContents($currentDir, false)->filter(
function (StorageAttributes $attrs) use ($protectedPaths) {
if (!$attrs->isDir()) {
return false;
}
if (in_array($file['path'], $protectedPaths, true)) {
return null;
}
if (in_array($attrs->path(), $protectedPaths, true)) {
return false;
}
return [
'name' => $file['basename'],
'path' => $file['path'],
return true;
}
);
$directories = [];
foreach ($directoriesRaw as $directory) {
/** @var StorageAttributes $directory */
$path = $directory->path();
$directories[] = [
'name' => basename($path),
'path' => $path,
];
}, $fs->listContents($currentDir)));
}
return $response->withJson([
'rows' => array_values($directories),
]);
return $response->withJson(
[
'rows' => $directories,
]
);
}
}

View File

@ -4,16 +4,17 @@ namespace App\Controller\Api\Stations\Files;
use App\Entity;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use SebastianBergmann\CodeCoverage\DirectoryCouldNotBeCreatedException;
class MakeDirectoryAction
{
public function __invoke(
ServerRequest $request,
Response $response,
FilesystemManager $filesystem
Response $response
): ResponseInterface {
$currentDir = $request->getParam('currentDirectory', '');
$newDirName = $request->getParam('name', '');
@ -24,12 +25,17 @@ class MakeDirectoryAction
}
$station = $request->getStation();
$fs = $filesystem->getPrefixedAdapterForStation($station, FilesystemManager::PREFIX_MEDIA, true);
$fsStation = new StationFilesystems($station);
$fsMedia = $fsStation->getMediaFilesystem();
$newDir = $currentDir . '/' . $newDirName;
if (!$fs->createDir($newDir)) {
return $response->withStatus(403)
->withJson(new Entity\Api\Error(403, __('Directory "%s" was not created', $newDir)));
try {
$fsMedia->createDirectory($newDir);
} catch (DirectoryCouldNotBeCreatedException $e) {
return $response->withStatus(400)
->withJson(new Entity\Api\Error(400, $e->getMessage()));
}
return $response->withJson(new Entity\Api\Status());

View File

@ -3,7 +3,7 @@
namespace App\Controller\Api\Stations\Files;
use App\Entity;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@ -14,7 +14,6 @@ class PlayAction
ServerRequest $request,
Response $response,
int $id,
FilesystemManager $filesystem,
Entity\Repository\StationMediaRepository $mediaRepo
): ResponseInterface {
set_time_limit(600);
@ -28,8 +27,9 @@ class PlayAction
->withJson(new Entity\Api\Error(404, 'Not Found'));
}
$fs = $filesystem->getForStation($station, false);
$fsStation = new StationFilesystems($station);
$fsMedia = $fsStation->getMediaFilesystem();
return $fs->streamToResponse($response, $media->getPathUri());
return $fsMedia->streamToResponse($response, $media->getPath());
}
}

View File

@ -3,6 +3,7 @@
namespace App\Controller\Api\Stations\Files;
use App\Entity;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Utilities\File;
@ -33,43 +34,45 @@ class RenameAction extends BatchAction
$station = $request->getStation();
$storageLocation = $station->getMediaStorageLocation();
$fs = $storageLocation->getFilesystem();
if ($fs->rename($from, $to)) {
$pathMeta = $fs->getMetadata($to);
$fsStation = new StationFilesystems($station);
$fsMedia = $fsStation->getMediaFilesystem();
if ('dir' === $pathMeta['type']) {
// Update the paths of all media contained within the directory.
$toRename = [
$this->iterateMediaInDirectory($storageLocation, $from),
$this->iterateUnprocessableMediaInDirectory($storageLocation, $from),
$this->iteratePlaylistFoldersInDirectory($station, $from),
];
$fsMedia->move($from, $to);
foreach ($toRename as $iterator) {
foreach ($iterator as $record) {
/** @var Entity\PathAwareInterface $record */
$record->setPath(
File::renameDirectoryInPath($record->getPath(), $from, $to)
);
$this->em->persist($record);
}
$pathMeta = $fsMedia->getMetadata($to);
if ($pathMeta->isDir()) {
// Update the paths of all media contained within the directory.
$toRename = [
$this->iterateMediaInDirectory($storageLocation, $from),
$this->iterateUnprocessableMediaInDirectory($storageLocation, $from),
$this->iteratePlaylistFoldersInDirectory($station, $from),
];
foreach ($toRename as $iterator) {
foreach ($iterator as $record) {
/** @var Entity\PathAwareInterface $record */
$record->setPath(
File::renameDirectoryInPath($record->getPath(), $from, $to)
);
$this->em->persist($record);
}
} else {
$record = $this->mediaRepo->findByPath($from, $storageLocation);
}
} else {
$record = $this->mediaRepo->findByPath($from, $storageLocation);
if ($record instanceof Entity\StationMedia) {
if ($record instanceof Entity\StationMedia) {
$record->setPath($to);
$this->em->persist($record);
$this->em->flush();
} else {
$record = $this->unprocessableMediaRepo->findByPath($from, $storageLocation);
if ($record instanceof Entity\UnprocessableMedia) {
$record->setPath($to);
$this->em->persist($record);
$this->em->flush();
} else {
$record = $this->unprocessableMediaRepo->findByPath($from, $storageLocation);
if ($record instanceof Entity\UnprocessableMedia) {
$record->setPath($to);
$this->em->persist($record);
$this->em->flush();
}
}
}
}

View File

@ -4,7 +4,7 @@ namespace App\Controller\Api\Stations;
use App\Entity;
use App\Exception\ValidationException;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Message\WritePlaylistFileMessage;
@ -24,8 +24,6 @@ class FilesController extends AbstractStationApiCrudController
protected string $entityClass = Entity\StationMedia::class;
protected string $resourceRouteName = 'api:stations:file';
protected FilesystemManager $filesystem;
protected Adapters $adapters;
protected MessageBus $messageBus;
@ -40,7 +38,6 @@ class FilesController extends AbstractStationApiCrudController
EntityManagerInterface $em,
Serializer $serializer,
ValidatorInterface $validator,
FilesystemManager $filesystem,
Adapters $adapters,
MessageBus $messageBus,
Entity\Repository\CustomFieldRepository $customFieldsRepo,
@ -49,7 +46,6 @@ class FilesController extends AbstractStationApiCrudController
) {
parent::__construct($em, $serializer, $validator);
$this->filesystem = $filesystem;
$this->adapters = $adapters;
$this->messageBus = $messageBus;
@ -190,10 +186,8 @@ class FilesController extends AbstractStationApiCrudController
$temp_path = $station->getRadioTempDir() . '/' . $api_record->getSanitizedFilename();
file_put_contents($temp_path, $api_record->getFileContents());
$sanitized_path = FilesystemManager::PREFIX_MEDIA . '://' . $api_record->getSanitizedPath();
// Process temp path as regular media record.
$record = $this->mediaRepo->getOrCreate($station, $sanitized_path, $temp_path);
$record = $this->mediaRepo->getOrCreate($station, $api_record->getSanitizedPath(), $temp_path);
$return = $this->viewRecord($record, $request);
@ -219,16 +213,18 @@ class FilesController extends AbstractStationApiCrudController
$playlists = $data['playlists'] ?? null;
unset($data['custom_fields'], $data['playlists']);
$fsStation = new StationFilesystems($station);
$fsMedia = $fsStation->getMediaFilesystem();
$record = $this->fromArray(
$data,
$record,
[
AbstractNormalizer::CALLBACKS => [
'path' => function ($new_value, $record) {
'path' => function ($new_value, $record) use ($fsMedia) {
// Detect and handle a rename.
if (($record instanceof Entity\StationMedia) && $new_value !== $record->getPath()) {
$fs = $record->getStorageLocation()->getFilesystem();
$fs->rename($record->getPath(), $new_value);
$fsMedia->move($record->getPath(), $new_value);
}
return $new_value;

View File

@ -3,7 +3,7 @@
namespace App\Controller\Api\Stations\OnDemand;
use App\Entity;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@ -14,8 +14,7 @@ class DownloadAction
ServerRequest $request,
Response $response,
string $media_id,
Entity\Repository\StationMediaRepository $mediaRepo,
FilesystemManager $filesystem
Entity\Repository\StationMediaRepository $mediaRepo
): ResponseInterface {
$station = $request->getStation();
@ -32,10 +31,10 @@ class DownloadAction
->withJson(new Entity\Api\Error(404, __('File not found.')));
}
$filePath = $media->getPathUri();
$fs = $filesystem->getForStation($station);
$fsStation = new StationFilesystems($station);
$fsMedia = $fsStation->getMediaFilesystem();
set_time_limit(600);
return $fs->streamToResponse($response, $filePath);
return $fsMedia->streamToResponse($response, $media->getPath());
}
}

View File

@ -4,7 +4,7 @@ namespace App\Controller\Api\Stations\Streamers;
use App\Controller\Api\AbstractApiCrudController;
use App\Entity;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Paginator;
@ -19,14 +19,12 @@ class BroadcastsController extends AbstractApiCrudController
/**
* @param ServerRequest $request
* @param Response $response
* @param FilesystemManager $filesystem
* @param string|int $station_id
* @param int $id
*/
public function listAction(
ServerRequest $request,
Response $response,
FilesystemManager $filesystem,
$station_id,
$id
): ResponseInterface {
@ -53,24 +51,22 @@ class BroadcastsController extends AbstractApiCrudController
$is_bootgrid = $paginator->isFromBootgrid();
$router = $request->getRouter();
$fs = $filesystem->getForStation($station);
$fsStation = new StationFilesystems($station);
$fsRecordings = $fsStation->getRecordingsFilesystem();
$paginator->setPostprocessor(
function ($row) use ($is_bootgrid, $router, $fs) {
function ($row) use ($is_bootgrid, $router, $fsRecordings) {
/** @var Entity\StationStreamerBroadcast $row */
$return = $this->toArray($row);
unset($return['recordingPath']);
$recordingPath = $row->getRecordingPath();
$recordingUri = FilesystemManager::PREFIX_RECORDINGS . '://' . $recordingPath;
if ($fs->has($recordingUri)) {
$recordingMeta = $fs->getMetadata($recordingUri);
if ($fsRecordings->fileExists($recordingPath)) {
$return['recording'] = [
'path' => $recordingPath,
'size' => $recordingMeta['size'],
'size' => $fsRecordings->fileSize($recordingPath),
'links' => [
'download' => $router->fromHere(
'api:stations:streamer:broadcast:download',
@ -104,7 +100,6 @@ class BroadcastsController extends AbstractApiCrudController
/**
* @param ServerRequest $request
* @param Response $response
* @param FilesystemManager $filesystem
* @param string|int $station_id
* @param int $id
* @param int $broadcast_id
@ -112,7 +107,6 @@ class BroadcastsController extends AbstractApiCrudController
public function downloadAction(
ServerRequest $request,
Response $response,
FilesystemManager $filesystem,
$station_id,
$id,
$broadcast_id
@ -132,12 +126,12 @@ class BroadcastsController extends AbstractApiCrudController
->withJson(new Entity\Api\Error(400, __('No recording available.')));
}
$fs = $filesystem->getForStation($station);
$filename = basename($recordingPath);
$recordingPath = FilesystemManager::PREFIX_RECORDINGS . '://' . $recordingPath;
$fsStation = new StationFilesystems($station);
$fsRecordings = $fsStation->getRecordingsFilesystem();
return $fs->streamToResponse(
return $fsRecordings->streamToResponse(
$response,
$recordingPath,
File::sanitizeFileName($broadcast->getStreamer()->getDisplayName()) . '_' . $filename
@ -147,7 +141,6 @@ class BroadcastsController extends AbstractApiCrudController
public function deleteAction(
ServerRequest $request,
Response $response,
FilesystemManager $filesystem,
$station_id,
$id,
$broadcast_id
@ -163,10 +156,10 @@ class BroadcastsController extends AbstractApiCrudController
$recordingPath = $broadcast->getRecordingPath();
if (!empty($recordingPath)) {
$fs = $filesystem->getForStation($station);
$recordingPath = FilesystemManager::PREFIX_RECORDINGS . '://' . $recordingPath;
$fsStation = new StationFilesystems($station);
$fsRecordings = $fsStation->getRecordingsFilesystem();
$fs->delete($recordingPath);
$fsRecordings->delete($recordingPath);
$broadcast->clearRecordingPath();
$this->em->persist($broadcast);

View File

@ -5,7 +5,7 @@ namespace App\Controller\Api\Stations\Waveform;
use App\Entity\Api\Error;
use App\Entity\Repository\StationMediaRepository;
use App\Entity\StationMedia;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@ -15,22 +15,23 @@ class GetWaveformAction
public function __invoke(
ServerRequest $request,
Response $response,
FilesystemManager $filesystem,
StationMediaRepository $mediaRepo,
$media_id
): ResponseInterface {
$response = $response->withCacheLifetime(Response::CACHE_ONE_YEAR);
$station = $request->getStation();
$fs = $filesystem->getForStation($station);
$fsStation = new StationFilesystems($station);
$fsMedia = $fsStation->getMediaFilesystem();
// If a timestamp delimiter is added, strip it automatically.
$media_id = explode('-', $media_id)[0];
if (StationMedia::UNIQUE_ID_LENGTH === strlen($media_id)) {
$waveformUri = StationMedia::getWaveformUri($media_id);
if ($fs->has($waveformUri)) {
return $fs->streamToResponse($response, $waveformUri, null, 'inline');
$waveformPath = StationMedia::getWaveformPath($media_id);
if ($fsMedia->fileExists($waveformPath)) {
return $fsMedia->streamToResponse($response, $waveformPath, null, 'inline');
}
}
@ -39,11 +40,11 @@ class GetWaveformAction
return $response->withStatus(500)->withJson(new Error(500, 'Media not found.'));
}
$waveformUri = StationMedia::getWaveformUri($media->getUniqueId());
if (!$fs->has($waveformUri)) {
$waveformPath = StationMedia::getWaveformPath($media->getUniqueId());
if (!$fsMedia->fileExists($waveformPath)) {
$mediaRepo->updateWaveform($media);
}
return $fs->streamToResponse($response, $waveformUri, null, 'inline');
return $fsMedia->streamToResponse($response, $waveformPath, null, 'inline');
}
}

View File

@ -44,7 +44,7 @@ class StationMedia extends AbstractFixture implements DependentFixtureInterface
$fileBaseName = basename($filePath);
// Copy the file to the station media directory.
$fs->copyFromLocal($filePath, '/' . $fileBaseName);
$fs->upload($filePath, '/' . $fileBaseName);
$mediaRow = $this->mediaRepo->getOrCreate($mediaStorage, $fileBaseName);
$em->persist($mediaRow);

View File

@ -8,8 +8,7 @@ use App\Entity;
use App\Entity\StationPlaylist;
use App\Environment;
use App\Exception\CannotProcessMediaException;
use App\Flysystem\Filesystem;
use App\Flysystem\FilesystemManager;
use App\Flysystem\FilesystemInterface;
use App\Media\MetadataManager;
use App\Service\AudioWaveform;
use Exception;
@ -17,7 +16,7 @@ use Generator;
use Intervention\Image\Constraint;
use Intervention\Image\ImageManager;
use InvalidArgumentException;
use League\Flysystem\FileNotFoundException;
use League\Flysystem\FilesystemException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\Serializer;
@ -37,8 +36,6 @@ class StationMediaRepository extends Repository
protected MetadataManager $metadataManager;
protected FilesystemManager $filesystem;
protected ImageManager $imageManager;
public function __construct(
@ -51,7 +48,6 @@ class StationMediaRepository extends Repository
StationPlaylistMediaRepository $spmRepo,
StorageLocationRepository $storageLocationRepo,
UnprocessableMediaRepository $unprocessableMediaRepo,
FilesystemManager $filesystem,
ImageManager $imageManager
) {
parent::__construct($em, $serializer, $environment, $logger);
@ -62,7 +58,6 @@ class StationMediaRepository extends Repository
$this->unprocessableMediaRepo = $unprocessableMediaRepo;
$this->metadataManager = $metadataManager;
$this->filesystem = $filesystem;
$this->imageManager = $imageManager;
}
@ -173,8 +168,6 @@ class StationMediaRepository extends Repository
string $path,
?string $uploadedFrom = null
): Entity\StationMedia {
$path = FilesystemManager::stripPrefix($path);
$record = $this->findByPath($path, $source);
$storageLocation = $this->getStorageLocation($source);
@ -226,16 +219,16 @@ class StationMediaRepository extends Repository
try {
$this->loadFromFile($media, $uploadedPath, $fs);
} finally {
$fs->putFromLocal($uploadedPath, $path);
$fs->uploadAndDeleteOriginal($uploadedPath, $path);
}
$mediaMtime = time();
} else {
if (!$fs->has($path)) {
if (!$fs->fileExists($path)) {
throw new CannotProcessMediaException(sprintf('Media path "%s" not found.', $path));
}
$mediaMtime = (int)$fs->getTimestamp($path);
$mediaMtime = $fs->lastModified($path);
// No need to update if all of these conditions are true.
if (!$force && !$media->needsReprocessing($mediaMtime)) {
@ -261,12 +254,12 @@ class StationMediaRepository extends Repository
*
* @param Entity\StationMedia $media
* @param string $filePath
* @param Filesystem|null $fs
* @param FilesystemInterface|null $fs
*/
public function loadFromFile(
Entity\StationMedia $media,
string $filePath,
?Filesystem $fs = null
?FilesystemInterface $fs = null
): void {
// Load metadata from supported files.
$metadata = $this->metadataManager->getMetadata($media, $filePath);
@ -344,8 +337,8 @@ class StationMediaRepository extends Repository
public function writeAlbumArt(
Entity\StationMedia $media,
string $rawArtString,
?Filesystem $fs = null
): bool {
?FilesystemInterface $fs = null
): void {
$fs ??= $this->getFilesystem($media);
$media->setArtUpdatedAt(time());
@ -363,12 +356,12 @@ class StationMediaRepository extends Repository
$albumArtPath = Entity\StationMedia::getArtPath($media->getUniqueId());
$albumArtStream = $albumArt->stream('jpg', 90);
return $fs->putStream($albumArtPath, $albumArtStream->detach());
$fs->writeStream($albumArtPath, $albumArtStream->detach());
}
public function removeAlbumArt(
Entity\StationMedia $media,
?Filesystem $fs = null
?FilesystemInterface $fs = null
): void {
$fs ??= $this->getFilesystem($media);
@ -384,14 +377,14 @@ class StationMediaRepository extends Repository
public function writeToFile(
Entity\StationMedia $media,
?Filesystem $fs = null
?FilesystemInterface $fs = null
): bool {
$fs ??= $this->getFilesystem($media);
$metadata = $media->toMetadata();
$art_path = Entity\StationMedia::getArtPath($media->getUniqueId());
if ($fs->has($art_path)) {
if ($fs->fileExists($art_path)) {
$metadata->setArtwork($fs->read($art_path));
}
@ -414,7 +407,7 @@ class StationMediaRepository extends Repository
public function updateWaveform(
Entity\StationMedia $media,
?Filesystem $fs = null
?FilesystemInterface $fs = null
): void {
$fs ??= $this->getFilesystem($media);
$fs->withLocalFile(
@ -428,14 +421,14 @@ class StationMediaRepository extends Repository
public function writeWaveform(
Entity\StationMedia $media,
string $path,
?Filesystem $fs = null
): bool {
?FilesystemInterface $fs = null
): void {
$fs ??= $this->getFilesystem($media);
$waveform = AudioWaveform::getWaveformFor($path);
$waveformPath = Entity\StationMedia::getWaveformPath($media->getUniqueId());
return $fs->put(
$fs->write(
$waveformPath,
json_encode(
$waveform,
@ -444,30 +437,17 @@ class StationMediaRepository extends Repository
);
}
/**
* Return the full path associated with a media entity.
*
* @param Entity\StationMedia $media
*/
public function getFullPath(Entity\StationMedia $media): string
{
$fs = $this->getFilesystem($media);
$uri = $media->getPathUri();
return $fs->getFullPath($uri);
}
/**
* @param Entity\StationMedia $media
* @param bool $deleteFile Whether to remove the media file itself (disabled for batch operations).
* @param Filesystem|null $fs
* @param FilesystemInterface|null $fs
*
* @return StationPlaylist[] The IDs as keys and records as values for all affected playlists.
*/
public function remove(
Entity\StationMedia $media,
bool $deleteFile = false,
?Filesystem $fs = null
?FilesystemInterface $fs = null
): array {
$fs ??= $this->getFilesystem($media);
@ -475,7 +455,7 @@ class StationMediaRepository extends Repository
foreach ($media->getRelatedFilePaths() as $relatedFilePath) {
try {
$fs->delete($relatedFilePath);
} catch (FileNotFoundException $e) {
} catch (FilesystemException $e) {
// Skip
}
}
@ -483,7 +463,7 @@ class StationMediaRepository extends Repository
if ($deleteFile) {
try {
$fs->delete($media->getPath());
} catch (FileNotFoundException $e) {
} catch (FilesystemException $e) {
// Skip
}
}
@ -496,11 +476,8 @@ class StationMediaRepository extends Repository
return $affectedPlaylists;
}
protected function getFilesystem(Entity\StationMedia $media, bool $cached = true): Filesystem
protected function getFilesystem(Entity\StationMedia $media): FilesystemInterface
{
return $this->filesystem->getFilesystemForAdapter(
$media->getStorageLocation()->getStorageAdapter(),
$cached
);
return $media->getStorageLocation()->getFilesystem();
}
}

View File

@ -7,6 +7,7 @@ use App\Doctrine\Repository;
use App\Entity;
use App\Environment;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Radio\Adapters;
use App\Radio\AutoDJ\Scheduler;
use Psr\Log\LoggerInterface;
@ -18,22 +19,18 @@ class StationStreamerRepository extends Repository
protected StationStreamerBroadcastRepository $broadcastRepo;
protected FilesystemManager $filesystem;
public function __construct(
ReloadableEntityManagerInterface $em,
Serializer $serializer,
Environment $environment,
LoggerInterface $logger,
Scheduler $scheduler,
StationStreamerBroadcastRepository $broadcastRepo,
FilesystemManager $filesystem
StationStreamerBroadcastRepository $broadcastRepo
) {
parent::__construct($em, $serializer, $environment, $logger);
$this->scheduler = $scheduler;
$this->broadcastRepo = $broadcastRepo;
$this->filesystem = $filesystem;
}
/**
@ -94,8 +91,10 @@ class StationStreamerRepository extends Repository
$this->em->persist($record);
$this->em->flush();
$fs = $this->filesystem->getForStation($station);
return $fs->getFullPath(FilesystemManager::PREFIX_TEMP . '://' . $recordingPath);
$fsStations = new StationFilesystems($station);
$fsTemp = $fsStations->getTempFilesystem();
return $fsTemp->getLocalPath($recordingPath);
}
}
@ -105,17 +104,17 @@ class StationStreamerRepository extends Repository
public function onDisconnect(Entity\Station $station): bool
{
$fs = $this->filesystem->getForStation($station);
$fs = new StationFilesystems($station);
$fsTemp = $fs->getTempFilesystem();
$fsRecordings = $fs->getRecordingsFilesystem();
$broadcasts = $this->broadcastRepo->getActiveBroadcasts($station);
foreach ($broadcasts as $broadcast) {
$broadcastPath = $broadcast->getRecordingPath();
$tempPath = FilesystemManager::PREFIX_TEMP . '://' . $broadcastPath;
$destPath = FilesystemManager::PREFIX_RECORDINGS . '://' . $broadcastPath;
if ($fs->has($tempPath)) {
$fs->move($tempPath, $destPath);
if ($fsTemp->fileExists($broadcastPath)) {
$tempPath = $fsTemp->getLocalPath($broadcastPath);
$fsRecordings->uploadAndDeleteOriginal($tempPath, $broadcastPath);
}
$broadcast->setTimestampEnd(time());

View File

@ -14,8 +14,7 @@ use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use League\Flysystem\Adapter\Local;
use League\Flysystem\AdapterInterface;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use OpenApi\Annotations as OA;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
@ -640,17 +639,20 @@ class Station
}
// Flysystem adapters will automatically create the main directory.
$this->getRadioBaseDirAdapter();
$this->getRadioPlaylistsDirAdapter();
$this->getRadioConfigDirAdapter();
$this->getRadioTempDirAdapter();
$this->ensureDirectoryExists($this->getRadioBaseDir());
$this->ensureDirectoryExists($this->getRadioPlaylistsDir());
$this->ensureDirectoryExists($this->getRadioConfigDir());
$this->ensureDirectoryExists($this->getRadioTempDir());
if (null === $this->media_storage_location) {
$storageLocation = new StorageLocation(
StorageLocation::TYPE_STATION_MEDIA,
StorageLocation::ADAPTER_LOCAL
);
$storageLocation->setPath($this->getRadioBaseDir() . '/media');
$mediaPath = $this->getRadioBaseDir() . '/media';
$this->ensureDirectoryExists($mediaPath);
$storageLocation->setPath($mediaPath);
$this->media_storage_location = $storageLocation;
}
@ -660,19 +662,28 @@ class Station
StorageLocation::TYPE_STATION_RECORDINGS,
StorageLocation::ADAPTER_LOCAL
);
$storageLocation->setPath($this->getRadioBaseDir() . '/recordings');
$recordingsPath = $this->getRadioBaseDir() . '/recordings';
$this->ensureDirectoryExists($recordingsPath);
$storageLocation->setPath($recordingsPath);
$this->recordings_storage_location = $storageLocation;
}
$this->getRadioMediaDirAdapter();
$this->getRadioRecordingsDirAdapter();
}
public function getRadioBaseDirAdapter(?string $suffix = null): AdapterInterface
protected function ensureDirectoryExists(string $dirname): void
{
$path = $this->radio_base_dir . $suffix;
return new Local($path);
if (is_dir($dirname)) {
return;
}
$visibilityConverter = new PortableVisibilityConverter();
$visibility = $visibilityConverter->defaultForDirectories();
if (!mkdir($dirname, $visibility, true) && !is_dir($dirname)) {
throw new \RuntimeException(sprintf('Directory "%s" was not created', $dirname));
}
clearstatcache(false, $dirname);
}
public function getRadioPlaylistsDir(): string
@ -680,41 +691,16 @@ class Station
return $this->radio_base_dir . '/playlists';
}
public function getRadioPlaylistsDirAdapter(): AdapterInterface
{
return new Local($this->getRadioPlaylistsDir());
}
public function getRadioConfigDir(): string
{
return $this->radio_base_dir . '/config';
}
public function getRadioConfigDirAdapter(): AdapterInterface
{
return new Local($this->getRadioConfigDir());
}
public function getRadioTempDir(): string
{
return $this->radio_base_dir . '/temp';
}
public function getRadioTempDirAdapter(): AdapterInterface
{
return new Local($this->getRadioTempDir());
}
public function getRadioMediaDirAdapter(): AdapterInterface
{
return $this->getMediaStorageLocation()->getStorageAdapter();
}
public function getRadioRecordingsDirAdapter(): AdapterInterface
{
return $this->getRecordingsStorageLocation()->getStorageAdapter();
}
public function getNowplaying(): ?Api\NowPlaying
{
if ($this->nowplaying instanceof Api\NowPlaying) {

View File

@ -329,17 +329,6 @@ class StationMedia implements SongInterface, ProcessableMediaInterface, PathAwar
$this->path = $path;
}
/**
* Return the abstracted "full path" filesystem URI for this record.
*/
public function getPathUri(): string
{
return FilesystemManager::applyPrefix(
FilesystemManager::PREFIX_MEDIA,
$this->path
);
}
public function getMtime(): ?int
{
return $this->mtime;
@ -589,24 +578,8 @@ class StationMedia implements SongInterface, ProcessableMediaInterface, PathAwar
return self::DIR_ALBUM_ART . '/' . $uniqueId . '.jpg';
}
public static function getArtUri(string $uniqueId): string
{
return FilesystemManager::applyPrefix(
FilesystemManager::PREFIX_MEDIA,
self::getArtPath($uniqueId)
);
}
public static function getWaveformPath(string $uniqueId): string
{
return self::DIR_WAVEFORMS . '/' . $uniqueId . '.json';
}
public static function getWaveformUri(string $uniqueId): string
{
return FilesystemManager::applyPrefix(
FilesystemManager::PREFIX_MEDIA,
self::getWaveformPath($uniqueId)
);
}
}

View File

@ -5,7 +5,13 @@
namespace App\Entity;
use App\Annotations\AuditLog;
use App\Flysystem\Filesystem;
use App\Flysystem\Adapter\AdapterInterface;
use App\Flysystem\Adapter\AwsS3Adapter;
use App\Flysystem\Adapter\DropboxAdapter;
use App\Flysystem\Adapter\LocalAdapter;
use App\Flysystem\FilesystemInterface;
use App\Flysystem\LocalFilesystem;
use App\Flysystem\RemoteFilesystem;
use App\Radio\Quota;
use App\Validator\Constraints as AppAssert;
use Aws\S3\S3Client;
@ -13,13 +19,7 @@ use Brick\Math\BigInteger;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use League\Flysystem\Adapter\Local;
use League\Flysystem\AdapterInterface;
use League\Flysystem\AwsS3v3\AwsS3Adapter;
use League\Flysystem\Config;
use League\Flysystem\Util;
use Spatie\Dropbox\Client;
use Spatie\FlysystemDropbox\DropboxAdapter;
use Symfony\Component\Validator\Constraints as Assert;
/**
@ -391,7 +391,9 @@ class StorageLocation
}
return $quota;
} elseif (null !== $quota) {
}
if (null !== $quota) {
return $quota;
}
@ -472,7 +474,7 @@ class StorageLocation
}
$adapter = $this->getStorageAdapter();
$adapter->has('/test');
$adapter->fileExists('/test');
}
public function getStorageAdapter(): AdapterInterface
@ -487,7 +489,7 @@ class StorageLocation
case self::ADAPTER_LOCAL:
default:
return new Local($this->path);
return new LocalAdapter($this->path);
}
}
@ -520,18 +522,13 @@ class StorageLocation
return new Client($this->dropboxAuthToken);
}
/**
* @param Config|array|null $config
*
*/
public function getFilesystem($config = null): Filesystem
public function getFilesystem(): FilesystemInterface
{
$config = Util::ensureConfig($config);
if (self::ADAPTER_DROPBOX === $this->adapter) {
$config->set('case_sensitive', false);
}
$adapter = $this->getStorageAdapter();
return new Filesystem($this->getStorageAdapter(), $config);
return ($adapter instanceof LocalAdapter)
? new LocalFilesystem($adapter)
: new RemoteFilesystem($adapter);
}
public function __toString(): string

View File

@ -0,0 +1,83 @@
<?php
namespace App\Flysystem;
use App\Flysystem\Adapter\AdapterInterface;
use App\Http\Response;
use League\Flysystem\Filesystem;
use League\Flysystem\PathNormalizer;
use League\Flysystem\StorageAttributes;
use League\MimeTypeDetection\FinfoMimeTypeDetector;
use Psr\Http\Message\ResponseInterface;
abstract class AbstractFilesystem extends Filesystem implements FilesystemInterface
{
protected AdapterInterface $adapter;
public function __construct(AdapterInterface $adapter, array $config = [], PathNormalizer $pathNormalizer = null)
{
$this->adapter = $adapter;
parent::__construct($adapter, $config, $pathNormalizer);
}
public function getAdapter(): AdapterInterface
{
return $this->adapter;
}
public function getMetadata(string $path): StorageAttributes
{
return $this->adapter->getMetadata($path);
}
public function uploadAndDeleteOriginal(string $localPath, string $to): void
{
$this->upload($localPath, $to);
@unlink($localPath);
}
public function streamToResponse(
Response $response,
string $path,
string $fileName = null,
string $disposition = 'attachment'
): ResponseInterface {
$localPath = $this->getLocalPath($path);
$mime = new FinfoMimeTypeDetector();
$mimeType = $mime->detectMimeTypeFromFile($localPath);
$fileName ??= basename($localPath);
if ('attachment' === $disposition) {
/*
* The regex used below is to ensure that the $fileName contains only
* characters ranging from ASCII 128-255 and ASCII 0-31 and 127 are replaced with an empty string
*/
$disposition .= '; filename="' . preg_replace('/[\x00-\x1F\x7F\"]/', ' ', $fileName) . '"';
$disposition .= "; filename*=UTF-8''" . rawurlencode($fileName);
}
$response = $response->withHeader('Content-Disposition', $disposition)
->withHeader('Content-Length', filesize($localPath))
->withHeader('X-Accel-Buffering', 'no');
// Special internal nginx routes to use X-Accel-Redirect for far more performant file serving.
$specialPaths = [
'/var/azuracast/backups' => '/internal/backups',
'/var/azuracast/stations' => '/internal/stations',
];
foreach ($specialPaths as $diskPath => $nginxPath) {
if (0 === strpos($localPath, $diskPath)) {
$accelPath = str_replace($diskPath, $nginxPath, $localPath);
return $response->withHeader('Content-Type', $mimeType)
->withHeader('X-Accel-Redirect', $accelPath);
}
}
return $response->withFile($localPath, $mimeType);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Flysystem\Adapter;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\StorageAttributes;
interface AdapterInterface extends FilesystemAdapter
{
/**
* @param string $path
*
*/
public function getMetadata(string $path): StorageAttributes;
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Flysystem\Adapter;
use Aws\Api\DateTimeResult;
use Aws\S3\S3ClientInterface;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
use League\Flysystem\AwsS3V3\VisibilityConverter;
use League\Flysystem\DirectoryAttributes;
use League\Flysystem\FileAttributes;
use League\Flysystem\PathPrefixer;
use League\Flysystem\StorageAttributes;
use League\Flysystem\UnableToRetrieveMetadata;
use League\MimeTypeDetection\MimeTypeDetector;
use Throwable;
class AwsS3Adapter extends AwsS3V3Adapter implements AdapterInterface
{
protected S3ClientInterface $client;
protected string $bucket;
protected PathPrefixer $prefixer;
/**
* @var string[]
*/
protected const EXTRA_METADATA_FIELDS = [
'Metadata',
'StorageClass',
'ETag',
'VersionId',
];
public function __construct(
S3ClientInterface $client,
string $bucket,
string $prefix = '',
VisibilityConverter $visibility = null,
MimeTypeDetector $mimeTypeDetector = null,
array $options = [],
bool $streamReads = true
) {
$this->client = $client;
$this->bucket = $bucket;
$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;
return new FileAttributes(
$path,
$fileSize,
null,
$lastModified,
$mimetype
);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Flysystem\Adapter;
use League\Flysystem\StorageAttributes;
use League\Flysystem\UnableToRetrieveMetadata;
use Spatie\Dropbox\Exceptions\BadRequest;
class DropboxAdapter extends \Spatie\FlysystemDropbox\DropboxAdapter implements AdapterInterface
{
/** @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,63 @@
<?php
namespace App\Flysystem\Adapter;
use League\Flysystem\DirectoryAttributes;
use League\Flysystem\FileAttributes;
use League\Flysystem\Local\LocalFilesystemAdapter;
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;
class LocalAdapter extends LocalFilesystemAdapter implements AdapterInterface
{
protected PathPrefixer $pathPrefixer;
protected 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 getFullPath(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);
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace App\Flysystem\Cache;
use League\Flysystem\Cached\Storage\Psr6Cache as LeaguePsr6Cache;
class Psr6Cache extends LeaguePsr6Cache
{
/**
* Modification to the main cache adapter to always return null if the "has" call doesn't return true;
* this is a "pessimistic" assumption that the cache doesn't fully manage the filesystem (which it doesn't)
* and allows the cached adapter to take over if necessary.
*
* @inheritDoc
*/
public function has($path)
{
$cacheHasPath = parent::has($path);
if (true === $cacheHasPath) {
return true;
}
return null;
}
}

View File

@ -1,217 +0,0 @@
<?php
namespace App\Flysystem;
use App\Http\Response;
use InvalidArgumentException;
use Iterator;
use Jhofm\FlysystemIterator\FilesystemFilterIterator;
use Jhofm\FlysystemIterator\FilesystemIterator;
use Jhofm\FlysystemIterator\Options\Options;
use Jhofm\FlysystemIterator\RecursiveFilesystemIteratorIterator;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Cached\CachedAdapter;
use League\Flysystem\Cached\Storage\AbstractCache;
use League\Flysystem\Filesystem as LeagueFilesystem;
use Psr\Http\Message\ResponseInterface;
class Filesystem extends LeagueFilesystem implements FilesystemInterface
{
/**
* 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)
{
try {
$localPath = $this->getFullPath($path);
return $function($localPath);
} catch (InvalidArgumentException $e) {
$tempPath = $this->copyToLocal($path);
try {
$returnVal = $function($tempPath);
} finally {
unlink($tempPath);
}
return $returnVal;
}
}
public function putFromLocal(string $localPath, string $to): bool
{
$uploaded = $this->copyFromLocal($localPath, $to);
if ($uploaded) {
@unlink($localPath);
}
return $uploaded;
}
public function copyFromLocal(string $localPath, string $to): bool
{
if (!file_exists($localPath)) {
throw new \RuntimeException(sprintf('Source upload file not found at path: %s', $localPath));
}
$stream = fopen($localPath, 'rb');
$uploaded = $this->putStream($to, $stream);
if (is_resource($stream)) {
fclose($stream);
}
return $uploaded;
}
public function copyToLocal(string $from, ?string $localPath = null): string
{
if (null === $localPath) {
$folderPrefix = substr(md5($from), 0, 10);
$localPath = sys_get_temp_dir() . '/' . $folderPrefix . '_' . basename($from);
}
if (file_exists($localPath)) {
if (filemtime($localPath) >= $this->getTimestamp($from)) {
touch($localPath);
return $localPath;
}
unlink($localPath);
}
$stream = $this->readStream($from);
file_put_contents($localPath, $stream);
if (is_resource($stream)) {
fclose($stream);
}
return $localPath;
}
public function clearCache(bool $inMemoryOnly = false): void
{
$adapter = $this->getAdapter();
if ($adapter instanceof CachedAdapter) {
$cache = $adapter->getCache();
if ($inMemoryOnly && $cache instanceof AbstractCache) {
$prev_autosave = $cache->getAutosave();
$cache->setAutosave(false);
$cache->flush();
$cache->setAutosave($prev_autosave);
} else {
$cache->flush();
}
}
}
public function getFullPath(string $path): string
{
$adapter = $this->getAdapter();
if ($adapter instanceof CachedAdapter) {
$adapter = $adapter->getAdapter();
}
if (!($adapter instanceof Local)) {
throw new InvalidArgumentException('Filesystem adapter is not a Local or cached Local adapter.');
}
return $adapter->applyPathPrefix($path);
}
/**
* Create an iterator that loops through the entire contents of a given prefix.
*
* @param string $path
* @param array $iteratorOptions
*
*/
public function createIterator(string $path, array $iteratorOptions = []): Iterator
{
$iterator = new FilesystemIterator($this, $path, $iteratorOptions);
$options = Options::fromArray($iteratorOptions);
/** @phpstan-ignore-next-line */
if ($options->{Options::OPTION_IS_RECURSIVE}) {
$iterator = new RecursiveFilesystemIteratorIterator($iterator);
}
/** @phpstan-ignore-next-line */
if ($options->{Options::OPTION_FILTER} !== null) {
$iterator = new FilesystemFilterIterator($iterator, $options->{Options::OPTION_FILTER});
}
return $iterator;
}
/** @inheritDoc */
public function streamToResponse(
Response $response,
string $path,
string $fileName = null,
string $disposition = 'attachment'
): ResponseInterface {
$meta = $this->getMetadata($path);
try {
$mime = $this->getMimetype($path);
} catch (\Exception $e) {
$mime = 'application/octet-stream';
}
$fileName ??= basename($path);
if ('attachment' === $disposition) {
/*
* The regex used below is to ensure that the $fileName contains only
* characters ranging from ASCII 128-255 and ASCII 0-31 and 127 are replaced with an empty string
*/
$disposition .= '; filename="' . preg_replace('/[\x00-\x1F\x7F\"]/', ' ', $fileName) . '"';
$disposition .= "; filename*=UTF-8''" . rawurlencode($fileName);
}
$response = $response->withHeader('Content-Disposition', $disposition)
->withHeader('Content-Length', $meta['size'])
->withHeader('X-Accel-Buffering', 'no');
try {
$localPath = $this->getFullPath($path);
} catch (InvalidArgumentException $e) {
$localPath = $this->copyToLocal($path);
}
// Special internal nginx routes to use X-Accel-Redirect for far more performant file serving.
$specialPaths = [
'/var/azuracast/backups' => '/internal/backups',
'/var/azuracast/stations' => '/internal/stations',
];
foreach ($specialPaths as $diskPath => $nginxPath) {
if (0 === strpos($localPath, $diskPath)) {
$accelPath = str_replace($diskPath, $nginxPath, $localPath);
// Temporary work around, see SlimPHP/Slim#2924
$response->getBody()->write(' ');
return $response->withHeader('Content-Type', $mime)
->withHeader('X-Accel-Redirect', $accelPath);
}
}
return $response->withFile($localPath, $mime);
}
}

View File

@ -2,24 +2,67 @@
namespace App\Flysystem;
use App\Flysystem\Adapter\AdapterInterface;
use App\Http\Response;
use Iterator;
use League\Flysystem\FilesystemOperator;
use League\Flysystem\StorageAttributes;
use Psr\Http\Message\ResponseInterface;
interface FilesystemInterface extends \League\Flysystem\FilesystemInterface
interface FilesystemInterface extends FilesystemOperator
{
public function clearCache(bool $inMemoryOnly = false): void;
public function getFullPath(string $uri): string;
/**
* @return AdapterInterface The underlying filesystem adapter.
*/
public function getAdapter(): AdapterInterface;
/**
* Create an iterator that loops through the entire contents of a given prefix.
* @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;
/**
* 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 array $iteratorOptions
* @param callable $function
*
* @return mixed
*/
public function createIterator(string $path, array $iteratorOptions = []): Iterator;
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;
/**
* Read a stream from the filesystem and directly write it to a PSR-7-compatible response object.

View File

@ -1,161 +0,0 @@
<?php
namespace App\Flysystem;
use App\Entity;
use App\Flysystem\Cache\Psr6Cache;
use Cache\Prefixed\PrefixedCachePool;
use League\Flysystem\Adapter\AbstractAdapter;
use League\Flysystem\AdapterInterface;
use League\Flysystem\AwsS3v3\AwsS3Adapter;
use League\Flysystem\Cached\CachedAdapter;
use League\Flysystem\Config;
use Psr\Cache\CacheItemPoolInterface;
use Spatie\FlysystemDropbox\DropboxAdapter;
use Symfony\Component\Cache\Adapter\ProxyAdapter;
/**
* A wrapper and manager class for accessing assets on the filesystem.
*/
class FilesystemManager
{
public const PREFIX_MEDIA = 'media';
public const PREFIX_PLAYLISTS = 'playlists';
public const PREFIX_CONFIG = 'config';
public const PREFIX_RECORDINGS = 'recordings';
public const PREFIX_TEMP = 'temp';
protected CacheItemPoolInterface $cachePool;
public function __construct(CacheItemPoolInterface $cachePool)
{
$this->cachePool = new ProxyAdapter($cachePool, 'fs.');
}
public function getForStation(Entity\Station $station, bool $cached = true): StationFilesystemGroup
{
$aliases = [
self::PREFIX_MEDIA,
self::PREFIX_PLAYLISTS,
self::PREFIX_CONFIG,
self::PREFIX_RECORDINGS,
self::PREFIX_TEMP,
];
$filesystems = [];
foreach ($aliases as $alias) {
$filesystems[$alias] = $this->getPrefixedAdapterForStation($station, $alias, $cached);
}
return new StationFilesystemGroup($filesystems);
}
public function getPrefixedAdapterForStation(
Entity\Station $station,
string $prefix,
bool $cached = true
): Filesystem {
$isCachable = false;
switch ($prefix) {
case self::PREFIX_MEDIA:
$adapter = $station->getRadioMediaDirAdapter();
$isCachable = true;
break;
case self::PREFIX_RECORDINGS:
$adapter = $station->getRadioRecordingsDirAdapter();
$isCachable = true;
break;
case self::PREFIX_PLAYLISTS:
$adapter = $station->getRadioPlaylistsDirAdapter();
break;
case self::PREFIX_CONFIG:
$adapter = $station->getRadioConfigDirAdapter();
break;
case self::PREFIX_TEMP:
$adapter = $station->getRadioTempDirAdapter();
break;
default:
throw new \InvalidArgumentException(sprintf("Invalid adapter: %s", $prefix));
}
return $this->getFilesystemForAdapter($adapter, $isCachable && $cached);
}
public function getFilesystemForAdapter(AdapterInterface $adapter, bool $cached = false): Filesystem
{
$config = new Config();
if ($adapter instanceof DropboxAdapter) {
$config->set('case_sensitive', false);
}
if ($cached) {
$cachedClient = new Psr6Cache($this->cachePool, $this->getCacheKey($adapter), 3600);
$adapter = new CachedAdapter($adapter, $cachedClient);
}
return new Filesystem($adapter, $config);
}
public function flushCacheForAdapter(AdapterInterface $adapter, bool $inMemoryOnly = false): void
{
$fs = $this->getFilesystemForAdapter($adapter, true);
$fs->clearCache($inMemoryOnly);
}
protected function getCacheKey(AdapterInterface $adapter): string
{
if ($adapter instanceof CachedAdapter) {
$adapter = $adapter->getAdapter();
}
if ($adapter instanceof AwsS3Adapter) {
$s3Client = $adapter->getClient();
$bucket = $adapter->getBucket();
$objectUrl = $s3Client->getObjectUrl($bucket, $adapter->applyPathPrefix('/cache'));
return $this->filterCacheKey($objectUrl);
}
if ($adapter instanceof DropboxAdapter) {
return $this->filterCacheKey(
'dropbox_' . $adapter->getClient()->getAccessToken() . $adapter->applyPathPrefix('/cache')
);
}
if ($adapter instanceof AbstractAdapter) {
return $this->filterCacheKey(ltrim($adapter->getPathPrefix(), '/'));
}
throw new \InvalidArgumentException('Adapter does not have a cache key.');
}
protected function filterCacheKey(string $cacheKey): string
{
if (preg_match('|[\{\}\(\)/\\\@\:]|', $cacheKey)) {
return preg_replace('|[\{\}\(\)/\\\@\:]|', '_', $cacheKey);
}
return $cacheKey;
}
public static function applyPrefix(string $prefix, string $path): string
{
$path = ltrim(self::stripPrefix($path), '/');
return $prefix . '://' . $path;
}
public static function stripPrefix(string $path): string
{
if (strpos($path, '://') !== false) {
[, $path] = explode('://', $path, 2);
}
return $path;
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Flysystem;
use App\Flysystem\Adapter\LocalAdapter;
use League\Flysystem\PathNormalizer;
class LocalFilesystem extends AbstractFilesystem
{
protected LocalAdapter $localAdapter;
public function __construct(LocalAdapter $adapter, array $config = [], PathNormalizer $pathNormalizer = null)
{
$this->localAdapter = $adapter;
parent::__construct($adapter, $config, $pathNormalizer);
}
/** @inheritDoc */
public function isLocal(): bool
{
return true;
}
/** @inheritDoc */
public function getLocalPath(string $path): string
{
return $this->localAdapter->getFullPath($path);
}
/** @inheritDoc */
public function upload(string $localPath, string $to): void
{
$destPath = $this->getLocalPath($to);
copy($localPath, $destPath);
}
/** @inheritDoc */
public function download(string $from, string $localPath): void
{
$sourcePath = $this->getLocalPath($from);
copy($sourcePath, $localPath);
}
/** @inheritDoc */
public function withLocalFile(string $path, callable $function)
{
$localPath = $this->getLocalPath($path);
return $function($localPath);
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace App\Flysystem;
use App\Flysystem\Adapter\AdapterInterface;
use League\Flysystem\PathNormalizer;
use League\Flysystem\PathPrefixer;
use Spatie\FlysystemDropbox\DropboxAdapter;
class RemoteFilesystem extends AbstractFilesystem
{
protected AdapterInterface $remoteAdapter;
protected PathPrefixer $localPath;
public function __construct(
AdapterInterface $remoteAdapter,
string $localPath = null,
array $config = [],
PathNormalizer $pathNormalizer = null
) {
if ($remoteAdapter instanceof DropboxAdapter) {
$config['case_sensitive'] = false;
}
$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 (!file_exists($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 (file_exists($localPath)) {
if (filemtime($localPath) >= $this->lastModified($from)) {
touch($localPath);
}
unlink($localPath);
}
$stream = $this->readStream($from);
file_put_contents($localPath, $stream);
if (is_resource($stream)) {
fclose($stream);
}
}
}

View File

@ -1,124 +0,0 @@
<?php
namespace App\Flysystem;
use App\Exception;
use App\Http\Response;
use Iterator;
use League\Flysystem\MountManager;
use Psr\Http\Message\ResponseInterface;
class StationFilesystemGroup extends MountManager implements FilesystemInterface
{
public function upload(string $localPath, string $to): bool
{
[$prefix, $path] = $this->getPrefixAndPath($to);
/** @var Filesystem $fs */
$fs = $this->getFilesystem($prefix);
return $fs->putFromLocal($localPath, $to);
}
public function clearCache(bool $inMemoryOnly = false): void
{
foreach ($this->filesystems as $prefix => $filesystem) {
/** @var Filesystem $filesystem */
$filesystem->clearCache($inMemoryOnly);
}
}
public function getFullPath(string $uri): string
{
[$prefix, $path] = $this->getPrefixAndPath($uri);
/** @var Filesystem $fs */
$fs = $this->getFilesystem($prefix);
return $fs->getFullPath($path);
}
public function getLocalPath(string $uri): string
{
[$prefix, $path] = $this->getPrefixAndPath($uri);
/** @var Filesystem $fs */
$fs = $this->getFilesystem($prefix);
try {
return $fs->getFullPath($path);
} catch (\InvalidArgumentException $e) {
$tempUri = $this->copyToTemp($uri);
return $this->getFullPath($tempUri);
}
}
public function copyToTemp(string $from, ?string $to = null): string
{
[, $fromPath] = $this->getPrefixAndPath($from);
if (null === $to) {
$folderPrefix = substr(md5($fromPath), 0, 10);
$to = FilesystemManager::PREFIX_TEMP . '://' . $folderPrefix . '_' . basename($fromPath);
}
if ($this->has($to)) {
if ($this->getTimestamp($to) >= $this->getTimestamp($from)) {
$tempFullPath = $this->getLocalPath($to);
touch($tempFullPath);
return $to;
}
$this->delete($to);
}
$this->copy($from, $to);
return $to;
}
public function putFromTemp(string $from, string $to, array $config = []): string
{
$buffer = $this->readStream($from);
if ($buffer === false) {
throw new Exception('Source file could not be read.');
}
$written = $this->putStream($to, $buffer, $config);
if (is_resource($buffer)) {
fclose($buffer);
}
if ($written) {
$this->delete($from);
}
return $to;
}
/** @inheritDoc */
public function createIterator(string $path, array $iteratorOptions = []): Iterator
{
[$prefix, $path] = $this->getPrefixAndPath($path);
/** @var FilesystemInterface $fs */
$fs = $this->getFilesystem($prefix);
return $fs->createIterator($path, $iteratorOptions);
}
/** @inheritDoc */
public function streamToResponse(
Response $response,
string $path,
string $fileName = null,
string $disposition = 'attachment'
): ResponseInterface {
[$prefix, $path] = $this->getPrefixAndPath($path);
/** @var FilesystemInterface $fs */
$fs = $this->getFilesystem($prefix);
return $fs->streamToResponse($response, $path, $fileName, $disposition);
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Flysystem;
use App\Entity;
use App\Flysystem\Adapter\LocalAdapter;
class StationFilesystems
{
protected Entity\Station $station;
protected FilesystemInterface $fsMedia;
protected FilesystemInterface $fsRecordings;
protected LocalFilesystem $fsPlaylists;
protected LocalFilesystem $fsConfig;
protected LocalFilesystem $fsTemp;
public function __construct(Entity\Station $station)
{
$this->station = $station;
}
public function getMediaFilesystem(): FilesystemInterface
{
if (!isset($this->fsMedia)) {
$mediaAdapter = $this->station->getMediaStorageLocation()->getStorageAdapter();
if ($mediaAdapter instanceof LocalAdapter) {
$this->fsMedia = new LocalFilesystem($mediaAdapter);
} else {
$tempDir = $this->station->getRadioTempDir();
$this->fsMedia = new RemoteFilesystem($mediaAdapter, $tempDir);
}
}
return $this->fsMedia;
}
public function getRecordingsFilesystem(): FilesystemInterface
{
if (!isset($this->fsRecordings)) {
$recordingsAdapter = $this->station->getRecordingsStorageLocation()->getStorageAdapter();
if ($recordingsAdapter instanceof LocalAdapter) {
$this->fsRecordings = new LocalFilesystem($recordingsAdapter);
} else {
$tempDir = $this->station->getRadioTempDir();
$this->fsRecordings = new RemoteFilesystem($recordingsAdapter, $tempDir);
}
}
return $this->fsRecordings;
}
public function getPlaylistsFilesystem(): LocalFilesystem
{
if (!isset($this->fsPlaylists)) {
$playlistsDir = $this->station->getRadioPlaylistsDir();
$this->fsPlaylists = new LocalFilesystem(new LocalAdapter($playlistsDir));
}
return $this->fsPlaylists;
}
public function getConfigFilesystem(): LocalFilesystem
{
if (!isset($this->fsConfig)) {
$configDir = $this->station->getRadioConfigDir();
$this->fsConfig = new LocalFilesystem(new LocalAdapter($configDir));
}
return $this->fsConfig;
}
public function getTempFilesystem(): LocalFilesystem
{
if (!isset($this->fsTemp)) {
$tempDir = $this->station->getRadioTempDir();
$this->fsTemp = new LocalFilesystem(new LocalAdapter($tempDir));
}
return $this->fsTemp;
}
}

View File

@ -5,7 +5,6 @@ namespace App\Form;
use App\Config;
use App\Entity;
use App\Environment;
use App\Flysystem\FilesystemManager;
use App\Http\ServerRequest;
use App\Radio\Adapters;
use App\Radio\Configuration;
@ -23,8 +22,6 @@ class StationCloneForm extends StationForm
protected CheckMediaTask $media_sync;
protected FilesystemManager $filesystem;
public function __construct(
EntityManagerInterface $em,
Serializer $serializer,
@ -36,8 +33,7 @@ class StationCloneForm extends StationForm
Environment $environment,
Adapters $adapters,
Configuration $configuration,
CheckMediaTask $media_sync,
FilesystemManager $filesystem
CheckMediaTask $media_sync
) {
parent::__construct(
$em,
@ -56,7 +52,6 @@ class StationCloneForm extends StationForm
$this->configuration = $configuration;
$this->media_sync = $media_sync;
$this->filesystem = $filesystem;
}
/**

View File

@ -4,7 +4,7 @@ namespace App\Radio\AutoDJ;
use App\Entity;
use App\Event\Radio\AnnotateNextSong;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Radio\Adapters;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@ -17,21 +17,17 @@ class Annotations implements EventSubscriberInterface
protected Entity\Repository\StationStreamerRepository $streamerRepo;
protected FilesystemManager $filesystem;
protected Adapters $adapters;
public function __construct(
EntityManagerInterface $em,
Entity\Repository\StationQueueRepository $queueRepo,
Entity\Repository\StationStreamerRepository $streamerRepo,
FilesystemManager $filesystem,
Adapters $adapters
) {
$this->em = $em;
$this->queueRepo = $queueRepo;
$this->streamerRepo = $streamerRepo;
$this->filesystem = $filesystem;
$this->adapters = $adapters;
}
@ -54,9 +50,10 @@ class Annotations implements EventSubscriberInterface
{
$media = $event->getMedia();
if ($media instanceof Entity\StationMedia) {
$fs = $this->filesystem->getForStation($event->getStation());
$fsStation = new StationFilesystems($event->getStation());
$fsMedia = $fsStation->getMediaFilesystem();
$localMediaPath = $fs->getLocalPath($media->getPathUri());
$localMediaPath = $fsMedia->getLocalPath($media->getPath());
$event->setSongPath($localMediaPath);
$backend = $this->adapters->getBackendAdapter($event->getStation());

View File

@ -6,11 +6,12 @@ use App\Entity;
use App\Environment;
use App\Event\Radio\WriteLiquidsoapConfiguration;
use App\Exception;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Message;
use App\Radio\Adapters;
use App\Radio\Backend\Liquidsoap;
use Doctrine\ORM\EntityManagerInterface;
use League\Flysystem\StorageAttributes;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@ -33,8 +34,6 @@ class ConfigWriter implements EventSubscriberInterface
protected Liquidsoap $liquidsoap;
protected FilesystemManager $filesystem;
protected Environment $environment;
protected LoggerInterface $logger;
@ -43,14 +42,12 @@ class ConfigWriter implements EventSubscriberInterface
EntityManagerInterface $em,
Entity\Repository\SettingsRepository $settingsRepo,
Liquidsoap $liquidsoap,
FilesystemManager $filesystem,
Environment $environment,
LoggerInterface $logger
) {
$this->em = $em;
$this->settingsRepo = $settingsRepo;
$this->liquidsoap = $liquidsoap;
$this->filesystem = $filesystem;
$this->environment = $environment;
$this->logger = $logger;
}
@ -146,9 +143,9 @@ class ConfigWriter implements EventSubscriberInterface
$this->writeCustomConfigurationSection($event, self::CUSTOM_TOP);
$station = $event->getStation();
$fs = $this->filesystem->getForStation($station, false);
$pidfile = $fs->getFullPath(FilesystemManager::PREFIX_CONFIG . '://liquidsoap.pid');
$configDir = $station->getRadioConfigDir();
$pidfile = $configDir . DIRECTORY_SEPARATOR . 'liquidsoap.pid';
$telnetBindAddr = $this->environment->isDocker() ? '0.0.0.0' : '127.0.0.1';
$telnetPort = $this->liquidsoap->getTelnetPort($station);
@ -191,11 +188,16 @@ class ConfigWriter implements EventSubscriberInterface
$this->writeCustomConfigurationSection($event, self::CUSTOM_PRE_PLAYLISTS);
// Clear out existing playlists directory.
$fs = $this->filesystem->getForStation($station, false);
foreach ($fs->listContents(FilesystemManager::PREFIX_PLAYLISTS . '://', true) as $file) {
if ('file' === $file['type']) {
$fs->delete($file['filesystem'] . '://' . $file['path']);
$fsStation = new StationFilesystems($station);
$fsPlaylists = $fsStation->getPlaylistsFilesystem();
foreach ($fsPlaylists->listContents('', false) as $file) {
/** @var StorageAttributes $file */
if ($file->isDir()) {
$fsPlaylists->deleteDirectory($file->path());
} else {
$fsPlaylists->delete($file->path());
}
}

View File

@ -4,7 +4,7 @@ namespace App\Sync\Task;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use DoctrineBatchUtils\BatchProcessing\SimpleBatchIteratorAggregate;
use Psr\Log\LoggerInterface;
@ -14,20 +14,16 @@ class CheckFolderPlaylistsTask extends AbstractTask
protected Entity\Repository\StationPlaylistMediaRepository $spmRepo;
protected FilesystemManager $filesystem;
public function __construct(
ReloadableEntityManagerInterface $em,
LoggerInterface $logger,
Entity\Repository\StationPlaylistMediaRepository $spmRepo,
Entity\Repository\StationPlaylistFolderRepository $folderRepo,
FilesystemManager $filesystem
Entity\Repository\StationPlaylistFolderRepository $folderRepo
) {
parent::__construct($em, $logger);
$this->spmRepo = $spmRepo;
$this->folderRepo = $folderRepo;
$this->filesystem = $filesystem;
}
public function run(bool $force = false): void
@ -57,7 +53,8 @@ class CheckFolderPlaylistsTask extends AbstractTask
]
);
$fs = $this->filesystem->getForStation($station);
$fsStation = new StationFilesystems($station);
$fsMedia = $fsStation->getMediaFilesystem();
$mediaInPlaylistQuery = $this->em->createQuery(
<<<'DQL'
@ -95,7 +92,7 @@ class CheckFolderPlaylistsTask extends AbstractTask
$path = $folder->getPath();
// Verify the folder still exists.
if (!$fs->has(FilesystemManager::PREFIX_MEDIA . '://' . $path)) {
if (!$fsMedia->fileExists($path)) {
$this->em->remove($folder);
}

View File

@ -2,18 +2,17 @@
namespace App\Sync\Task;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity;
use App\Flysystem\FilesystemManager;
use App\Flysystem\StationFilesystems;
use App\Media\MimeType;
use App\Message;
use App\MessageQueue\QueueManager;
use App\Radio\Quota;
use Aws\S3\Exception\S3Exception;
use Brick\Math\BigInteger;
use Doctrine\ORM\EntityManagerInterface;
use DoctrineBatchUtils\BatchProcessing\SimpleBatchIteratorAggregate;
use Jhofm\FlysystemIterator\Filter\FilterFactory;
use Jhofm\FlysystemIterator\Options\Options;
use League\Flysystem\StorageAttributes;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBus;
@ -25,19 +24,16 @@ class CheckMediaTask extends AbstractTask
protected Entity\Repository\UnprocessableMediaRepository $unprocessableMediaRepo;
protected FilesystemManager $filesystem;
protected MessageBus $messageBus;
protected QueueManager $queueManager;
public function __construct(
EntityManagerInterface $em,
ReloadableEntityManagerInterface $em,
LoggerInterface $logger,
Entity\Repository\StationMediaRepository $mediaRepo,
Entity\Repository\StorageLocationRepository $storageLocationRepo,
Entity\Repository\UnprocessableMediaRepository $unprocessableMediaRepo,
FilesystemManager $filesystem,
MessageBus $messageBus,
QueueManager $queueManager
) {
@ -46,8 +42,6 @@ class CheckMediaTask extends AbstractTask
$this->storageLocationRepo = $storageLocationRepo;
$this->mediaRepo = $mediaRepo;
$this->unprocessableMediaRepo = $unprocessableMediaRepo;
$this->filesystem = $filesystem;
$this->messageBus = $messageBus;
$this->queueManager = $queueManager;
}
@ -103,8 +97,7 @@ class CheckMediaTask extends AbstractTask
public function importMusic(Entity\StorageLocation $storageLocation): void
{
$adapter = $storageLocation->getStorageAdapter();
$fs = $this->filesystem->getFilesystemForAdapter($adapter, false);
$fs = $storageLocation->getFilesystem();
$stats = [
'total_size' => '0',
@ -117,16 +110,16 @@ class CheckMediaTask extends AbstractTask
'not_processable' => 0,
];
/** @var StorageAttributes[] $musicFiles */
$musicFiles = [];
$total_size = BigInteger::zero();
try {
$fsIterator = $fs->createIterator(
'/',
[
Options::OPTION_IS_RECURSIVE => true,
Options::OPTION_FILTER => FilterFactory::isFile(),
]
$fsIterator = $fs->listContents('/', true)->filter(
function (StorageAttributes $attrs) {
return $attrs->isFile();
}
);
} catch (S3Exception $e) {
$this->logger->error(
@ -143,18 +136,18 @@ class CheckMediaTask extends AbstractTask
Entity\StationMedia::DIR_WAVEFORMS,
];
/** @var StorageAttributes $file */
foreach ($fsIterator as $file) {
foreach ($protectedPaths as $protectedPath) {
if (0 === strpos($file['path'], $protectedPath)) {
if (0 === strpos($file->path(), $protectedPath)) {
continue 2;
}
}
if (!empty($file['size'])) {
$total_size = $total_size->plus($file['size']);
}
$size = $fs->fileSize($file->path());
$total_size = $total_size->plus($size);
$pathHash = md5($file['path']);
$pathHash = md5($file->path());
$musicFiles[$pathHash] = $file;
}
@ -206,7 +199,7 @@ class CheckMediaTask extends AbstractTask
if (isset($queuedMediaUpdates[$media_row->getId()])) {
$stats['already_queued']++;
} elseif ($force_reprocess || $media_row->needsReprocessing($fileInfo['timestamp'])) {
} elseif ($force_reprocess || $media_row->needsReprocessing($fileInfo->lastModified())) {
$message = new Message\ReprocessMediaMessage();
$message->media_id = $media_row->getId();
$message->force = $force_reprocess;
@ -242,10 +235,10 @@ class CheckMediaTask extends AbstractTask
if (isset($musicFiles[$pathHash])) {
$fileInfo = $musicFiles[$pathHash];
if ($unprocessableRow->needsReprocessing($fileInfo['timestamp'])) {
if ($unprocessableRow->needsReprocessing($fileInfo->lastModified())) {
$message = new Message\AddNewMediaMessage();
$message->storage_location_id = $storageLocation->getId();
$message->path = $fileInfo['path'];
$message->path = $fileInfo->path();
$this->messageBus->dispatch($message);
@ -264,7 +257,7 @@ class CheckMediaTask extends AbstractTask
// Create files that do not currently exist.
foreach ($musicFiles as $pathHash => $newMusicFile) {
$path = $newMusicFile['path'];
$path = $newMusicFile->path();
if (!MimeType::isPathProcessable($path)) {
$mimeType = MimeType::getMimeTypeFromPath($path);
@ -295,33 +288,41 @@ class CheckMediaTask extends AbstractTask
public function importPlaylists(Entity\Station $station): void
{
$fs = $this->filesystem->getForStation($station, false);
$fsStation = new StationFilesystems($station);
$fsMedia = $fsStation->getMediaFilesystem();
// Skip playlist importing for remote filesystems.
if (!$fsMedia->isLocal()) {
return;
}
$fsPlaylists = $fsStation->getPlaylistsFilesystem();
// Create a lookup cache of all valid imported media.
$media_lookup = [];
foreach ($station->getMedia() as $media) {
/** @var Entity\StationMedia $media */
$media_path = $fs->getFullPath($media->getPathUri());
$media_path = $fsMedia->getLocalPath($media->getPath());
$media_hash = md5($media_path);
$media_lookup[$media_hash] = $media;
}
// Iterate through playlists.
$playlist_files_raw = $fs->createIterator(
FilesystemManager::PREFIX_PLAYLISTS . '://',
[
'filter' => FilterFactory::pathMatchesRegex('/^.+\.(m3u|pls)$/i'),
]
$playlist_files_raw = $fsPlaylists->listContents('/', true)->filter(
function (StorageAttributes $attrs) {
return preg_match('/^.+\.(m3u|pls)$/i', $attrs->path()) > 0;
}
);
foreach ($playlist_files_raw as $playlist_file) {
/** @var StorageAttributes $playlist_file */
// Create new StationPlaylist record.
$record = new Entity\StationPlaylist($station);
$playlist_file_path = $fs->getFullPath(
FilesystemManager::PREFIX_PLAYLISTS . '://' . $playlist_file['path']
);
$playlist_file_path = $fsPlaylists->getLocalPath($playlist_file->path());
$path_parts = pathinfo($playlist_file_path);
$playlist_name = str_replace('playlist_', '', $path_parts['filename']);

View File

@ -2,27 +2,13 @@
namespace App\Sync\Task;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity;
use App\Flysystem\FilesystemManager;
use DoctrineBatchUtils\BatchProcessing\SimpleBatchIteratorAggregate;
use Psr\Log\LoggerInterface;
use League\Flysystem\StorageAttributes;
use Symfony\Component\Finder\Finder;
class CleanupStorageTask extends AbstractTask
{
protected FilesystemManager $filesystem;
public function __construct(
ReloadableEntityManagerInterface $em,
LoggerInterface $logger,
FilesystemManager $filesystem
) {
parent::__construct($em, $logger);
$this->filesystem = $filesystem;
}
public function run(bool $force = false): void
{
$stationsQuery = $this->em->createQuery(
@ -108,8 +94,12 @@ class CleanupStorageTask extends AbstractTask
$dirContents = $fs->listContents($dirBase, true);
foreach ($dirContents as $row) {
if (!isset($allUniqueIds[$row['filename']])) {
$fs->delete($row['path']);
/** @var StorageAttributes $row */
$path = $row->path();
$filename = pathinfo($path, PATHINFO_FILENAME);
if (!isset($allUniqueIds[$filename])) {
$fs->delete($path);
$removed[$key]++;
}
}

View File

@ -6,7 +6,7 @@ use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity;
use App\Environment;
use App\Radio\Adapters;
use Jhofm\FlysystemIterator\Options\Options;
use League\Flysystem\StorageAttributes;
use Psr\Log\LoggerInterface;
use Supervisor\Supervisor;
use Symfony\Component\Finder\Finder;
@ -86,19 +86,16 @@ class RotateLogsTask extends AbstractTask
): void {
$fs = $storageLocation->getFilesystem();
$iterator = $fs->createIterator(
'',
[
Options::OPTION_IS_RECURSIVE => false,
Options::OPTION_FILTER => function (array $item): bool {
return (isset($item['path']) && 0 === stripos($item['path'], 'automatic_backup'));
},
]
$iterator = $fs->listContents('', false)->filter(
function (StorageAttributes $attrs) {
return 0 === stripos($attrs->path(), 'automatic_backup');
}
);
$backupsByTime = [];
foreach ($iterator as $backup) {
$backupsByTime[$backup['timestamp']] = $backup['path'];
/** @var StorageAttributes $backup */
$backupsByTime[$backup->lastModified()] = $backup->path();
}
if (count($backupsByTime) <= $copiesToKeep) {

View File

@ -122,7 +122,7 @@ abstract class CestAbstract
$storageLocation = $testStation->getMediaStorageLocation();
$storageFs = $storageLocation->getFilesystem();
$storageFs->copyFromLocal($songSrc, 'test.mp3');
$storageFs->upload($songSrc, 'test.mp3');
/** @var Entity\Repository\StationMediaRepository $mediaRepo */
$mediaRepo = $this->di->get(Entity\Repository\StationMediaRepository::class);