mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-24 13:49:03 +03:00
Merge pull request #402 from acelaya/feature/update-db-on-process
Feature/update db on process
This commit is contained in:
commit
dddf64031f
14 changed files with 467 additions and 26 deletions
|
@ -8,7 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
|
|
||||||
* *Nothing*
|
* [#377](https://github.com/shlinkio/shlink/issues/377) Updated `visit:locate` command (formerly `visit:process`) to automatically update the GeoLite2 database if it is too old or it does not exist.
|
||||||
|
|
||||||
|
This simplifies processing visits in a container-based infrastructure, since a fresh container is capable of getting an updated version of the file by itself.
|
||||||
|
|
||||||
|
It also removes the need of asynchronously and programmatically updating the file, which deprecates the `visit:update-db` command.
|
||||||
|
|
||||||
#### Changed
|
#### Changed
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ return [
|
||||||
Command\ShortUrl\GeneratePreviewCommand::NAME => Command\ShortUrl\GeneratePreviewCommand::class,
|
Command\ShortUrl\GeneratePreviewCommand::NAME => Command\ShortUrl\GeneratePreviewCommand::class,
|
||||||
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
|
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
|
||||||
|
|
||||||
Command\Visit\ProcessVisitsCommand::NAME => Command\Visit\ProcessVisitsCommand::class,
|
Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class,
|
||||||
Command\Visit\UpdateDbCommand::NAME => Command\Visit\UpdateDbCommand::class,
|
Command\Visit\UpdateDbCommand::NAME => Command\Visit\UpdateDbCommand::class,
|
||||||
|
|
||||||
Command\Config\GenerateCharsetCommand::NAME => Command\Config\GenerateCharsetCommand::class,
|
Command\Config\GenerateCharsetCommand::NAME => Command\Config\GenerateCharsetCommand::class,
|
||||||
|
|
|
@ -3,6 +3,8 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI;
|
namespace Shlinkio\Shlink\CLI;
|
||||||
|
|
||||||
|
use GeoIp2\Database\Reader;
|
||||||
|
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdater;
|
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdater;
|
||||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||||
|
@ -19,6 +21,8 @@ return [
|
||||||
'factories' => [
|
'factories' => [
|
||||||
Application::class => Factory\ApplicationFactory::class,
|
Application::class => Factory\ApplicationFactory::class,
|
||||||
|
|
||||||
|
GeolocationDbUpdater::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Command\ShortUrl\GenerateShortUrlCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\GenerateShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
|
||||||
|
@ -26,7 +30,7 @@ return [
|
||||||
Command\ShortUrl\GeneratePreviewCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\GeneratePreviewCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Command\Visit\ProcessVisitsCommand::class => ConfigAbstractFactory::class,
|
Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Visit\UpdateDbCommand::class => ConfigAbstractFactory::class,
|
Command\Visit\UpdateDbCommand::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Command\Config\GenerateCharsetCommand::class => InvokableFactory::class,
|
Command\Config\GenerateCharsetCommand::class => InvokableFactory::class,
|
||||||
|
@ -44,6 +48,8 @@ return [
|
||||||
],
|
],
|
||||||
|
|
||||||
ConfigAbstractFactory::class => [
|
ConfigAbstractFactory::class => [
|
||||||
|
GeolocationDbUpdater::class => [DbUpdater::class, Reader::class],
|
||||||
|
|
||||||
Command\ShortUrl\GenerateShortUrlCommand::class => [Service\UrlShortener::class, 'config.url_shortener.domain'],
|
Command\ShortUrl\GenerateShortUrlCommand::class => [Service\UrlShortener::class, 'config.url_shortener.domain'],
|
||||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class],
|
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class],
|
||||||
Command\ShortUrl\ListShortUrlsCommand::class => [Service\ShortUrlService::class, 'config.url_shortener.domain'],
|
Command\ShortUrl\ListShortUrlsCommand::class => [Service\ShortUrlService::class, 'config.url_shortener.domain'],
|
||||||
|
@ -51,10 +57,11 @@ return [
|
||||||
Command\ShortUrl\GeneratePreviewCommand::class => [Service\ShortUrlService::class, PreviewGenerator::class],
|
Command\ShortUrl\GeneratePreviewCommand::class => [Service\ShortUrlService::class, PreviewGenerator::class],
|
||||||
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
|
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
|
||||||
|
|
||||||
Command\Visit\ProcessVisitsCommand::class => [
|
Command\Visit\LocateVisitsCommand::class => [
|
||||||
Service\VisitService::class,
|
Service\VisitService::class,
|
||||||
IpLocationResolverInterface::class,
|
IpLocationResolverInterface::class,
|
||||||
Lock\Factory::class,
|
Lock\Factory::class,
|
||||||
|
GeolocationDbUpdater::class,
|
||||||
],
|
],
|
||||||
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
|
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,9 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
|
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
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\Exception\IpCannotBeLocatedException;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
|
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Helper\ProgressBar;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
@ -20,9 +23,10 @@ use Symfony\Component\Lock\Factory as Locker;
|
||||||
|
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
|
|
||||||
class ProcessVisitsCommand extends Command
|
class LocateVisitsCommand extends Command
|
||||||
{
|
{
|
||||||
public const NAME = 'visit:process';
|
public const NAME = 'visit:locate';
|
||||||
|
public const ALIASES = ['visit:process'];
|
||||||
|
|
||||||
/** @var VisitServiceInterface */
|
/** @var VisitServiceInterface */
|
||||||
private $visitService;
|
private $visitService;
|
||||||
|
@ -30,39 +34,48 @@ class ProcessVisitsCommand extends Command
|
||||||
private $ipLocationResolver;
|
private $ipLocationResolver;
|
||||||
/** @var Locker */
|
/** @var Locker */
|
||||||
private $locker;
|
private $locker;
|
||||||
/** @var OutputInterface */
|
/** @var GeolocationDbUpdaterInterface */
|
||||||
private $output;
|
private $dbUpdater;
|
||||||
|
|
||||||
|
/** @var SymfonyStyle */
|
||||||
|
private $io;
|
||||||
|
/** @var ProgressBar */
|
||||||
|
private $progressBar;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
VisitServiceInterface $visitService,
|
VisitServiceInterface $visitService,
|
||||||
IpLocationResolverInterface $ipLocationResolver,
|
IpLocationResolverInterface $ipLocationResolver,
|
||||||
Locker $locker
|
Locker $locker,
|
||||||
|
GeolocationDbUpdaterInterface $dbUpdater
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->visitService = $visitService;
|
$this->visitService = $visitService;
|
||||||
$this->ipLocationResolver = $ipLocationResolver;
|
$this->ipLocationResolver = $ipLocationResolver;
|
||||||
$this->locker = $locker;
|
$this->locker = $locker;
|
||||||
|
$this->dbUpdater = $dbUpdater;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
->setDescription('Processes visits where location is not set yet');
|
->setAliases(self::ALIASES)
|
||||||
|
->setDescription('Resolves visits origin locations.');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||||
{
|
{
|
||||||
$this->output = $output;
|
$this->io = new SymfonyStyle($input, $output);
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
|
|
||||||
$lock = $this->locker->createLock(self::NAME);
|
$lock = $this->locker->createLock(self::NAME);
|
||||||
if (! $lock->acquire()) {
|
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;
|
return ExitCodes::EXIT_WARNING;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
$this->checkDbUpdate();
|
||||||
|
|
||||||
$this->visitService->locateUnlocatedVisits(
|
$this->visitService->locateUnlocatedVisits(
|
||||||
[$this, 'getGeolocationDataForVisit'],
|
[$this, 'getGeolocationDataForVisit'],
|
||||||
function (VisitLocation $location) use ($output) {
|
function (VisitLocation $location) use ($output) {
|
||||||
|
@ -74,7 +87,7 @@ class ProcessVisitsCommand extends Command
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
$io->success('Finished processing all IPs');
|
$this->io->success('Finished processing all IPs');
|
||||||
} finally {
|
} finally {
|
||||||
$lock->release();
|
$lock->release();
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
return ExitCodes::EXIT_SUCCESS;
|
||||||
|
@ -84,7 +97,7 @@ class ProcessVisitsCommand extends Command
|
||||||
public function getGeolocationDataForVisit(Visit $visit): Location
|
public function getGeolocationDataForVisit(Visit $visit): Location
|
||||||
{
|
{
|
||||||
if (! $visit->hasRemoteAddr()) {
|
if (! $visit->hasRemoteAddr()) {
|
||||||
$this->output->writeln(
|
$this->io->writeln(
|
||||||
'<comment>Ignored visit with no IP address</comment>',
|
'<comment>Ignored visit with no IP address</comment>',
|
||||||
OutputInterface::VERBOSITY_VERBOSE
|
OutputInterface::VERBOSITY_VERBOSE
|
||||||
);
|
);
|
||||||
|
@ -92,21 +105,51 @@ class ProcessVisitsCommand extends Command
|
||||||
}
|
}
|
||||||
|
|
||||||
$ipAddr = $visit->getRemoteAddr();
|
$ipAddr = $visit->getRemoteAddr();
|
||||||
$this->output->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
|
$this->io->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
|
||||||
if ($ipAddr === IpAddress::LOCALHOST) {
|
if ($ipAddr === IpAddress::LOCALHOST) {
|
||||||
$this->output->writeln(' [<comment>Ignored localhost address</comment>]');
|
$this->io->writeln(' [<comment>Ignored localhost address</comment>]');
|
||||||
throw IpCannotBeLocatedException::forLocalhost();
|
throw IpCannotBeLocatedException::forLocalhost();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
return $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
||||||
} catch (WrongIpException $e) {
|
} catch (WrongIpException $e) {
|
||||||
$this->output->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
|
$this->io->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
|
||||||
if ($this->output->isVerbose()) {
|
if ($this->io->isVerbose()) {
|
||||||
$this->getApplication()->renderException($e, $this->output);
|
$this->getApplication()->renderException($e, $this->io);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw IpCannotBeLocatedException::forError($e);
|
throw IpCannotBeLocatedException::forError($e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function checkDbUpdate(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) {
|
||||||
|
$this->io->writeln(
|
||||||
|
sprintf('<fg=blue>%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(
|
||||||
|
'<fg=yellow;options=bold>[Warning] GeoLite2 database update failed. Proceeding with old version.</>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -15,6 +15,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
class UpdateDbCommand extends Command
|
class UpdateDbCommand extends Command
|
||||||
{
|
{
|
||||||
public const NAME = 'visit:update-db';
|
public const NAME = 'visit:update-db';
|
||||||
|
@ -32,7 +33,7 @@ class UpdateDbCommand extends Command
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
->setDescription('Updates the GeoLite2 database file used to geolocate IP addresses')
|
->setDescription('[DEPRECATED] Updates the GeoLite2 database file used to geolocate IP addresses')
|
||||||
->setHelp(
|
->setHelp(
|
||||||
'The GeoLite2 database is updated first Tuesday every month, so this command should be ideally run '
|
'The GeoLite2 database is updated first Tuesday every month, so this command should be ideally run '
|
||||||
. 'every first Wednesday'
|
. 'every first Wednesday'
|
||||||
|
|
10
module/CLI/src/Exception/ExceptionInterface.php
Normal file
10
module/CLI/src/Exception/ExceptionInterface.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Exception;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
interface ExceptionInterface extends Throwable
|
||||||
|
{
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Exception;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class GeolocationDbUpdateFailedException extends RuntimeException implements ExceptionInterface
|
||||||
|
{
|
||||||
|
/** @var bool */
|
||||||
|
private $olderDbExists;
|
||||||
|
|
||||||
|
public function __construct(bool $olderDbExists, string $message = '', int $code = 0, Throwable $previous = null)
|
||||||
|
{
|
||||||
|
$this->olderDbExists = $olderDbExists;
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create(bool $olderDbExists, Throwable $prev = null): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$olderDbExists,
|
||||||
|
'An error occurred while updating geolocation database, and an older version could not be found',
|
||||||
|
0,
|
||||||
|
$prev
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function olderDbExists(): bool
|
||||||
|
{
|
||||||
|
return $this->olderDbExists;
|
||||||
|
}
|
||||||
|
}
|
67
module/CLI/src/Util/GeolocationDbUpdater.php
Normal file
67
module/CLI/src/Util/GeolocationDbUpdater.php
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Util;
|
||||||
|
|
||||||
|
use Cake\Chronos\Chronos;
|
||||||
|
use GeoIp2\Database\Reader;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||||
|
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||||
|
|
||||||
|
class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
||||||
|
{
|
||||||
|
/** @var DbUpdaterInterface */
|
||||||
|
private $dbUpdater;
|
||||||
|
/** @var Reader */
|
||||||
|
private $geoLiteDbReader;
|
||||||
|
|
||||||
|
public function __construct(DbUpdaterInterface $dbUpdater, Reader $geoLiteDbReader)
|
||||||
|
{
|
||||||
|
$this->dbUpdater = $dbUpdater;
|
||||||
|
$this->geoLiteDbReader = $geoLiteDbReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws GeolocationDbUpdateFailedException
|
||||||
|
*/
|
||||||
|
public function checkDbUpdate(callable $mustBeUpdated = null, callable $handleProgress = null): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$meta = $this->geoLiteDbReader->metadata();
|
||||||
|
if ($this->buildIsTooOld($meta->__get('buildEpoch'))) {
|
||||||
|
$this->downloadNewDb(true, $mustBeUpdated, $handleProgress);
|
||||||
|
}
|
||||||
|
} catch (InvalidArgumentException $e) {
|
||||||
|
// This is the exception thrown by the reader when the database file does not exist
|
||||||
|
$this->downloadNewDb(false, $mustBeUpdated, $handleProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildIsTooOld(int $buildTimestamp): bool
|
||||||
|
{
|
||||||
|
$buildDate = Chronos::createFromTimestamp($buildTimestamp);
|
||||||
|
$now = Chronos::now();
|
||||||
|
return $now->gt($buildDate->addDays(35));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws GeolocationDbUpdateFailedException
|
||||||
|
*/
|
||||||
|
private function downloadNewDb(
|
||||||
|
bool $olderDbExists,
|
||||||
|
callable $mustBeUpdated = null,
|
||||||
|
callable $handleProgress = null
|
||||||
|
): void {
|
||||||
|
if ($mustBeUpdated !== null) {
|
||||||
|
$mustBeUpdated($olderDbExists);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->dbUpdater->downloadFreshCopy($handleProgress);
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
throw GeolocationDbUpdateFailedException::create($olderDbExists, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
module/CLI/src/Util/GeolocationDbUpdaterInterface.php
Normal file
14
module/CLI/src/Util/GeolocationDbUpdaterInterface.php
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Util;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
|
|
||||||
|
interface GeolocationDbUpdaterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws GeolocationDbUpdateFailedException
|
||||||
|
*/
|
||||||
|
public function checkDbUpdate(callable $mustBeUpdated = null, callable $handleProgress = null): void;
|
||||||
|
}
|
|
@ -6,7 +6,9 @@ namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
|
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\Exception\WrongIpException;
|
||||||
use Shlinkio\Shlink\Common\IpGeolocation\IpApiLocationResolver;
|
use Shlinkio\Shlink\Common\IpGeolocation\IpApiLocationResolver;
|
||||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||||
|
@ -24,7 +26,7 @@ use Symfony\Component\Lock;
|
||||||
use function array_shift;
|
use function array_shift;
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
|
|
||||||
class ProcessVisitsCommandTest extends TestCase
|
class LocateVisitsCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
/** @var CommandTester */
|
/** @var CommandTester */
|
||||||
private $commandTester;
|
private $commandTester;
|
||||||
|
@ -36,11 +38,14 @@ class ProcessVisitsCommandTest extends TestCase
|
||||||
private $locker;
|
private $locker;
|
||||||
/** @var ObjectProphecy */
|
/** @var ObjectProphecy */
|
||||||
private $lock;
|
private $lock;
|
||||||
|
/** @var ObjectProphecy */
|
||||||
|
private $dbUpdater;
|
||||||
|
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
$this->visitService = $this->prophesize(VisitService::class);
|
$this->visitService = $this->prophesize(VisitService::class);
|
||||||
$this->ipResolver = $this->prophesize(IpApiLocationResolver::class);
|
$this->ipResolver = $this->prophesize(IpApiLocationResolver::class);
|
||||||
|
$this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
|
||||||
|
|
||||||
$this->locker = $this->prophesize(Lock\Factory::class);
|
$this->locker = $this->prophesize(Lock\Factory::class);
|
||||||
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
||||||
|
@ -49,10 +54,11 @@ class ProcessVisitsCommandTest extends TestCase
|
||||||
});
|
});
|
||||||
$this->locker->createLock(Argument::type('string'))->willReturn($this->lock->reveal());
|
$this->locker->createLock(Argument::type('string'))->willReturn($this->lock->reveal());
|
||||||
|
|
||||||
$command = new ProcessVisitsCommand(
|
$command = new LocateVisitsCommand(
|
||||||
$this->visitService->reveal(),
|
$this->visitService->reveal(),
|
||||||
$this->ipResolver->reveal(),
|
$this->ipResolver->reveal(),
|
||||||
$this->locker->reveal()
|
$this->locker->reveal(),
|
||||||
|
$this->dbUpdater->reveal()
|
||||||
);
|
);
|
||||||
$app = new Application();
|
$app = new Application();
|
||||||
$app->add($command);
|
$app->add($command);
|
||||||
|
@ -176,10 +182,47 @@ class ProcessVisitsCommandTest extends TestCase
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertStringContainsString(
|
$this->assertStringContainsString(
|
||||||
sprintf('There is already an instance of the "%s" command', ProcessVisitsCommand::NAME),
|
sprintf('There is already an instance of the "%s" command', LocateVisitsCommand::NAME),
|
||||||
$output
|
$output
|
||||||
);
|
);
|
||||||
$locateVisits->shouldNotHaveBeenCalled();
|
$locateVisits->shouldNotHaveBeenCalled();
|
||||||
$resolveIpLocation->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.'];
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use RuntimeException;
|
||||||
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class GeolocationDbUpdateFailedExceptionTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider provideOlderDbExists
|
||||||
|
*/
|
||||||
|
public function constructCreatesExceptionWithDefaultArgs(bool $olderDbExists): void
|
||||||
|
{
|
||||||
|
$e = new GeolocationDbUpdateFailedException($olderDbExists);
|
||||||
|
|
||||||
|
$this->assertEquals($olderDbExists, $e->olderDbExists());
|
||||||
|
$this->assertEquals('', $e->getMessage());
|
||||||
|
$this->assertEquals(0, $e->getCode());
|
||||||
|
$this->assertNull($e->getPrevious());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideOlderDbExists(): iterable
|
||||||
|
{
|
||||||
|
yield 'with older DB' => [true];
|
||||||
|
yield 'without older DB' => [false];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider provideConstructorArgs
|
||||||
|
*/
|
||||||
|
public function constructCreatesException(bool $olderDbExists, string $message, int $code, ?Throwable $prev): void
|
||||||
|
{
|
||||||
|
$e = new GeolocationDbUpdateFailedException($olderDbExists, $message, $code, $prev);
|
||||||
|
|
||||||
|
$this->assertEquals($olderDbExists, $e->olderDbExists());
|
||||||
|
$this->assertEquals($message, $e->getMessage());
|
||||||
|
$this->assertEquals($code, $e->getCode());
|
||||||
|
$this->assertEquals($prev, $e->getPrevious());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideConstructorArgs(): iterable
|
||||||
|
{
|
||||||
|
yield [true, 'This is a nice error message', 99, new Exception('prev')];
|
||||||
|
yield [false, 'Another message', 0, new RuntimeException('prev')];
|
||||||
|
yield [true, 'An yet another message', -50, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider provideCreateArgs
|
||||||
|
*/
|
||||||
|
public function createBuildsException(bool $olderDbExists, ?Throwable $prev): void
|
||||||
|
{
|
||||||
|
$e = GeolocationDbUpdateFailedException::create($olderDbExists, $prev);
|
||||||
|
|
||||||
|
$this->assertEquals($olderDbExists, $e->olderDbExists());
|
||||||
|
$this->assertEquals(
|
||||||
|
'An error occurred while updating geolocation database, and an older version could not be found',
|
||||||
|
$e->getMessage()
|
||||||
|
);
|
||||||
|
$this->assertEquals(0, $e->getCode());
|
||||||
|
$this->assertEquals($prev, $e->getPrevious());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideCreateArgs(): iterable
|
||||||
|
{
|
||||||
|
yield 'older DB and no prev' => [true, null];
|
||||||
|
yield 'older DB and prev' => [true, new RuntimeException('prev')];
|
||||||
|
yield 'no older DB and no prev' => [false, null];
|
||||||
|
yield 'no older DB and prev' => [false, new Exception('prev')];
|
||||||
|
}
|
||||||
|
}
|
139
module/CLI/test/Util/GeolocationDbUpdaterTest.php
Normal file
139
module/CLI/test/Util/GeolocationDbUpdaterTest.php
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Util;
|
||||||
|
|
||||||
|
use Cake\Chronos\Chronos;
|
||||||
|
use GeoIp2\Database\Reader;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use MaxMind\Db\Reader\Metadata;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
|
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||||
|
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use function Functional\map;
|
||||||
|
use function range;
|
||||||
|
|
||||||
|
class GeolocationDbUpdaterTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var GeolocationDbUpdater */
|
||||||
|
private $geolocationDbUpdater;
|
||||||
|
/** @var ObjectProphecy */
|
||||||
|
private $dbUpdater;
|
||||||
|
/** @var ObjectProphecy */
|
||||||
|
private $geoLiteDbReader;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
|
||||||
|
$this->geoLiteDbReader = $this->prophesize(Reader::class);
|
||||||
|
|
||||||
|
$this->geolocationDbUpdater = new GeolocationDbUpdater(
|
||||||
|
$this->dbUpdater->reveal(),
|
||||||
|
$this->geoLiteDbReader->reveal()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void
|
||||||
|
{
|
||||||
|
$mustBeUpdated = function () {
|
||||||
|
$this->assertTrue(true);
|
||||||
|
};
|
||||||
|
$getMeta = $this->geoLiteDbReader->metadata()->willThrow(InvalidArgumentException::class);
|
||||||
|
$prev = new RuntimeException('');
|
||||||
|
$download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->geolocationDbUpdater->checkDbUpdate($mustBeUpdated);
|
||||||
|
$this->assertTrue(false); // If this is reached, the test will fail
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
/** @var GeolocationDbUpdateFailedException $e */
|
||||||
|
$this->assertInstanceOf(GeolocationDbUpdateFailedException::class, $e);
|
||||||
|
$this->assertSame($prev, $e->getPrevious());
|
||||||
|
$this->assertFalse($e->olderDbExists());
|
||||||
|
}
|
||||||
|
|
||||||
|
$getMeta->shouldHaveBeenCalledOnce();
|
||||||
|
$download->shouldHaveBeenCalledOnce();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider provideBigDays
|
||||||
|
*/
|
||||||
|
public function exceptionIsThrownWhenOlderDbIsTooOldAndDownloadFails(int $days): void
|
||||||
|
{
|
||||||
|
$getMeta = $this->geoLiteDbReader->metadata()->willReturn(new Metadata([
|
||||||
|
'binary_format_major_version' => '',
|
||||||
|
'binary_format_minor_version' => '',
|
||||||
|
'build_epoch' => Chronos::now()->subDays($days)->getTimestamp(),
|
||||||
|
'database_type' => '',
|
||||||
|
'languages' => '',
|
||||||
|
'description' => '',
|
||||||
|
'ip_version' => '',
|
||||||
|
'node_count' => 1,
|
||||||
|
'record_size' => 4,
|
||||||
|
]));
|
||||||
|
$prev = new RuntimeException('');
|
||||||
|
$download = $this->dbUpdater->downloadFreshCopy(null)->willThrow($prev);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->geolocationDbUpdater->checkDbUpdate();
|
||||||
|
$this->assertTrue(false); // If this is reached, the test will fail
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
/** @var GeolocationDbUpdateFailedException $e */
|
||||||
|
$this->assertInstanceOf(GeolocationDbUpdateFailedException::class, $e);
|
||||||
|
$this->assertSame($prev, $e->getPrevious());
|
||||||
|
$this->assertTrue($e->olderDbExists());
|
||||||
|
}
|
||||||
|
|
||||||
|
$getMeta->shouldHaveBeenCalledOnce();
|
||||||
|
$download->shouldHaveBeenCalledOnce();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideBigDays(): iterable
|
||||||
|
{
|
||||||
|
yield [36];
|
||||||
|
yield [50];
|
||||||
|
yield [75];
|
||||||
|
yield [100];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider provideSmallDays
|
||||||
|
*/
|
||||||
|
public function databaseIsNotUpdatedIfItIsYoungerThanOneWeek(int $days): void
|
||||||
|
{
|
||||||
|
$getMeta = $this->geoLiteDbReader->metadata()->willReturn(new Metadata([
|
||||||
|
'binary_format_major_version' => '',
|
||||||
|
'binary_format_minor_version' => '',
|
||||||
|
'build_epoch' => Chronos::now()->subDays($days)->getTimestamp(),
|
||||||
|
'database_type' => '',
|
||||||
|
'languages' => '',
|
||||||
|
'description' => '',
|
||||||
|
'ip_version' => '',
|
||||||
|
'node_count' => 1,
|
||||||
|
'record_size' => 4,
|
||||||
|
]));
|
||||||
|
$download = $this->dbUpdater->downloadFreshCopy(null)->will(function () {
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->geolocationDbUpdater->checkDbUpdate();
|
||||||
|
|
||||||
|
$getMeta->shouldHaveBeenCalledOnce();
|
||||||
|
$download->shouldNotHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideSmallDays(): iterable
|
||||||
|
{
|
||||||
|
return map(range(0, 34), function (int $days) {
|
||||||
|
return [$days];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue