From e902d83c9519e83b32a19cebb4b086653024a453 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Tue, 13 May 2025 22:17:39 +0300 Subject: [PATCH] Update RestoreCommand to support both async/sync requests. --- src/Backends/Common/ClientInterface.php | 7 +- src/Commands/Backend/RestoreCommand.php | 126 +++++++++--------------- 2 files changed, 52 insertions(+), 81 deletions(-) diff --git a/src/Backends/Common/ClientInterface.php b/src/Backends/Common/ClientInterface.php index 9bfa8d28..e230c2a9 100644 --- a/src/Backends/Common/ClientInterface.php +++ b/src/Backends/Common/ClientInterface.php @@ -18,7 +18,6 @@ use Psr\Http\Message\StreamInterface as iStream; use Psr\Http\Message\UriInterface as iUri; use Psr\Log\LoggerInterface as iLogger; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; -use Symfony\Contracts\HttpClient\ResponseInterface as iResponse; interface ClientInterface { @@ -86,7 +85,7 @@ interface ClientInterface * @param iImport $mapper mapper to use. * @param iDate|null $after only import items after this date. * - * @return array responses. + * @return array responses. */ public function pull(iImport $mapper, iDate|null $after = null): array; @@ -97,7 +96,7 @@ interface ClientInterface * @param iStream|null $writer writer to use. * @param array $opts options for backup. * - * @return array responses. + * @return array responses. */ public function backup(iImport $mapper, iStream|null $writer = null, array $opts = []): array; @@ -108,7 +107,7 @@ interface ClientInterface * @param QueueRequests $queue queue to use. * @param iDate|null $after only export items after this date. * - * @return array responses. + * @return array responses. */ public function export(iImport $mapper, QueueRequests $queue, iDate|null $after = null): array; diff --git a/src/Commands/Backend/RestoreCommand.php b/src/Commands/Backend/RestoreCommand.php index 7a6d8e1a..c634fce9 100644 --- a/src/Commands/Backend/RestoreCommand.php +++ b/src/Commands/Backend/RestoreCommand.php @@ -5,10 +5,12 @@ declare(strict_types=1); namespace App\Commands\Backend; use App\Command; +use App\Libs\Attributes\DI\Inject; use App\Libs\Attributes\Route\Cli; use App\Libs\Config; use App\Libs\Enums\Http\Status; use App\Libs\Exceptions\RuntimeException; +use App\Libs\Extends\RetryableHttpClient; use App\Libs\Extends\StreamLogHandler; use App\Libs\LogSuppressor; use App\Libs\Mappers\Import\RestoreMapper; @@ -28,6 +30,7 @@ use Symfony\Component\Console\Input\InputInterface as iInput; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface as iOutput; use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp; use Throwable; /** @@ -51,7 +54,9 @@ class RestoreCommand extends Command public function __construct( private readonly QueueRequests $queue, private readonly iLogger $logger, - private LogSuppressor $suppressor + private LogSuppressor $suppressor, + #[Inject(RetryableHttpClient::class)] + private iHttp $http, ) { set_time_limit(0); ini_set('memory_limit', '-1'); @@ -71,70 +76,26 @@ class RestoreCommand extends Command ->addOption('timeout', null, InputOption::VALUE_REQUIRED, 'Set request timeout in seconds.') ->addOption('select-backend', 's', InputOption::VALUE_REQUIRED, 'Select backend.') ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'Select sub user.', 'main') + ->addOption( + 'ignore', + 'i', + InputOption::VALUE_NONE, + 'Bypass backend export.enabled check. Use with caution.', + ) ->addArgument('file', InputArgument::REQUIRED, 'Backup file to restore from') - ->addOption('logfile', null, InputOption::VALUE_REQUIRED, 'Save console output to file.') - ->setHelp( - r( - <<state:backup] command. - - This restore process only works on backends that has export enabled. - - The restore process is exactly the same as the [state:export] with [--ignore-date, --force-full] - flags enabled, the difference is instead of reading state from database we are reading it from backup file. - - ------------------- - [ Risk Assessment ] - ------------------- - - If you are trying to restore a backend that has import play state enabled, the changes from restoring from backup file - will propagate back to your other backends. If you don't intend for that to happen, then DISABLE import from the backend. - - -------------------------------- - [ Enable restore functionality ] - -------------------------------- - - If you understand the risks and what might happen if you do restore from a backup file, - then you can enable the command by adding [--execute] to the command. - - For example, - - {cmd} {route} --execute -vv -s backend_name -- {backupDir}/backup_file.json - - ------- - [ FAQ ] - ------- - - # Restore operation is cancelled. - - If you encounter this error, it means either you didn't answer with yes for risk assessment confirmation, - or the interaction is disabled, if you can't enable interaction, then you can add another flag [--assume-yes] - to bypass the check. This confirms that you understand the risks of restoring backend that has import enabled. - - # Ignoring [backend_name] [item_title]. [Movie|Episode] Is not imported yet. - - This is normal, this is likely because the backup is already outdated and some items in remote does not exist in backup file, - or you are using backup from another source which likely does not have matching data. - - # Where are the backups stored? - - By default, it should be at [{backupDir}]. - - # How to see what data will be changed? - - if you do not add [--execute] to the comment, it will run in dry mode by default, - To see what data will be changed run the command with [-v] log level. - - HELP, - [ - 'cmd' => trim(commandContext()), - 'route' => self::ROUTE, - 'backupDir' => after(Config::get('path') . '/backup', ROOT_PATH), - ] - ) - ); + ->addOption( + 'sync-requests', + null, + InputOption::VALUE_NONE, + 'Send one request at a time instead of all at once. note: Slower but more reliable.' + ) + ->addOption( + 'async-requests', + null, + InputOption::VALUE_NONE, + 'Send all requests at once. note: Faster but less reliable. Default.' + ) + ->addOption('logfile', null, InputOption::VALUE_REQUIRED, 'Save console output to file.'); } /** @@ -220,11 +181,21 @@ class RestoreCommand extends Command } if (false === (bool)ag($backend, 'export.enabled')) { - $output->writeln(r("ERROR: Export to '{user}@{backend}' are disabled.", [ - 'backend' => $name, - 'user' => $userContext->name, - ])); - return self::FAILURE; + if (false === $input->getOption('ignore')) { + $output->writeln(r("ERROR: Export to '{user}@{backend}' are disabled.", [ + 'backend' => $name, + 'user' => $userContext->name, + ])); + return self::FAILURE; + } + + $this->logger->warning( + "Exporting to '{user}@{backend}' is disabled, However, the check was bypass due to [-i, --ignore] flag being used.", + [ + 'backend' => $name, + 'user' => $userContext->name, + ] + ); } if (true === (bool)ag($backend, 'import.enabled')) { @@ -313,6 +284,14 @@ class RestoreCommand extends Command 'user' => $userContext->name, ]); + if (false === ($syncRequests = $input->getOption('sync-requests'))) { + $syncRequests = (bool)Config::get('http.default.sync_requests', false); + } + + if (true === $input->getOption('async-requests')) { + $syncRequests = false; + } + $requests = $backend->export($mapper, $this->queue, null); $start = microtime(true); @@ -322,14 +301,7 @@ class RestoreCommand extends Command 'user' => $userContext->name, ]); - foreach ($requests as $response) { - $requestData = $response->getInfo('user_data'); - try { - $requestData['ok']($response); - } catch (Throwable $e) { - $requestData['error']($e); - } - } + send_requests(requests: $requests, client: $this->http, sync: $syncRequests, logger: $this->logger); $this->logger->notice("SYSTEM: Completed '{total}' requests in '{duration}'s for '{user}@{backend}'.", [ 'backend' => $name,