249 lines
7.2 KiB
PHP
249 lines
7.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Console\Command\Backup;
|
|
|
|
use App\Entity;
|
|
use App\Environment;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Component\Console\Attribute\AsCommand;
|
|
use Symfony\Component\Console\Input\InputArgument;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Input\InputOption;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
use Symfony\Component\Filesystem\Filesystem;
|
|
use Symfony\Component\Filesystem\Path;
|
|
use Throwable;
|
|
|
|
use const PATHINFO_EXTENSION;
|
|
|
|
#[AsCommand(
|
|
name: 'azuracast:backup',
|
|
description: 'Back up the AzuraCast database and statistics (and optionally media).',
|
|
)]
|
|
class BackupCommand extends AbstractBackupCommand
|
|
{
|
|
public function __construct(
|
|
Environment $environment,
|
|
EntityManagerInterface $em,
|
|
protected Entity\Repository\StorageLocationRepository $storageLocationRepo,
|
|
) {
|
|
parent::__construct($environment, $em);
|
|
}
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this->addArgument('path', InputArgument::REQUIRED)
|
|
->addOption('storage-location-id', null, InputOption::VALUE_OPTIONAL)
|
|
->addOption('exclude-media', null, InputOption::VALUE_NONE);
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
{
|
|
$io = new SymfonyStyle($input, $output);
|
|
$fsUtils = new Filesystem();
|
|
|
|
$path = $input->getArgument('path');
|
|
$excludeMedia = (bool)$input->getOption('exclude-media');
|
|
$storageLocationId = $input->getOption('storage-location-id');
|
|
|
|
$start_time = microtime(true);
|
|
|
|
if (empty($path)) {
|
|
$path = 'manual_backup_' . gmdate('Ymd_Hi') . '.zip';
|
|
}
|
|
|
|
$file_ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
|
|
|
if (Path::isAbsolute($path)) {
|
|
$tmpPath = $path;
|
|
$storageLocation = null;
|
|
} else {
|
|
$tmpPath = $fsUtils->tempnam(
|
|
sys_get_temp_dir(),
|
|
'backup_',
|
|
'.' . $file_ext
|
|
);
|
|
|
|
// Zip command cannot handle an existing file (even an empty one)
|
|
@unlink($tmpPath);
|
|
|
|
if (null === $storageLocationId) {
|
|
$io->error('You must specify a storage location when providing a relative path.');
|
|
return 1;
|
|
}
|
|
|
|
$storageLocation = $this->storageLocationRepo->findByType(
|
|
Entity\Enums\StorageLocationTypes::Backup,
|
|
$storageLocationId
|
|
);
|
|
if (!($storageLocation instanceof Entity\StorageLocation)) {
|
|
$io->error('Invalid storage location specified.');
|
|
return 1;
|
|
}
|
|
|
|
if ($storageLocation->isStorageFull()) {
|
|
$io->error('Storage location is full.');
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
$includeMedia = !$excludeMedia;
|
|
$files_to_backup = [];
|
|
|
|
$io->title(__('AzuraCast Backup'));
|
|
$io->writeln(__('Please wait while a backup is generated...'));
|
|
|
|
// Create temp directories
|
|
$io->section(__('Creating temporary directories...'));
|
|
|
|
$tmp_dir_mariadb = '/tmp/azuracast_backup_mariadb';
|
|
try {
|
|
$fsUtils->mkdir($tmp_dir_mariadb);
|
|
} catch (Throwable $e) {
|
|
$io->error($e->getMessage());
|
|
return 1;
|
|
}
|
|
|
|
$io->newLine();
|
|
|
|
// Back up MariaDB
|
|
$io->section(__('Backing up MariaDB...'));
|
|
|
|
$path_db_dump = $tmp_dir_mariadb . '/db.sql';
|
|
|
|
[$commandFlags, $commandEnvVars] = $this->getDatabaseSettingsAsCliFlags();
|
|
|
|
$commandFlags[] = '--add-drop-table';
|
|
$commandFlags[] = '--default-character-set=UTF8MB4';
|
|
|
|
$commandEnvVars['DB_DEST'] = $path_db_dump;
|
|
|
|
$this->passThruProcess(
|
|
$io,
|
|
'mysqldump ' . implode(' ', $commandFlags) . ' $DB_DATABASE > $DB_DEST',
|
|
$tmp_dir_mariadb,
|
|
$commandEnvVars
|
|
);
|
|
|
|
$files_to_backup[] = $path_db_dump;
|
|
$io->newLine();
|
|
|
|
// Include station media if specified.
|
|
if ($includeMedia) {
|
|
$stations = $this->em->createQuery(
|
|
<<<'DQL'
|
|
SELECT s FROM App\Entity\Station s
|
|
DQL
|
|
)->execute();
|
|
|
|
foreach ($stations as $station) {
|
|
/** @var Entity\Station $station */
|
|
|
|
$mediaAdapter = $station->getMediaStorageLocation();
|
|
if ($mediaAdapter->isLocal()) {
|
|
$files_to_backup[] = $mediaAdapter->getPath();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compress backup files.
|
|
$io->section(__('Creating backup archive...'));
|
|
|
|
// Strip leading slashes from backup paths.
|
|
$files_to_backup = array_map(
|
|
static function (string $val) {
|
|
if (str_starts_with($val, '/')) {
|
|
return substr($val, 1);
|
|
}
|
|
return $val;
|
|
},
|
|
$files_to_backup
|
|
);
|
|
|
|
switch ($file_ext) {
|
|
case 'tzst':
|
|
$this->passThruProcess(
|
|
$io,
|
|
array_merge(
|
|
[
|
|
'tar',
|
|
'-I',
|
|
'zstd',
|
|
'-cvf',
|
|
$tmpPath,
|
|
],
|
|
$files_to_backup
|
|
),
|
|
'/'
|
|
);
|
|
break;
|
|
|
|
case 'gz':
|
|
case 'tgz':
|
|
$this->passThruProcess(
|
|
$io,
|
|
array_merge(
|
|
[
|
|
'tar',
|
|
'zcvf',
|
|
$tmpPath,
|
|
],
|
|
$files_to_backup
|
|
),
|
|
'/'
|
|
);
|
|
break;
|
|
|
|
case 'zip':
|
|
default:
|
|
$dont_compress = ['.tar.gz', '.zip', '.jpg', '.mp3', '.ogg', '.flac', '.aac', '.wav'];
|
|
|
|
$this->passThruProcess(
|
|
$io,
|
|
array_merge(
|
|
[
|
|
'zip',
|
|
'-r',
|
|
'-n',
|
|
implode(':', $dont_compress),
|
|
$tmpPath,
|
|
],
|
|
$files_to_backup
|
|
),
|
|
'/'
|
|
);
|
|
break;
|
|
}
|
|
|
|
if (null !== $storageLocation) {
|
|
$fs = $storageLocation->getFilesystem();
|
|
$fs->uploadAndDeleteOriginal($tmpPath, $path);
|
|
}
|
|
|
|
$io->newLine();
|
|
|
|
// Cleanup
|
|
$io->section(__('Cleaning up temporary files...'));
|
|
|
|
$fsUtils->remove($tmp_dir_mariadb);
|
|
|
|
$io->newLine();
|
|
|
|
$end_time = microtime(true);
|
|
$time_diff = $end_time - $start_time;
|
|
|
|
$io->success(
|
|
[
|
|
sprintf(
|
|
__('Backup complete in %.2f seconds.'),
|
|
$time_diff
|
|
),
|
|
]
|
|
);
|
|
return 0;
|
|
}
|
|
}
|