Switch from FTP to SFTP on Docker installations.
This commit is contained in:
parent
c8110fcce1
commit
e9d8775af6
2
.env
2
.env
|
@ -3,4 +3,4 @@ COMPOSE_PROJECT_NAME=azuracast
|
|||
AZURACAST_HTTP_PORT=80
|
||||
AZURACAST_HTTPS_PORT=443
|
||||
|
||||
AZURACAST_FTP_PORT=21
|
||||
AZURACAST_SFTP_PORT=2022
|
|
@ -43,14 +43,9 @@ return function (Application $console) {
|
|||
)->setDescription('Send upcoming song feedback from the AutoDJ back to AzuraCast.');
|
||||
|
||||
$console->command(
|
||||
'azuracast:internal:ftp-auth',
|
||||
Command\Internal\FtpAuthCommand::class
|
||||
)->setDescription('Authenticate a user for PureFTPD');
|
||||
|
||||
$console->command(
|
||||
'azuracast:internal:ftp-upload path',
|
||||
Command\Internal\FtpUploadCommand::class
|
||||
)->setDescription('Process a file uploaded in PureFTPD');
|
||||
'azuracast:internal:sftp-upload action username path target-path ssh-cmd',
|
||||
Command\Internal\SFtpUploadCommand::class
|
||||
)->setDescription('Process a file uploaded via SFTP');
|
||||
|
||||
$console->command(
|
||||
'azuracast:internal:nextsong station-id [as-autodj]',
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
return [
|
||||
'elements' => [
|
||||
|
||||
'username' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Username'),
|
||||
'class' => 'half-width',
|
||||
'maxLength' => 8,
|
||||
],
|
||||
],
|
||||
|
||||
'password' => [
|
||||
'password',
|
||||
[
|
||||
'label' => __('New Password'),
|
||||
'description' => __('Leave blank to use the current password.'),
|
||||
'autocomplete' => 'off',
|
||||
'required' => false,
|
||||
],
|
||||
],
|
||||
|
||||
'publicKeys' => [
|
||||
'textarea',
|
||||
[
|
||||
'label' => __('SSH Public Keys'),
|
||||
'class' => 'text-preformatted',
|
||||
'description' => __('Optionally supply SSH public keys this user can use to connect instead of a password. Enter one key per line.'),
|
||||
'required' => false,
|
||||
],
|
||||
],
|
||||
|
||||
'submit' => [
|
||||
'submit',
|
||||
[
|
||||
'type' => 'submit',
|
||||
'label' => __('Save Changes'),
|
||||
'class' => 'btn btn-lg btn-primary',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
|
@ -133,6 +133,12 @@ return function (\App\Event\BuildStationMenu $e) {
|
|||
'label' => __('Utilities'),
|
||||
'icon' => 'settings',
|
||||
'items' => [
|
||||
'sftp_users' => [
|
||||
'label' => __('SFTP Users'),
|
||||
'url' => $router->fromHere('stations:sftp_users:index'),
|
||||
'visible' => \App\Service\SFTPGo::isSupported(),
|
||||
'permission' => Acl::STATION_MEDIA,
|
||||
],
|
||||
'automation' => [
|
||||
'label' => __('Automated Assignment'),
|
||||
'url' => $router->fromHere('stations:automation:index'),
|
||||
|
|
|
@ -620,6 +620,22 @@ return function (App $app) {
|
|||
|
||||
})->add(new Middleware\Permissions(Acl::STATION_REPORTS, true));
|
||||
|
||||
$group->group('/sftp_users', function (RouteCollectorProxy $group) {
|
||||
|
||||
$group->get('', Controller\Stations\SFTPUsersController::class . ':indexAction')
|
||||
->setName('stations:sftp_users:index');
|
||||
|
||||
$group->map(['GET', 'POST'], '/edit/{id}', Controller\Stations\SFTPUsersController::class . ':editAction')
|
||||
->setName('stations:sftp_users:edit');
|
||||
|
||||
$group->map(['GET', 'POST'], '/add', Controller\Stations\SFTPUsersController::class . ':editAction')
|
||||
->setName('stations:sftp_users:add');
|
||||
|
||||
$group->get('/delete/{id}/{csrf}', Controller\Stations\SFTPUsersController::class . ':deleteAction')
|
||||
->setName('stations:sftp_users:delete');
|
||||
|
||||
})->add(new Middleware\Permissions(Acl::STATION_MEDIA, true));
|
||||
|
||||
$group->group('/streamers', function (RouteCollectorProxy $group) {
|
||||
|
||||
$group->get('', Controller\Stations\StreamersController::class . ':indexAction')
|
||||
|
|
|
@ -131,8 +131,10 @@ return [
|
|||
App\Customization::class => DI\autowire(),
|
||||
App\Version::class => DI\autowire(),
|
||||
App\Service\AzuraCastCentral::class => DI\autowire(),
|
||||
App\Service\Sentry::class => DI\autowire(),
|
||||
App\Service\GeoLite::class => DI\autowire(),
|
||||
App\Service\NChan::class => DI\autowire(),
|
||||
App\Service\Sentry::class => DI\autowire(),
|
||||
App\Service\SFTPGo::class => DI\autowire(),
|
||||
App\Validator\Constraints\StationPortCheckerValidator::class => DI\autowire(),
|
||||
|
||||
// Message queue manager class
|
||||
|
@ -178,9 +180,6 @@ return [
|
|||
return $mq;
|
||||
},
|
||||
|
||||
// MaxMind (IP Geolocation database for listener metadata)
|
||||
App\Service\GeoLite::class => DI\autowire(),
|
||||
|
||||
// InfluxDB
|
||||
InfluxDB\Database::class => function (Settings $settings) {
|
||||
$opts = [
|
||||
|
@ -271,6 +270,7 @@ return [
|
|||
[ // Every 5 minutes tasks
|
||||
$di->get(App\Sync\Task\Media::class),
|
||||
$di->get(App\Sync\Task\CheckForUpdates::class),
|
||||
$di->get(App\Sync\Task\SyncSFTPUsers::class),
|
||||
],
|
||||
[ // Every hour tasks
|
||||
$di->get(App\Sync\Task\Analytics::class),
|
||||
|
@ -292,6 +292,7 @@ return [
|
|||
App\Sync\Task\RadioRequests::class => DI\autowire(),
|
||||
App\Sync\Task\RelayCleanup::class => DI\autowire(),
|
||||
App\Sync\Task\RotateLogs::class => DI\autowire(),
|
||||
App\Sync\Task\SyncSFTPUsers::class => DI\autowire(),
|
||||
|
||||
/**
|
||||
* Web Hooks
|
||||
|
@ -341,46 +342,16 @@ return [
|
|||
App\Notification\Manager::class => DI\autowire(),
|
||||
|
||||
/*
|
||||
* Forms
|
||||
* Class Groups
|
||||
*/
|
||||
|
||||
App\Form\EntityFormManager::class => function (
|
||||
EntityManager $em,
|
||||
Symfony\Component\Serializer\Serializer $serializer,
|
||||
Symfony\Component\Validator\Validator\ValidatorInterface $validator,
|
||||
ContainerInterface $di
|
||||
) {
|
||||
$custom_forms = [
|
||||
App\Entity\Station::class => $di->get(App\Form\StationForm::class),
|
||||
App\Entity\User::class => $di->get(App\Form\UserForm::class),
|
||||
App\Entity\RolePermission::class => $di->get(App\Form\PermissionsForm::class),
|
||||
App\Entity\Settings::class => $di->get(App\Form\SettingsForm::class),
|
||||
App\Entity\StationPlaylist::class => $di->get(App\Form\StationPlaylistForm::class),
|
||||
App\Entity\StationMount::class => $di->get(App\Form\StationMountForm::class),
|
||||
App\Entity\StationWebhook::class => $di->get(App\Form\StationWebhookForm::class),
|
||||
];
|
||||
|
||||
return new App\Form\EntityFormManager($em, $serializer, $validator, $custom_forms);
|
||||
},
|
||||
|
||||
App\Form\BackupSettingsForm::class => DI\autowire(),
|
||||
App\Form\BrandingSettingsForm::class => DI\autowire(),
|
||||
App\Form\PermissionsForm::class => DI\autowire(),
|
||||
App\Form\SettingsForm::class => DI\autowire(),
|
||||
App\Form\StationForm::class => DI\autowire(),
|
||||
App\Form\StationCloneForm::class => DI\autowire(),
|
||||
App\Form\StationMountForm::class => DI\autowire(),
|
||||
App\Form\StationPlaylistForm::class => DI\autowire(),
|
||||
App\Form\StationWebhookForm::class => DI\autowire(),
|
||||
App\Form\UserForm::class => DI\autowire(),
|
||||
App\Form\UserProfileForm::class => DI\autowire(),
|
||||
'App\Form\*Form' => DI\autowire(),
|
||||
'App\Entity\Fixture\*' => DI\autowire(),
|
||||
|
||||
/*
|
||||
* Controller Groups
|
||||
* Controller Classes
|
||||
*/
|
||||
|
||||
'App\Entity\Fixture\*' => DI\autowire(),
|
||||
|
||||
'App\Controller\Admin\*Controller' => DI\autowire(),
|
||||
|
||||
'App\Controller\Api\*Controller' => DI\autowire(),
|
||||
|
|
|
@ -27,17 +27,17 @@ services:
|
|||
env_file: azuracast.env
|
||||
environment: &default-environment
|
||||
LANG: ${LANG:-en_US.UTF-8}
|
||||
AZURACAST_DC_REVISION: 6
|
||||
AZURACAST_FTP_PORT: ${AZURACAST_FTP_PORT:-21}
|
||||
AZURACAST_DC_REVISION: 7
|
||||
AZURACAST_SFTP_PORT: ${AZURACAST_SFTP_PORT:-2022}
|
||||
ports:
|
||||
- '${AZURACAST_FTP_PORT:-21}:21'
|
||||
- '${AZURACAST_FTP_PASV_RANGE:-30000-30049}:30000-30049' # FTP PASV support
|
||||
- '${AZURACAST_SFTP_PORT:-2022}:2022'
|
||||
volumes:
|
||||
- .:/var/azuracast/www
|
||||
- tmp_data:/var/azuracast/www_tmp
|
||||
- station_data:/var/azuracast/stations
|
||||
- shoutcast2_install:/var/azuracast/servers/shoutcast2
|
||||
- geolite_install:/var/azuracast/geoip
|
||||
- sftpgo_data:/var/azuracast/sftpgo/persist
|
||||
- backups:/var/azuracast/backups
|
||||
restart: always
|
||||
ulimits: &default-ulimits
|
||||
|
@ -143,6 +143,7 @@ volumes:
|
|||
station_data: {}
|
||||
shoutcast2_install: {}
|
||||
geolite_install: {}
|
||||
sftpgo_data: {}
|
||||
tmp_data: {}
|
||||
redis_data: {}
|
||||
backups: {}
|
||||
|
|
|
@ -20,8 +20,7 @@ services:
|
|||
ports:
|
||||
- '${AZURACAST_HTTP_PORT:-80}:80'
|
||||
- '${AZURACAST_HTTPS_PORT:-443}:443'
|
||||
- '${AZURACAST_FTP_PORT:-21}:21'
|
||||
- '30000-30049:30000-30049' # FTP PASV support
|
||||
- '${AZURACAST_SFTP_PORT:-2022}:2022'
|
||||
depends_on:
|
||||
- mariadb
|
||||
- influxdb
|
||||
|
@ -30,8 +29,8 @@ services:
|
|||
env_file: azuracast.env
|
||||
environment: &default-environment
|
||||
LANG: ${LANG:-en_US.UTF-8}
|
||||
AZURACAST_DC_REVISION: 6
|
||||
AZURACAST_FTP_PORT: ${AZURACAST_FTP_PORT:-21}
|
||||
AZURACAST_DC_REVISION: 7
|
||||
AZURACAST_SFTP_PORT: ${AZURACAST_SFTP_PORT:-2022}
|
||||
volumes:
|
||||
- letsencrypt:/etc/letsencrypt
|
||||
- www_data:/var/azuracast/www
|
||||
|
@ -39,6 +38,7 @@ services:
|
|||
- station_data:/var/azuracast/stations
|
||||
- shoutcast2_install:/var/azuracast/servers/shoutcast2
|
||||
- geolite_install:/var/azuracast/geoip
|
||||
- sftpgo_data:/var/azuracast/sftpgo/persist
|
||||
- backups:/var/azuracast/backups
|
||||
restart: always
|
||||
ulimits: &default-ulimits
|
||||
|
@ -247,6 +247,7 @@ volumes:
|
|||
letsencrypt: {}
|
||||
shoutcast2_install: {}
|
||||
geolite_install: {}
|
||||
sftpgo_data: {}
|
||||
station_data: {}
|
||||
tmp_data: {}
|
||||
www_data: {}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
<?php
|
||||
namespace App\Console\Command\Internal;
|
||||
|
||||
use App\Service\Ftp;
|
||||
use Azura\Console\Command\CommandAbstract;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class FtpAuthCommand extends CommandAbstract
|
||||
{
|
||||
public function __invoke(
|
||||
SymfonyStyle $io,
|
||||
Ftp $ftp
|
||||
) {
|
||||
$username = getenv('AUTHD_ACCOUNT');
|
||||
$password = getenv('AUTHD_PASSWORD');
|
||||
|
||||
$ftp_output = $ftp->auth($username, $password);
|
||||
foreach ($ftp_output as $output_ln) {
|
||||
$io->writeln($output_ln);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
<?php
|
||||
namespace App\Console\Command\Internal;
|
||||
|
||||
use App\Entity;
|
||||
use App\Message;
|
||||
use App\MessageQueue;
|
||||
use App\Radio\Filesystem;
|
||||
use Azura\Console\Command\CommandAbstract;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class FtpUploadCommand extends CommandAbstract
|
||||
{
|
||||
public function __invoke(
|
||||
SymfonyStyle $io,
|
||||
EntityManager $em,
|
||||
Entity\Repository\StationRepository $stationRepo,
|
||||
LoggerInterface $logger,
|
||||
Filesystem $filesystem,
|
||||
MessageQueue $messageQueue,
|
||||
string $path
|
||||
) {
|
||||
$logger->info('FTP file uploaded', ['path' => $path]);
|
||||
|
||||
// Working backwards from the media's path, find the associated station(s) to process.
|
||||
$stations = [];
|
||||
$all_stations = $stationRepo->fetchAll();
|
||||
|
||||
$parts = explode('/', dirname($path));
|
||||
for ($i = count($parts); $i >= 1; $i--) {
|
||||
$search_path = implode('/', array_slice($parts, 0, $i));
|
||||
|
||||
$stations = array_filter($all_stations, function (Entity\Station $station) use ($search_path) {
|
||||
return $search_path === $station->getRadioMediaDir();
|
||||
});
|
||||
|
||||
if (!empty($stations)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($stations as $station) {
|
||||
/** @var Entity\Station $station */
|
||||
$fs = $filesystem->getForStation($station);
|
||||
$fs->flushAllCaches();
|
||||
|
||||
$relative_path = str_replace($station->getRadioMediaDir() . '/', '', $path);
|
||||
|
||||
$message = new Message\AddNewMediaMessage;
|
||||
$message->station_id = $station->getId();
|
||||
$message->path = $relative_path;
|
||||
$messageQueue->produce($message);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
namespace App\Console\Command\Internal;
|
||||
|
||||
use App\Entity;
|
||||
use App\Message;
|
||||
use App\MessageQueue;
|
||||
use App\Radio\Filesystem;
|
||||
use Azura\Console\Command\CommandAbstract;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class SFtpUploadCommand extends CommandAbstract
|
||||
{
|
||||
public function __invoke(
|
||||
SymfonyStyle $io,
|
||||
EntityManager $em,
|
||||
Entity\Repository\StationRepository $stationRepo,
|
||||
LoggerInterface $logger,
|
||||
Filesystem $filesystem,
|
||||
MessageQueue $messageQueue,
|
||||
string $action = null,
|
||||
string $username = null,
|
||||
string $path = null,
|
||||
string $targetPath = null,
|
||||
string $sshCmd = null
|
||||
) {
|
||||
$logger->info('SFTP file uploaded', ['path' => $path]);
|
||||
|
||||
// Determine which station the username belongs to.
|
||||
$userRepo = $em->getRepository(Entity\SFTPUser::class);
|
||||
|
||||
$sftpUser = $userRepo->findOneBy([
|
||||
'username' => $username,
|
||||
]);
|
||||
|
||||
if (!$sftpUser instanceof Entity\SFTPUser) {
|
||||
$logger->error('SFTP Username not found.', ['username' => $username]);
|
||||
return;
|
||||
}
|
||||
|
||||
$station = $sftpUser->getStation();
|
||||
|
||||
$fs = $filesystem->getForStation($station);
|
||||
$fs->flushAllCaches();
|
||||
|
||||
$relative_path = str_replace($station->getRadioMediaDir() . '/', '', $path);
|
||||
|
||||
$message = new Message\AddNewMediaMessage;
|
||||
$message->station_id = $station->getId();
|
||||
$message->path = $relative_path;
|
||||
$messageQueue->produce($message);
|
||||
}
|
||||
}
|
|
@ -2,22 +2,16 @@
|
|||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Entity;
|
||||
use App\Form\EntityFormManager;
|
||||
use App\Form\CustomFieldForm;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Azura\Config;
|
||||
use Azura\Session\Flash;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class CustomFieldsController extends AbstractAdminCrudController
|
||||
{
|
||||
/**
|
||||
* @param Config $config
|
||||
* @param EntityFormManager $formManager
|
||||
*/
|
||||
public function __construct(Config $config, EntityFormManager $formManager)
|
||||
public function __construct(CustomFieldForm $form)
|
||||
{
|
||||
$form = $formManager->getForm(Entity\CustomField::class, $config->get('forms/custom_field'));
|
||||
parent::__construct($form);
|
||||
|
||||
$this->csrf_namespace = 'admin_custom_fields';
|
||||
|
|
|
@ -66,7 +66,7 @@ abstract class AbstractStationCrudController
|
|||
return null;
|
||||
}
|
||||
|
||||
$record = $this->record_repo->findOneBy(['id' => $id, 'station_id' => $station->getId()]);
|
||||
$record = $this->record_repo->findOneBy(['id' => $id, 'station' => $station]);
|
||||
|
||||
if (!$record instanceof $this->entity_class) {
|
||||
throw new NotFoundException(__('Record not found.'));
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace App\Controller\Stations;
|
|||
use App\Entity;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Service\Ftp;
|
||||
use App\Service\SFTPGo;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
|
@ -13,8 +13,7 @@ class FilesController
|
|||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
EntityManager $em,
|
||||
Ftp $ftp
|
||||
EntityManager $em
|
||||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
|
||||
|
@ -45,7 +44,7 @@ class FilesController
|
|||
}
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'stations/files/index', [
|
||||
'ftp_info' => $ftp->getInfo(),
|
||||
'show_sftp' => SFTPGo::isSupported(),
|
||||
'playlists' => $playlists,
|
||||
'custom_fields' => $custom_fields,
|
||||
'space_used' => $station->getStorageUsed(),
|
||||
|
|
|
@ -4,20 +4,17 @@ namespace App\Controller\Stations;
|
|||
use App\Entity\Station;
|
||||
use App\Entity\StationRemote;
|
||||
use App\Exception\PermissionDeniedException;
|
||||
use App\Form\EntityFormManager;
|
||||
use App\Form\StationRemoteForm;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Azura\Config;
|
||||
use Azura\Session\Flash;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class RemotesController extends AbstractStationCrudController
|
||||
{
|
||||
public function __construct(EntityFormManager $formManager, Config $config)
|
||||
public function __construct(StationRemoteForm $form)
|
||||
{
|
||||
$form = $formManager->getForm(StationRemote::class, $config->get('forms/remote'));
|
||||
parent::__construct($form);
|
||||
|
||||
$this->csrf_namespace = 'stations_remotes';
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
namespace App\Controller\Stations;
|
||||
|
||||
use App\Exception\StationUnsupportedException;
|
||||
use App\Form\SFTPUserForm;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Service\AzuraCastCentral;
|
||||
use App\Service\SFTPGo;
|
||||
use Azura\Session\Flash;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class SFTPUsersController extends AbstractStationCrudController
|
||||
{
|
||||
protected AzuraCastCentral $ac_central;
|
||||
|
||||
public function __construct(SFTPUserForm $form, AzuraCastCentral $ac_central)
|
||||
{
|
||||
parent::__construct($form);
|
||||
|
||||
$this->ac_central = $ac_central;
|
||||
$this->csrf_namespace = 'stations_sftp_users';
|
||||
}
|
||||
|
||||
public function indexAction(ServerRequest $request, Response $response): ResponseInterface
|
||||
{
|
||||
if (!SFTPGo::isSupported()) {
|
||||
throw new StationUnsupportedException(__('This feature is not currently supported on this station.'));
|
||||
}
|
||||
|
||||
$station = $request->getStation();
|
||||
|
||||
$baseUrl = $request->getRouter()->getBaseUrl(false)
|
||||
->withScheme('sftp')
|
||||
->withPort(null);
|
||||
$port = $_ENV['AZURACAST_SFTP_PORT'] ?? 2022;
|
||||
|
||||
$sftpInfo = [
|
||||
'url' => (string)$baseUrl,
|
||||
'ip' => $this->ac_central->getIp(),
|
||||
'port' => $port,
|
||||
];
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'stations/sftp_users/index', [
|
||||
'users' => $station->getSFTPUsers(),
|
||||
'sftp_info' => $sftpInfo,
|
||||
'csrf' => $request->getCsrf()->generate($this->csrf_namespace),
|
||||
]);
|
||||
}
|
||||
|
||||
public function editAction(ServerRequest $request, Response $response, $id = null): ResponseInterface
|
||||
{
|
||||
if (false !== $this->_doEdit($request, $id)) {
|
||||
$request->getFlash()->addMessage('<b>' . __('Changes saved.') . '</b>', Flash::SUCCESS);
|
||||
return $response->withRedirect($request->getRouter()->fromHere('stations:sftp_users:index'));
|
||||
}
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'system/form_page', [
|
||||
'form' => $this->form,
|
||||
'render_mode' => 'edit',
|
||||
'title' => $id ? __('Edit SFTP User') : __('Add SFTP User'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleteAction(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
$id,
|
||||
$csrf
|
||||
): ResponseInterface {
|
||||
$this->_doDelete($request, $id, $csrf);
|
||||
|
||||
$request->getFlash()->addMessage('<b>' . __('SFTP User deleted.') . '</b>', Flash::SUCCESS);
|
||||
return $response->withRedirect($request->getRouter()->fromHere('stations:sftp_users:index'));
|
||||
}
|
||||
}
|
|
@ -3,11 +3,10 @@ namespace App\Controller\Stations;
|
|||
|
||||
use App\Entity;
|
||||
use App\Exception\StationUnsupportedException;
|
||||
use App\Form\EntityFormManager;
|
||||
use App\Form\StationStreamerForm;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Service\AzuraCastCentral;
|
||||
use Azura\Config;
|
||||
use Azura\Session\Flash;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
|
@ -18,12 +17,10 @@ class StreamersController extends AbstractStationCrudController
|
|||
protected Entity\Repository\SettingsRepository $settingsRepo;
|
||||
|
||||
public function __construct(
|
||||
EntityFormManager $formManager,
|
||||
Config $config,
|
||||
StationStreamerForm $form,
|
||||
AzuraCastCentral $ac_central,
|
||||
Entity\Repository\SettingsRepository $settingsRepo
|
||||
) {
|
||||
$form = $formManager->getForm(Entity\StationStreamer::class, $config->get('forms/streamer'));
|
||||
parent::__construct($form);
|
||||
|
||||
$this->ac_central = $ac_central;
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity\Migration;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20200105190343 extends AbstractMigration
|
||||
{
|
||||
public function getDescription() : string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('CREATE TABLE sftp_user (id INT AUTO_INCREMENT NOT NULL, station_id INT DEFAULT NULL, username VARCHAR(8) NOT NULL, password VARCHAR(255) NOT NULL, public_keys LONGTEXT DEFAULT NULL, INDEX IDX_3C32EA3421BDB235 (station_id), UNIQUE INDEX username_idx (username), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_general_ci` ENGINE = InnoDB');
|
||||
$this->addSql('ALTER TABLE sftp_user ADD CONSTRAINT FK_3C32EA3421BDB235 FOREIGN KEY (station_id) REFERENCES station (id) ON DELETE CASCADE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
|
||||
|
||||
$this->addSql('DROP TABLE sftp_user');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Annotations\AuditLog\Auditable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="sftp_user", uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(name="username_idx", columns={"username"})
|
||||
* })
|
||||
* @ORM\Entity()
|
||||
*
|
||||
* @Auditable()
|
||||
*/
|
||||
class SFTPUser
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="AUTO")
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Station", inversedBy="SFTPUsers")
|
||||
* @ORM\JoinColumns({
|
||||
* @ORM\JoinColumn(name="station_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* })
|
||||
* @var Station
|
||||
*/
|
||||
protected $station;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="username", type="string", length=8, nullable=false)
|
||||
* @var string
|
||||
*
|
||||
* @Assert\Length(min=1, max=8)
|
||||
* @Assert\NotBlank
|
||||
*/
|
||||
protected $username;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="password", type="string", length=255, nullable=false)
|
||||
* @var string
|
||||
*
|
||||
* @Assert\NotBlank
|
||||
*/
|
||||
protected $password;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="public_keys", type="text", nullable=true)
|
||||
* @var string|null
|
||||
*/
|
||||
protected $publicKeys;
|
||||
|
||||
public function __construct(Station $station)
|
||||
{
|
||||
$this->station = $station;
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getStation(): Station
|
||||
{
|
||||
return $this->station;
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function setUsername(string $username): void
|
||||
{
|
||||
$this->username = $username;
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getHashedPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(?string $password): void
|
||||
{
|
||||
if (!empty($password)) {
|
||||
$this->password = password_hash($password, \PASSWORD_ARGON2ID, [
|
||||
'memory_cost' => 65535,
|
||||
'time_cost' => 3,
|
||||
'threads' => 2,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getPublicKeys(): ?string
|
||||
{
|
||||
return $this->publicKeys;
|
||||
}
|
||||
|
||||
public function getPublicKeysArray(): array
|
||||
{
|
||||
$pubKeysRaw = trim($this->publicKeys);
|
||||
if (!empty($pubKeysRaw)) {
|
||||
return explode("\n", $pubKeysRaw);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function setPublicKeys(?string $publicKeys): void
|
||||
{
|
||||
$this->publicKeys = $publicKeys;
|
||||
}
|
||||
}
|
|
@ -381,6 +381,12 @@ class Station
|
|||
*/
|
||||
protected $webhooks;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity="SFTPUser", mappedBy="station")
|
||||
* @var Collection
|
||||
*/
|
||||
protected $SFTPUsers;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->history = new ArrayCollection;
|
||||
|
@ -390,6 +396,7 @@ class Station
|
|||
$this->remotes = new ArrayCollection;
|
||||
$this->webhooks = new ArrayCollection;
|
||||
$this->streamers = new ArrayCollection;
|
||||
$this->SFTPUsers = new ArrayCollection;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1223,6 +1230,14 @@ class Station
|
|||
return $this->webhooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
public function getSFTPUsers(): Collection
|
||||
{
|
||||
return $this->SFTPUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the API version of the object/array.
|
||||
*
|
||||
|
|
|
@ -12,7 +12,6 @@ use OpenApi\Annotations as OA;
|
|||
use OTPHP\Factory;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use const PASSWORD_ARGON2I;
|
||||
use const PASSWORD_BCRYPT;
|
||||
|
||||
/**
|
||||
|
@ -235,8 +234,8 @@ class User
|
|||
*/
|
||||
protected function _getPasswordAlgorithm(): array
|
||||
{
|
||||
if (defined('PASSWORD_ARGON2I')) {
|
||||
return [PASSWORD_ARGON2I, []];
|
||||
if (defined('PASSWORD_ARGON2ID')) {
|
||||
return [PASSWORD_ARGON2ID, []];
|
||||
}
|
||||
|
||||
return [PASSWORD_BCRYPT, []];
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity;
|
||||
use Azura\Config;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class CustomFieldForm extends EntityForm
|
||||
{
|
||||
public function __construct(
|
||||
EntityManager $em,
|
||||
Serializer $serializer,
|
||||
ValidatorInterface $validator,
|
||||
Config $config
|
||||
) {
|
||||
$form_config = $config->get('forms/custom_field');
|
||||
parent::__construct($em, $serializer, $validator, $form_config);
|
||||
|
||||
$this->entityClass = Entity\CustomField::class;
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
<?php
|
||||
namespace App\Form;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class EntityFormManager
|
||||
{
|
||||
protected EntityManager $em;
|
||||
|
||||
protected Serializer $serializer;
|
||||
|
||||
protected ValidatorInterface $validator;
|
||||
|
||||
/** @var EntityForm[] */
|
||||
protected array $custom_forms;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $em,
|
||||
Serializer $serializer,
|
||||
ValidatorInterface $validator,
|
||||
array $custom_forms
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->serializer = $serializer;
|
||||
$this->validator = $validator;
|
||||
|
||||
$this->custom_forms = $custom_forms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a specified entity class and form configuration array, return
|
||||
* a configured and initialized EntityForm.
|
||||
*
|
||||
* @param string $entity_class
|
||||
* @param array|null $form_config
|
||||
* @param array|null $defaults
|
||||
*
|
||||
* @return EntityForm
|
||||
*/
|
||||
public function getForm($entity_class, array $form_config = null, array $defaults = null): EntityForm
|
||||
{
|
||||
if (isset($this->custom_forms[$entity_class])) {
|
||||
return $this->custom_forms[$entity_class];
|
||||
}
|
||||
|
||||
$form = new EntityForm($this->em, $this->serializer, $this->validator, $form_config, $defaults);
|
||||
$form->setEntityClass($entity_class);
|
||||
|
||||
return $form;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Service\SFTPGo;
|
||||
use Azura\Config;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class SFTPUserForm extends EntityForm
|
||||
{
|
||||
protected SFTPGo $sftpgo;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $em,
|
||||
Serializer $serializer,
|
||||
ValidatorInterface $validator,
|
||||
Config $config,
|
||||
SFTPGo $sftpgo
|
||||
) {
|
||||
$form_config = $config->get('forms/sftp_user');
|
||||
|
||||
parent::__construct($em, $serializer, $validator, $form_config);
|
||||
|
||||
$this->sftpgo = $sftpgo;
|
||||
$this->entityClass = Entity\SFTPUser::class;
|
||||
}
|
||||
|
||||
public function process(ServerRequest $request, $record = null)
|
||||
{
|
||||
$result = parent::process($request, $record);
|
||||
if (false !== $result) {
|
||||
$this->sftpgo->sync();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity;
|
||||
use Azura\Config;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class StationRemoteForm extends EntityForm
|
||||
{
|
||||
public function __construct(
|
||||
EntityManager $em,
|
||||
Serializer $serializer,
|
||||
ValidatorInterface $validator,
|
||||
Config $config
|
||||
) {
|
||||
$form_config = $config->get('forms/remote');
|
||||
parent::__construct($em, $serializer, $validator, $form_config);
|
||||
|
||||
$this->entityClass = Entity\StationRemote::class;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity;
|
||||
use Azura\Config;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class StationStreamerForm extends EntityForm
|
||||
{
|
||||
public function __construct(
|
||||
EntityManager $em,
|
||||
Serializer $serializer,
|
||||
ValidatorInterface $validator,
|
||||
Config $config
|
||||
) {
|
||||
$form_config = $config->get('forms/streamer');
|
||||
parent::__construct($em, $serializer, $validator, $form_config);
|
||||
|
||||
$this->entityClass = Entity\StationStreamer::class;
|
||||
}
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
<?php
|
||||
namespace App\Service;
|
||||
|
||||
use App\Acl;
|
||||
use App\Entity;
|
||||
use App\Http\Router;
|
||||
use App\Settings;
|
||||
use App\Utilities;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
|
||||
class Ftp
|
||||
{
|
||||
protected AzuraCastCentral $ac_central;
|
||||
|
||||
protected Acl $acl;
|
||||
|
||||
protected Settings $app_settings;
|
||||
|
||||
protected EntityManager $em;
|
||||
|
||||
protected Entity\Repository\UserRepository $user_repo;
|
||||
|
||||
protected Entity\Repository\SettingsRepository $settings_repo;
|
||||
|
||||
protected LoggerInterface $logger;
|
||||
|
||||
protected Router $router;
|
||||
|
||||
public function __construct(
|
||||
AzuraCastCentral $ac_central,
|
||||
Acl $acl,
|
||||
Settings $app_settings,
|
||||
EntityManager $em,
|
||||
Entity\Repository\SettingsRepository $settings_repo,
|
||||
Entity\Repository\UserRepository $user_repo,
|
||||
LoggerInterface $logger,
|
||||
Router $router
|
||||
) {
|
||||
$this->ac_central = $ac_central;
|
||||
$this->acl = $acl;
|
||||
$this->app_settings = $app_settings;
|
||||
$this->em = $em;
|
||||
$this->logger = $logger;
|
||||
$this->router = $router;
|
||||
|
||||
$this->user_repo = $user_repo;
|
||||
$this->settings_repo = $settings_repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a username and password, handle a PureFTPD authentication request.
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function auth(string $username, string $password): array
|
||||
{
|
||||
$error = ['auth_ok:-1', 'end'];
|
||||
|
||||
if (!$this->isEnabled()) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
// Some FTP clients URL Encode the username, particularly the '@' of the e-mail address.
|
||||
$username = urldecode($username);
|
||||
|
||||
$this->logger->info('FTP Authentication attempt.', [
|
||||
'username' => $username,
|
||||
]);
|
||||
|
||||
$user = $this->user_repo->authenticate($username, $password);
|
||||
|
||||
if (!($user instanceof Entity\User)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
// Create a temporary directory with symlinks to every station that user can manage.
|
||||
$ftp_dir = '/tmp/azuracast_ftp_directories/user_' . $user->getId();
|
||||
Utilities::rmdirRecursive($ftp_dir);
|
||||
|
||||
if (!mkdir($ftp_dir) && !is_dir($ftp_dir)) {
|
||||
throw new RuntimeException(sprintf('Directory "%s" was not created', $ftp_dir));
|
||||
}
|
||||
|
||||
$stations = $this->em->getRepository(Entity\Station::class)->findAll();
|
||||
$has_any_stations = false;
|
||||
|
||||
foreach ($stations as $station) {
|
||||
/** @var Entity\Station $station */
|
||||
if ($this->acl->userAllowed($user, Acl::STATION_MEDIA, $station->getId())) {
|
||||
$has_any_stations = true;
|
||||
|
||||
$station_media_dir = $station->getRadioMediaDir();
|
||||
$symlink_path = $ftp_dir . '/' . $station->getShortName();
|
||||
|
||||
symlink($station_media_dir, $symlink_path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$has_any_stations) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
return [
|
||||
'auth_ok:1',
|
||||
'uid:1000',
|
||||
'gid:1000',
|
||||
'dir:' . $ftp_dir . '/./',
|
||||
'end',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Whether FTP services are enabled for this installation.
|
||||
*/
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
if (!$this->app_settings->isDocker() || $_ENV['AZURACAST_DC_REVISION'] < 6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool)$this->settings_repo->getSetting(Entity\Settings::ENABLE_FTP_SERVER, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|null FTP connection information, if FTP is enabled.
|
||||
*/
|
||||
public function getInfo(): ?array
|
||||
{
|
||||
if (!$this->isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$base_url = $this->router->getBaseUrl(false)
|
||||
->withScheme('ftp')
|
||||
->withPort(null);
|
||||
|
||||
$port = $_ENV['AZURACAST_FTP_PORT'] ?? 21;
|
||||
|
||||
return [
|
||||
'url' => (string)$base_url,
|
||||
'ip' => $this->ac_central->getIp(),
|
||||
'port' => $port,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\SFTPUser;
|
||||
use App\Settings;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class SFTPGo
|
||||
{
|
||||
protected EntityManager $em;
|
||||
|
||||
protected Client $client;
|
||||
|
||||
public function __construct(EntityManager $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
|
||||
$this->client = new Client([
|
||||
'base_uri' => 'http://localhost:10080/api/v1/',
|
||||
'timeout' => 2.0,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sync(): void
|
||||
{
|
||||
$users = $this->em->createQuery(/** @lang DQL */ '
|
||||
SELECT su, s
|
||||
FROM App\Entity\SFTPUser su JOIN su.station s
|
||||
')->execute();
|
||||
|
||||
$export = [];
|
||||
|
||||
foreach ($users as $user) {
|
||||
/** @var SFTPUser $user */
|
||||
|
||||
$row = [
|
||||
'id' => $user->getId(),
|
||||
'username' => $user->getUsername(),
|
||||
'expiration_date' => 0,
|
||||
'status' => 1,
|
||||
'password' => $user->getHashedPassword(),
|
||||
'public_keys' => $user->getPublicKeysArray(),
|
||||
'home_dir' => $user->getStation()->getRadioMediaDir(),
|
||||
'uid' => 0,
|
||||
'gid' => 0,
|
||||
'permissions' => [
|
||||
'/' => ['*'],
|
||||
],
|
||||
];
|
||||
|
||||
$export[] = $row;
|
||||
}
|
||||
|
||||
$backupContents = json_encode(['users' => $export], \JSON_THROW_ON_ERROR);
|
||||
|
||||
$backupDir = '/var/azuracast/sftpgo/backups';
|
||||
$backupFileName = time() . '.json';
|
||||
|
||||
$backupPath = $backupDir . '/' . $backupFileName;
|
||||
file_put_contents($backupPath, $backupContents);
|
||||
|
||||
$this->client->get('loaddata', [
|
||||
'query' => [
|
||||
'input_file' => $backupPath,
|
||||
'scan_quota' => 2,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public static function isSupported(): bool
|
||||
{
|
||||
$settings = Settings::getInstance();
|
||||
return $settings->isDockerRevisionNewerThan(7);
|
||||
}
|
||||
}
|
|
@ -21,5 +21,13 @@ class Settings extends \Azura\Settings
|
|||
return $this->getParentDirectory() . '/stations';
|
||||
}
|
||||
|
||||
public function isDockerRevisionNewerThan(int $version): bool
|
||||
{
|
||||
if (!$this->isDocker()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$compareVersion = (int)$this->get(self::DOCKER_REVISION, 0);
|
||||
return ($compareVersion >= $version);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
namespace App\Sync\Task;
|
||||
|
||||
use App\Entity;
|
||||
use App\Service\SFTPGo;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
|
||||
class SyncSFTPUsers extends AbstractTask
|
||||
{
|
||||
protected SFTPGo $sftpgo;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $em,
|
||||
Entity\Repository\SettingsRepository $settingsRepo,
|
||||
SFTPGo $sftpgo
|
||||
) {
|
||||
parent::__construct($em, $settingsRepo);
|
||||
|
||||
$this->sftpgo = $sftpgo;
|
||||
}
|
||||
|
||||
public function run($force = false): void
|
||||
{
|
||||
if (!SFTPGo::isSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sftpgo->sync();
|
||||
}
|
||||
}
|
|
@ -25,30 +25,31 @@ $assets
|
|||
<div class="card-header bg-primary-dark">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-7">
|
||||
<h2 class="card-title"><?= __('Music Files') ?></h2>
|
||||
<h2 class="card-title"><?=__('Music Files')?></h2>
|
||||
</div>
|
||||
<div class="col-md-5 text-right">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="<?= $space_percent ?>"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width: <?= $space_percent ?>%;">
|
||||
<span class="sr-only"><?= $space_percent ?>%</span>
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="<?=$space_percent?>"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width: <?=$space_percent?>%;">
|
||||
<span class="sr-only"><?=$space_percent?>%</span>
|
||||
</div>
|
||||
</div>
|
||||
<?= __('%s of %s Used (%d Files)', $space_used, $space_total, $files_count) ?>
|
||||
<?=__('%s of %s Used (%d Files)', $space_used, $space_total, $files_count)?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($ftp_info): ?>
|
||||
<?php if ($show_sftp): ?>
|
||||
<div class="card-body alert-info d-flex align-items-center" role="alert">
|
||||
<div class="flex-shrink-0 mr-2">
|
||||
<i class="material-icons" aria-hidden="true">info</i>
|
||||
</div>
|
||||
<div class="flex-fill">
|
||||
<p class="mb-0">
|
||||
<?= __('You can also upload files in bulk via FTP.') ?><br>
|
||||
<button type="button" class="btn btn-link p-0" data-toggle="modal" data-target="#ftpinfo">
|
||||
<?= __('View connection instructions') ?>
|
||||
</button>
|
||||
<?=__('You can also upload files in bulk via SFTP.')?><br>
|
||||
<a class="btn btn-link p-0" target="_blank"
|
||||
href="<?=$router->fromHere('stations:sftp_users:index')?>">
|
||||
<?=__('Manage SFTP Accounts')?>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -57,72 +58,4 @@ $assets
|
|||
<div id="media-manager"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($ftp_info): ?>
|
||||
<div id="ftpinfo" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><?= __('FTP Connection Information') ?></h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table class="table table-striped">
|
||||
<colgroup>
|
||||
<col width="40%">
|
||||
<col width="60%">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<?= __('Server') ?>
|
||||
</td>
|
||||
<td>
|
||||
<?= $ftp_info['url'] ?><br>
|
||||
<small><?= __('You may need to connect directly via your IP address, which is <code>%s</code>.',
|
||||
$ftp_info['ip']) ?></small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<?= __('Port') ?>
|
||||
</td>
|
||||
<td>
|
||||
<?= $ftp_info['port'] ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<?= __('Protocol') ?>
|
||||
</td>
|
||||
<td>
|
||||
<?= __('FTP with Explicit TLS (FTPS)') ?><br>
|
||||
<small><?= __('Unencrypted FTP is also allowed, but not recommended.') ?></small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<?= __('Username') ?>
|
||||
</td>
|
||||
<td>
|
||||
<?= __('Your AzuraCast E-mail Address') ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<?= __('Password') ?>
|
||||
</td>
|
||||
<td>
|
||||
<?= __('Your AzuraCast Password') ?>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
|
@ -0,0 +1,68 @@
|
|||
<?php $this->layout('main', ['title' => __('SFTP Users'), 'manual' => true]) ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title"><?=__('SFTP Users')?></h2>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-outline-primary" role="button"
|
||||
href="<?=$router->fromHere('stations:sftp_users:add')?>">
|
||||
<i class="material-icons" aria-hidden="true">add</i>
|
||||
<?=__('Add SFTP User')?>
|
||||
</a>
|
||||
</div>
|
||||
<table class="table table-responsive-lg table-striped mb-0">
|
||||
<colgroup>
|
||||
<col width="25%">
|
||||
<col width="75%">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?=__('Actions')?></th>
|
||||
<th><?=__('Username')?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $row): ?>
|
||||
<?php /** @var App\Entity\SFTPUser $row */ ?>
|
||||
<tr class="align-middle">
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a class="btn btn-sm btn-primary" href="<?=$router->fromHere('stations:sftp_users:edit',
|
||||
['id' => $row->getId()])?>"><?=__('Edit')?></a>
|
||||
<a class="btn btn-sm btn-danger"
|
||||
data-confirm-title="<?=$this->e(__('Delete SFTP User "%s"?', $row->getUsername()))?>"
|
||||
href="<?=$router->fromHere('stations:sftp_users:delete',
|
||||
['id' => $row->getId(), 'csrf' => $csrf])?>"><?=__('Delete')?></a>
|
||||
</div>
|
||||
</td>
|
||||
<td><code><?=$this->e($row->getUsername())?></code></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title"><?=__('Connection Information')?></h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl>
|
||||
<dt class="mb-1"><?=__('Server')?>:</dt>
|
||||
<dd><code><?=$this->e($sftp_info['url'])?></code></dd>
|
||||
<?php if ($sftp_info['ip']): ?>
|
||||
<dd><?=__('You may need to connect directly via your IP address, which is <code>%s</code>.',
|
||||
$sftp_info['ip'])?></dd>
|
||||
<?php endif; ?>
|
||||
|
||||
<dt class="mb-1"><?=__('Port')?>:</dt>
|
||||
<dd><code><?=(int)$sftp_info['port']?></code></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in New Issue