diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php
index d296d1aa..b56ff76e 100644
--- a/module/CLI/config/dependencies.config.php
+++ b/module/CLI/config/dependencies.config.php
@@ -61,6 +61,7 @@ return [
Service\VisitService::class,
IpLocationResolverInterface::class,
Lock\Factory::class,
+ GeolocationDbUpdater::class,
],
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
diff --git a/module/CLI/src/Command/Visit/LocateVisitsCommand.php b/module/CLI/src/Command/Visit/LocateVisitsCommand.php
index 6f34625b..fa458413 100644
--- a/module/CLI/src/Command/Visit/LocateVisitsCommand.php
+++ b/module/CLI/src/Command/Visit/LocateVisitsCommand.php
@@ -3,7 +3,9 @@ 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 Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
@@ -13,6 +15,7 @@ use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
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;
@@ -31,18 +34,25 @@ class LocateVisitsCommand extends Command
private $ipLocationResolver;
/** @var Locker */
private $locker;
- /** @var OutputInterface */
- private $output;
+ /** @var GeolocationDbUpdaterInterface */
+ private $dbUpdater;
+
+ /** @var SymfonyStyle */
+ private $io;
+ /** @var ProgressBar */
+ private $progressBar;
public function __construct(
VisitServiceInterface $visitService,
IpLocationResolverInterface $ipLocationResolver,
- Locker $locker
+ Locker $locker,
+ GeolocationDbUpdaterInterface $dbUpdater
) {
parent::__construct();
$this->visitService = $visitService;
$this->ipLocationResolver = $ipLocationResolver;
$this->locker = $locker;
+ $this->dbUpdater = $dbUpdater;
}
protected function configure(): void
@@ -55,16 +65,17 @@ class LocateVisitsCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
- $this->output = $output;
- $io = new SymfonyStyle($input, $output);
+ $this->io = new SymfonyStyle($input, $output);
$lock = $this->locker->createLock(self::NAME);
if (! $lock->acquire()) {
- $io->warning(sprintf('There is already an instance of the "%s" command in execution', self::NAME));
+ $this->io->warning(sprintf('There is already an instance of the "%s" command in execution', self::NAME));
return ExitCodes::EXIT_WARNING;
}
try {
+ $this->checkDbUpdate();
+
$this->visitService->locateUnlocatedVisits(
[$this, 'getGeolocationDataForVisit'],
function (VisitLocation $location) use ($output) {
@@ -76,7 +87,7 @@ class LocateVisitsCommand extends Command
}
);
- $io->success('Finished processing all IPs');
+ $this->io->success('Finished processing all IPs');
} finally {
$lock->release();
return ExitCodes::EXIT_SUCCESS;
@@ -86,7 +97,7 @@ class LocateVisitsCommand extends Command
public function getGeolocationDataForVisit(Visit $visit): Location
{
if (! $visit->hasRemoteAddr()) {
- $this->output->writeln(
+ $this->io->writeln(
'Ignored visit with no IP address',
OutputInterface::VERBOSITY_VERBOSE
);
@@ -94,21 +105,51 @@ class LocateVisitsCommand extends Command
}
$ipAddr = $visit->getRemoteAddr();
- $this->output->write(sprintf('Processing IP %s>', $ipAddr));
+ $this->io->write(sprintf('Processing IP %s>', $ipAddr));
if ($ipAddr === IpAddress::LOCALHOST) {
- $this->output->writeln(' [Ignored localhost address]');
+ $this->io->writeln(' [Ignored localhost address]');
throw IpCannotBeLocatedException::forLocalhost();
}
try {
return $this->ipLocationResolver->resolveIpLocation($ipAddr);
} catch (WrongIpException $e) {
- $this->output->writeln(' [An error occurred while locating IP. Skipped>]');
- if ($this->output->isVerbose()) {
- $this->getApplication()->renderException($e, $this->output);
+ $this->io->writeln(' [An error occurred while locating IP. Skipped>]');
+ if ($this->io->isVerbose()) {
+ $this->getApplication()->renderException($e, $this->io);
}
throw IpCannotBeLocatedException::forError($e);
}
}
+
+ private function checkDbUpdate(): void
+ {
+ try {
+ $this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) {
+ $this->io->writeln(
+ sprintf('%s GeoLite2 database...>', $olderDbExists ? 'Updating' : 'Downloading')
+ );
+ $this->progressBar = new ProgressBar($this->io);
+ }, function (int $total, int $downloaded) {
+ $this->progressBar->setMaxSteps($total);
+ $this->progressBar->setProgress($downloaded);
+ });
+
+ 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(
+ '[Warning] GeoLite2 database update failed. Proceeding with old version.>'
+ );
+ }
+ }
}
diff --git a/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php b/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php
index af536383..573b0c17 100644
--- a/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php
+++ b/module/CLI/test/Command/Visit/LocateVisitsCommandTest.php
@@ -7,6 +7,8 @@ use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
+use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
+use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\IpGeolocation\IpApiLocationResolver;
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
@@ -36,11 +38,14 @@ class LocateVisitsCommandTest extends TestCase
private $locker;
/** @var ObjectProphecy */
private $lock;
+ /** @var ObjectProphecy */
+ private $dbUpdater;
public function setUp(): void
{
$this->visitService = $this->prophesize(VisitService::class);
$this->ipResolver = $this->prophesize(IpApiLocationResolver::class);
+ $this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
$this->locker = $this->prophesize(Lock\Factory::class);
$this->lock = $this->prophesize(Lock\LockInterface::class);
@@ -52,7 +57,8 @@ class LocateVisitsCommandTest extends TestCase
$command = new LocateVisitsCommand(
$this->visitService->reveal(),
$this->ipResolver->reveal(),
- $this->locker->reveal()
+ $this->locker->reveal(),
+ $this->dbUpdater->reveal()
);
$app = new Application();
$app->add($command);
@@ -182,4 +188,41 @@ class LocateVisitsCommandTest extends TestCase
$locateVisits->shouldNotHaveBeenCalled();
$resolveIpLocation->shouldNotHaveBeenCalled();
}
+
+ /**
+ * @test
+ * @dataProvider provideParams
+ */
+ public function showsProperMessageWhenGeoLiteUpdateFails(bool $olderDbExists, string $expectedMessage): void
+ {
+ $locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function () {
+ });
+ $checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
+ function (array $args) use ($olderDbExists) {
+ [$mustBeUpdated, $handleProgress] = $args;
+
+ $mustBeUpdated($olderDbExists);
+ $handleProgress(100, 50);
+
+ throw GeolocationDbUpdateFailedException::create($olderDbExists);
+ }
+ );
+
+ $this->commandTester->execute([]);
+ $output = $this->commandTester->getDisplay();
+
+ $this->assertStringContainsString(
+ sprintf('%s GeoLite2 database...', $olderDbExists ? 'Updating' : 'Downloading'),
+ $output
+ );
+ $this->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.'];
+ }
}