mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-25 18:30:23 -06:00
Created new command containing the logic to download the GeoLite2 db file
This commit is contained in:
parent
74ea5969be
commit
f7b6f4ba19
@ -15,6 +15,7 @@ return [
|
||||
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
|
||||
|
||||
Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class,
|
||||
Command\Visit\DownloadGeoLiteDbCommand::NAME => Command\Visit\DownloadGeoLiteDbCommand::class,
|
||||
|
||||
Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
|
||||
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,
|
||||
|
@ -44,6 +44,7 @@ return [
|
||||
Command\ShortUrl\GetVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Visit\DownloadGeoLiteDbCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
|
||||
@ -80,11 +81,11 @@ return [
|
||||
Command\ShortUrl\GetVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
|
||||
|
||||
Command\Visit\DownloadGeoLiteDbCommand::class => [Util\GeolocationDbUpdater::class],
|
||||
Command\Visit\LocateVisitsCommand::class => [
|
||||
Visit\VisitLocator::class,
|
||||
IpLocationResolverInterface::class,
|
||||
LockFactory::class,
|
||||
Util\GeolocationDbUpdater::class,
|
||||
],
|
||||
|
||||
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class],
|
||||
|
80
module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php
Normal file
80
module/CLI/src/Command/Visit/DownloadGeoLiteDbCommand.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DownloadGeoLiteDbCommand extends Command
|
||||
{
|
||||
public const NAME = 'visit:download-db';
|
||||
|
||||
private GeolocationDbUpdaterInterface $dbUpdater;
|
||||
private ?ProgressBar $progressBar = null;
|
||||
|
||||
public function __construct(GeolocationDbUpdaterInterface $dbUpdater)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->dbUpdater = $dbUpdater;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription(
|
||||
'Checks if the GeoLite2 db file is too old or it does not exist, and tries to download an up-to-date '
|
||||
. 'copy if so.',
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
try {
|
||||
$this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) use ($io): void {
|
||||
$io->text(sprintf('<fg=blue>%s GeoLite2 db file...</>', $olderDbExists ? 'Updating' : 'Downloading'));
|
||||
$this->progressBar = new ProgressBar($io);
|
||||
}, function (int $total, int $downloaded): void {
|
||||
$this->progressBar->setMaxSteps($total);
|
||||
$this->progressBar->setProgress($downloaded);
|
||||
});
|
||||
|
||||
if ($this->progressBar !== null) {
|
||||
$this->progressBar->finish();
|
||||
$io->success('GeoLite2 db file properly downloaded.');
|
||||
} else {
|
||||
$io->info('GeoLite2 db file is up to date.');
|
||||
}
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (GeolocationDbUpdateFailedException $e) {
|
||||
$olderDbExists = $e->olderDbExists();
|
||||
|
||||
if ($olderDbExists) {
|
||||
$io->warning(
|
||||
'GeoLite2 db file update failed. Visits will continue to be located with the old version.',
|
||||
);
|
||||
} else {
|
||||
$io->error('GeoLite2 db file download failed. It will not be possible to locate visits.');
|
||||
}
|
||||
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()->renderThrowable($e, $io);
|
||||
}
|
||||
|
||||
return $olderDbExists ? ExitCodes::EXIT_WARNING : ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,9 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
@ -19,7 +17,6 @@ use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
@ -35,28 +32,26 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
||||
|
||||
private VisitLocatorInterface $visitLocator;
|
||||
private IpLocationResolverInterface $ipLocationResolver;
|
||||
private GeolocationDbUpdaterInterface $dbUpdater;
|
||||
|
||||
private SymfonyStyle $io;
|
||||
private ?ProgressBar $progressBar = null;
|
||||
|
||||
public function __construct(
|
||||
VisitLocatorInterface $visitLocator,
|
||||
IpLocationResolverInterface $ipLocationResolver,
|
||||
LockFactory $locker,
|
||||
GeolocationDbUpdaterInterface $dbUpdater
|
||||
LockFactory $locker
|
||||
) {
|
||||
parent::__construct($locker);
|
||||
$this->visitLocator = $visitLocator;
|
||||
$this->ipLocationResolver = $ipLocationResolver;
|
||||
$this->dbUpdater = $dbUpdater;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Resolves visits origin locations.')
|
||||
->setDescription(
|
||||
'Resolves visits origin locations. It implicitly downloads/updates the GeoLite2 db file if needed.',
|
||||
)
|
||||
->addOption(
|
||||
'retry',
|
||||
'r',
|
||||
@ -90,12 +85,12 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
||||
);
|
||||
}
|
||||
|
||||
if ($all && $retry && ! $this->warnAndVerifyContinue()) {
|
||||
if ($all && $retry && ! $this->warnAndVerifyContinue($input)) {
|
||||
throw new RuntimeException('Execution aborted');
|
||||
}
|
||||
}
|
||||
|
||||
private function warnAndVerifyContinue(): bool
|
||||
private function warnAndVerifyContinue(InputInterface $input): bool
|
||||
{
|
||||
$this->io->warning([
|
||||
'You are about to process the location of all existing visits your short URLs received.',
|
||||
@ -113,7 +108,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
||||
$all = $retry && $input->getOption('all');
|
||||
|
||||
try {
|
||||
$this->checkDbUpdate();
|
||||
$this->checkDbUpdate($input);
|
||||
|
||||
if ($all) {
|
||||
$this->visitLocator->locateAllVisits($this);
|
||||
@ -128,7 +123,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (Throwable $e) {
|
||||
$this->io->error($e->getMessage());
|
||||
if ($e instanceof Throwable && $this->io->isVerbose()) {
|
||||
if ($this->io->isVerbose()) {
|
||||
$this->getApplication()->renderThrowable($e, $this->io);
|
||||
}
|
||||
|
||||
@ -176,33 +171,13 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
||||
$this->io->writeln($message);
|
||||
}
|
||||
|
||||
private function checkDbUpdate(): void
|
||||
private function checkDbUpdate(InputInterface $input): void
|
||||
{
|
||||
try {
|
||||
$this->dbUpdater->checkDbUpdate(function (bool $olderDbExists): void {
|
||||
$this->io->writeln(
|
||||
sprintf('<fg=blue>%s GeoLite2 db file...</>', $olderDbExists ? 'Updating' : 'Downloading'),
|
||||
);
|
||||
$this->progressBar = new ProgressBar($this->io);
|
||||
}, function (int $total, int $downloaded): void {
|
||||
$this->progressBar->setMaxSteps($total);
|
||||
$this->progressBar->setProgress($downloaded);
|
||||
});
|
||||
$downloadDbCommand = $this->getApplication()->find(DownloadGeoLiteDbCommand::NAME);
|
||||
$exitCode = $downloadDbCommand->run($input, $this->io);
|
||||
|
||||
if ($this->progressBar !== null) {
|
||||
$this->progressBar->finish();
|
||||
$this->io->newLine();
|
||||
}
|
||||
} catch (GeolocationDbUpdateFailedException $e) {
|
||||
if (! $e->olderDbExists()) {
|
||||
$this->io->error('GeoLite2 database download failed. It is not possible to locate visits.');
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->io->newLine();
|
||||
$this->io->writeln(
|
||||
'<fg=yellow;options=bold>[Warning] GeoLite2 database update failed. Proceeding with old version.</>',
|
||||
);
|
||||
if ($exitCode === ExitCodes::EXIT_FAILURE) {
|
||||
throw new RuntimeException('It is not possible to locate visits without a GeoLite2 db file.');
|
||||
}
|
||||
}
|
||||
|
||||
|
32
module/CLI/test/CliTestUtilsTrait.php
Normal file
32
module/CLI/test/CliTestUtilsTrait.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI;
|
||||
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
|
||||
trait CliTestUtilsTrait
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @return ObjectProphecy|Command
|
||||
*/
|
||||
private function createCommandMock(string $name): ObjectProphecy
|
||||
{
|
||||
$command = $this->prophesize(Command::class);
|
||||
$command->getName()->willReturn($name);
|
||||
$command->getDefinition()->willReturn($name);
|
||||
$command->isEnabled()->willReturn(true);
|
||||
$command->getAliases()->willReturn([]);
|
||||
$command->setApplication(Argument::type(Application::class))->willReturn(function (): void {
|
||||
});
|
||||
|
||||
return $command;
|
||||
}
|
||||
}
|
@ -6,11 +6,10 @@ namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
@ -21,6 +20,7 @@ use Shlinkio\Shlink\Core\Visit\VisitLocator;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
@ -33,19 +33,18 @@ use const PHP_EOL;
|
||||
|
||||
class LocateVisitsCommandTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $visitService;
|
||||
private ObjectProphecy $ipResolver;
|
||||
private ObjectProphecy $lock;
|
||||
private ObjectProphecy $dbUpdater;
|
||||
private ObjectProphecy $downloadDbCommand;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->visitService = $this->prophesize(VisitLocator::class);
|
||||
$this->ipResolver = $this->prophesize(IpLocationResolverInterface::class);
|
||||
$this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
|
||||
|
||||
$locker = $this->prophesize(Lock\LockFactory::class);
|
||||
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
||||
@ -58,11 +57,14 @@ class LocateVisitsCommandTest extends TestCase
|
||||
$this->visitService->reveal(),
|
||||
$this->ipResolver->reveal(),
|
||||
$locker->reveal(),
|
||||
$this->dbUpdater->reveal(),
|
||||
);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->downloadDbCommand = $this->createCommandMock(DownloadGeoLiteDbCommand::NAME);
|
||||
$this->downloadDbCommand->run(Argument::cetera())->willReturn(ExitCodes::EXIT_SUCCESS);
|
||||
$app->add($this->downloadDbCommand->reveal());
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
@ -202,44 +204,56 @@ class LocateVisitsCommandTest extends TestCase
|
||||
$resolveIpLocation->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideParams
|
||||
*/
|
||||
public function showsProperMessageWhenGeoLiteUpdateFails(bool $olderDbExists, string $expectedMessage): void
|
||||
/** @test */
|
||||
public function showsProperMessageWhenGeoLiteUpdateFails(): void
|
||||
{
|
||||
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function (): void {
|
||||
});
|
||||
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
|
||||
function (array $args) use ($olderDbExists): void {
|
||||
[$mustBeUpdated, $handleProgress] = $args;
|
||||
|
||||
$mustBeUpdated($olderDbExists);
|
||||
$handleProgress(100, 50);
|
||||
|
||||
throw $olderDbExists
|
||||
? GeolocationDbUpdateFailedException::withOlderDb()
|
||||
: GeolocationDbUpdateFailedException::withoutOlderDb();
|
||||
},
|
||||
);
|
||||
$this->downloadDbCommand->run(Argument::cetera())->willReturn(ExitCodes::EXIT_FAILURE);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString(
|
||||
sprintf('%s GeoLite2 db file...', $olderDbExists ? 'Updating' : 'Downloading'),
|
||||
$output,
|
||||
);
|
||||
self::assertStringContainsString($expectedMessage, $output);
|
||||
$locateVisits->shouldHaveBeenCalledTimes((int) $olderDbExists);
|
||||
$checkDbUpdate->shouldHaveBeenCalledOnce();
|
||||
self::assertStringContainsString('It is not possible to locate visits without a GeoLite2 db file.', $output);
|
||||
$this->visitService->locateUnlocatedVisits(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function provideParams(): iterable
|
||||
{
|
||||
yield [true, '[Warning] GeoLite2 database update failed. Proceeding with old version.'];
|
||||
yield [false, 'GeoLite2 database download failed. It is not possible to locate visits.'];
|
||||
}
|
||||
// /**
|
||||
// * @test
|
||||
// * @dataProvider provideParams
|
||||
// */
|
||||
// public function showsProperMessageWhenGeoLiteUpdateFails(bool $olderDbExists, string $expectedMessage): void
|
||||
// {
|
||||
// $locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function (): void {
|
||||
// });
|
||||
// $checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
|
||||
// function (array $args) use ($olderDbExists): void {
|
||||
// [$mustBeUpdated, $handleProgress] = $args;
|
||||
//
|
||||
// $mustBeUpdated($olderDbExists);
|
||||
// $handleProgress(100, 50);
|
||||
//
|
||||
// throw $olderDbExists
|
||||
// ? GeolocationDbUpdateFailedException::withOlderDb()
|
||||
// : GeolocationDbUpdateFailedException::withoutOlderDb();
|
||||
// },
|
||||
// );
|
||||
//
|
||||
// $this->commandTester->execute([]);
|
||||
// $output = $this->commandTester->getDisplay();
|
||||
//
|
||||
// self::assertStringContainsString(
|
||||
// sprintf('%s GeoLite2 db file...', $olderDbExists ? 'Updating' : 'Downloading'),
|
||||
// $output,
|
||||
// );
|
||||
// self::assertStringContainsString($expectedMessage, $output);
|
||||
// $locateVisits->shouldHaveBeenCalledTimes((int) $olderDbExists);
|
||||
// $checkDbUpdate->shouldHaveBeenCalledOnce();
|
||||
// }
|
||||
//
|
||||
// public function provideParams(): iterable
|
||||
// {
|
||||
// yield [true, '[Warning] GeoLite2 database update failed. Proceeding with old version.'];
|
||||
// yield [false, 'GeoLite2 database download failed. It is not possible to locate visits.'];
|
||||
// }
|
||||
|
||||
/** @test */
|
||||
public function providingAllFlagOnItsOwnDisplaysNotice(): void
|
||||
|
@ -6,17 +6,13 @@ namespace ShlinkioTest\Shlink\CLI\Factory;
|
||||
|
||||
use Laminas\ServiceManager\ServiceManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
|
||||
class ApplicationFactoryTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private ApplicationFactory $factory;
|
||||
|
||||
@ -54,17 +50,4 @@ class ApplicationFactoryTest extends TestCase
|
||||
AppOptions::class => new AppOptions(),
|
||||
]]);
|
||||
}
|
||||
|
||||
private function createCommandMock(string $name): ObjectProphecy
|
||||
{
|
||||
$command = $this->prophesize(Command::class);
|
||||
$command->getName()->willReturn($name);
|
||||
$command->getDefinition()->willReturn($name);
|
||||
$command->isEnabled()->willReturn(true);
|
||||
$command->getAliases()->willReturn([]);
|
||||
$command->setApplication(Argument::type(Application::class))->willReturn(function (): void {
|
||||
});
|
||||
|
||||
return $command;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user