Created service to update geolite2 database file

This commit is contained in:
Alejandro Celaya 2018-11-11 21:08:23 +01:00
parent 06db082e3f
commit 3d7cf6992e
8 changed files with 302 additions and 0 deletions

View File

@ -5,6 +5,8 @@ return [
'geolite2' => [
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
'temp_dir' => sys_get_temp_dir(),
'download_from' => 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz',
],
];

View File

@ -37,6 +37,8 @@ return [
IpGeolocation\IpApiLocationResolver::class => ConfigAbstractFactory::class,
IpGeolocation\GeoLite2LocationResolver::class => ConfigAbstractFactory::class,
IpGeolocation\ChainIpLocationResolver::class => ConfigAbstractFactory::class,
IpGeolocation\GeoLite2\GeoLite2Options::class => ConfigAbstractFactory::class,
IpGeolocation\GeoLite2\DbUpdater::class => ConfigAbstractFactory::class,
Service\PreviewGenerator::class => ConfigAbstractFactory::class,
],
@ -68,6 +70,12 @@ return [
IpGeolocation\GeoLite2LocationResolver::class,
IpGeolocation\IpApiLocationResolver::class,
],
IpGeolocation\GeoLite2\GeoLite2Options::class => ['config.geolite2'],
IpGeolocation\GeoLite2\DbUpdater::class => [
GuzzleClient::class,
Filesystem::class,
IpGeolocation\GeoLite2\GeoLite2Options::class,
],
Service\PreviewGenerator::class => [
Image\ImageBuilder::class,

View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\IpGeolocation\GeoLite2;
use Fig\Http\Message\RequestMethodInterface as RequestMethod;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions;
use PharData;
use Shlinkio\Shlink\Common\Exception\RuntimeException;
use Symfony\Component\Filesystem\Exception as FilesystemException;
use Symfony\Component\Filesystem\Filesystem;
use Throwable;
use function sprintf;
class DbUpdater implements DbUpdaterInterface
{
private const DB_COMPRESSED_FILE = 'GeoLite2-City.tar.gz';
private const DB_DECOMPRESSED_FILE = 'GeoLite2-City.mmdb';
/**
* @var ClientInterface
*/
private $httpClient;
/**
* @var Filesystem
*/
private $filesystem;
/**
* @var GeoLite2Options
*/
private $options;
public function __construct(ClientInterface $httpClient, Filesystem $filesystem, GeoLite2Options $options)
{
$this->httpClient = $httpClient;
$this->filesystem = $filesystem;
$this->options = $options;
}
/**
* @throws RuntimeException
*/
public function downloadFreshCopy(): void
{
$tempDir = $this->options->getTempDir();
$compressedFile = sprintf('%s/%s', $tempDir, self::DB_COMPRESSED_FILE);
$this->downloadDbFile($compressedFile);
$tempFullPath = $this->extractDbFile($compressedFile, $tempDir);
$this->copyNewDbFile($tempFullPath);
$this->deleteTempFiles([$compressedFile, $tempFullPath]);
}
private function downloadDbFile(string $dest): void
{
try {
$this->httpClient->request(RequestMethod::METHOD_GET, $this->options->getDownloadFrom(), [
RequestOptions::SINK => $dest,
]);
} catch (Throwable | GuzzleException $e) {
throw new RuntimeException(
'An error occurred while trying to download a fresh copy of the GeoLite2 database',
0,
$e
);
}
}
private function extractDbFile(string $compressedFile, string $tempDir): string
{
try {
$phar = new PharData($compressedFile);
$internalPathToDb = sprintf('%s/%s', $phar->getBasename(), self::DB_DECOMPRESSED_FILE);
$phar->extractTo($tempDir, $internalPathToDb, true);
return sprintf('%s/%s', $tempDir, $internalPathToDb);
} catch (Throwable $e) {
throw new RuntimeException(
sprintf('An error occurred while trying to extract the GeoLite2 database from %s', $compressedFile),
0,
$e
);
}
}
private function copyNewDbFile(string $from): void
{
try {
$this->filesystem->copy($from, $this->options->getDbLocation(), true);
} catch (FilesystemException\FileNotFoundException | FilesystemException\IOException $e) {
throw new RuntimeException('An error occurred while trying to copy GeoLite2 db file to destination', 0, $e);
}
}
private function deleteTempFiles(array $files): void
{
try {
$this->filesystem->remove($files);
} catch (FilesystemException\IOException $e) {
// Ignore any error produced when trying to delete temp files
}
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\IpGeolocation\GeoLite2;
use Shlinkio\Shlink\Common\Exception\RuntimeException;
interface DbUpdaterInterface
{
/**
* @throws RuntimeException
*/
public function downloadFreshCopy(): void;
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\IpGeolocation\GeoLite2;
use Zend\Stdlib\AbstractOptions;
class GeoLite2Options extends AbstractOptions
{
private $dbLocation = '';
private $tempDir = '';
private $downloadFrom = 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz';
public function getDbLocation(): string
{
return $this->dbLocation;
}
protected function setDbLocation(string $dbLocation): self
{
$this->dbLocation = $dbLocation;
return $this;
}
public function getTempDir(): string
{
return $this->tempDir;
}
protected function setTempDir(string $tempDir): self
{
$this->tempDir = $tempDir;
return $this;
}
public function getDownloadFrom(): string
{
return $this->downloadFrom;
}
protected function setDownloadFrom(string $downloadFrom): self
{
$this->downloadFrom = $downloadFrom;
return $this;
}
}

View File

@ -0,0 +1 @@
geolite2-testing-db

Binary file not shown.

View File

@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\IpGeolocation\GeoLite2;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ClientException;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Exception\RuntimeException;
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdater;
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\GeoLite2Options;
use Symfony\Component\Filesystem\Exception as FilesystemException;
use Symfony\Component\Filesystem\Filesystem;
use Zend\Diactoros\Response;
class DbUpdaterTest extends TestCase
{
/**
* @var DbUpdater
*/
private $dbUpdater;
/**
* @var ObjectProphecy
*/
private $httpClient;
/**
* @var ObjectProphecy
*/
private $filesystem;
/**
* @var GeoLite2Options
*/
private $options;
public function setUp()
{
$this->httpClient = $this->prophesize(ClientInterface::class);
$this->filesystem = $this->prophesize(Filesystem::class);
$this->options = new GeoLite2Options([
'temp_dir' => __DIR__ . '/../../../test-resources',
'db_location' => '',
'download_from' => '',
]);
$this->dbUpdater = new DbUpdater($this->httpClient->reveal(), $this->filesystem->reveal(), $this->options);
}
/**
* @test
*/
public function anExceptionIsThrownIfFreshDbCannotBeDownloaded()
{
$request = $this->httpClient->request(Argument::cetera())->willThrow(ClientException::class);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage(
'An error occurred while trying to download a fresh copy of the GeoLite2 database'
);
$request->shouldBeCalledOnce();
$this->dbUpdater->downloadFreshCopy();
}
/**
* @test
*/
public function anExceptionIsThrownIfFreshDbCannotBeExtracted()
{
$this->options->tempDir = '__invalid__';
$request = $this->httpClient->request(Argument::cetera())->willReturn(new Response());
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage(
'An error occurred while trying to extract the GeoLite2 database from __invalid__/GeoLite2-City.tar.gz'
);
$request->shouldBeCalledOnce();
$this->dbUpdater->downloadFreshCopy();
}
/**
* @test
* @dataProvider provideFilesystemExceptions
*/
public function anExceptionIsThrownIfFreshDbCannotBeCopiedToDestination(string $e)
{
$request = $this->httpClient->request(Argument::cetera())->willReturn(new Response());
$copy = $this->filesystem->copy(Argument::cetera())->willThrow($e);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('An error occurred while trying to copy GeoLite2 db file to destination');
$request->shouldBeCalledOnce();
$copy->shouldBeCalledOnce();
$this->dbUpdater->downloadFreshCopy();
}
public function provideFilesystemExceptions(): array
{
return [
[FilesystemException\FileNotFoundException::class],
[FilesystemException\IOException::class],
];
}
/**
* @test
*/
public function noExceptionsAreThrownIfEverythingWorksFine()
{
$request = $this->httpClient->request(Argument::cetera())->willReturn(new Response());
$copy = $this->filesystem->copy(Argument::cetera())->will(function () {
});
$remove = $this->filesystem->remove(Argument::cetera())->will(function () {
});
$this->dbUpdater->downloadFreshCopy();
$request->shouldHaveBeenCalledOnce();
$copy->shouldHaveBeenCalledOnce();
$remove->shouldHaveBeenCalledOnce();
}
}