From 3d7cf6992e3f07b1845dd8a814f72d11d4a10dda Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 11 Nov 2018 21:08:23 +0100 Subject: [PATCH] Created service to update geolite2 database file --- config/autoload/geolite2.global.php | 2 + module/Common/config/dependencies.config.php | 8 ++ .../src/IpGeolocation/GeoLite2/DbUpdater.php | 105 +++++++++++++++ .../GeoLite2/DbUpdaterInterface.php | 14 ++ .../GeoLite2/GeoLite2Options.php | 46 +++++++ module/Common/test-resources/.gitignore | 1 + .../test-resources/GeoLite2-City.tar.gz | Bin 0 -> 202 bytes .../IpGeolocation/GeoLite2/DbUpdaterTest.php | 126 ++++++++++++++++++ 8 files changed, 302 insertions(+) create mode 100644 module/Common/src/IpGeolocation/GeoLite2/DbUpdater.php create mode 100644 module/Common/src/IpGeolocation/GeoLite2/DbUpdaterInterface.php create mode 100644 module/Common/src/IpGeolocation/GeoLite2/GeoLite2Options.php create mode 100644 module/Common/test-resources/.gitignore create mode 100644 module/Common/test-resources/GeoLite2-City.tar.gz create mode 100644 module/Common/test/IpGeolocation/GeoLite2/DbUpdaterTest.php diff --git a/config/autoload/geolite2.global.php b/config/autoload/geolite2.global.php index 0536a0be..da52a62c 100644 --- a/config/autoload/geolite2.global.php +++ b/config/autoload/geolite2.global.php @@ -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', ], ]; diff --git a/module/Common/config/dependencies.config.php b/module/Common/config/dependencies.config.php index 4ba6249d..8ac6311b 100644 --- a/module/Common/config/dependencies.config.php +++ b/module/Common/config/dependencies.config.php @@ -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, diff --git a/module/Common/src/IpGeolocation/GeoLite2/DbUpdater.php b/module/Common/src/IpGeolocation/GeoLite2/DbUpdater.php new file mode 100644 index 00000000..e8628cf2 --- /dev/null +++ b/module/Common/src/IpGeolocation/GeoLite2/DbUpdater.php @@ -0,0 +1,105 @@ +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 + } + } +} diff --git a/module/Common/src/IpGeolocation/GeoLite2/DbUpdaterInterface.php b/module/Common/src/IpGeolocation/GeoLite2/DbUpdaterInterface.php new file mode 100644 index 00000000..1fad0825 --- /dev/null +++ b/module/Common/src/IpGeolocation/GeoLite2/DbUpdaterInterface.php @@ -0,0 +1,14 @@ +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; + } +} diff --git a/module/Common/test-resources/.gitignore b/module/Common/test-resources/.gitignore new file mode 100644 index 00000000..3ffd27ba --- /dev/null +++ b/module/Common/test-resources/.gitignore @@ -0,0 +1 @@ +geolite2-testing-db diff --git a/module/Common/test-resources/GeoLite2-City.tar.gz b/module/Common/test-resources/GeoLite2-City.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..c945d2954ff2b596fd58f14cfe460588e2d100f8 GIT binary patch literal 202 zcmb2|=3vn2dJ)aQ{Pxm$u0sk84Ta}zTlE80&i=_DyQ}0ir`YDywY<}0tA20OcyYl# zL*U}~-5gpgYQ*;GIe$r$_-og;O5)k+jW1?qS*Gs}v|gQeTzlchttpClient = $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(); + } +}