diff --git a/data/.gitignore b/data/.gitignore index 81ab0a92..302e8399 100644 --- a/data/.gitignore +++ b/data/.gitignore @@ -1 +1,2 @@ local.ini +thumbnails diff --git a/src/Controllers/UserAvatarController.php b/src/Controllers/UserAvatarController.php index c74cc02b..d2d31435 100644 --- a/src/Controllers/UserAvatarController.php +++ b/src/Controllers/UserAvatarController.php @@ -61,7 +61,7 @@ final class UserAvatarController extends AbstractController if (!$this->fileService->exists($file)) $file = $this->userService->getBlankAvatarSourcePath(); - $sizedFile = $this->thumbnailService->generateFromFile($file, $size, $size); + $sizedFile = $this->thumbnailService->getOrGenerate($file, $size, $size); $this->fileService->serve($sizedFile); } } diff --git a/src/Services/FileService.php b/src/Services/FileService.php index 630852a9..88bd58bc 100644 --- a/src/Services/FileService.php +++ b/src/Services/FileService.php @@ -59,6 +59,13 @@ class FileService exit; } + public function createFolders($target) + { + $finalTarget = $this->getFullPath($target); + if (!file_exists($finalTarget)) + mkdir($finalTarget, 0777, true); + } + public function exists($source) { $finalSource = $this->getFullPath($source); diff --git a/src/Services/ThumbnailGenerators/IThumbnailGenerator.php b/src/Services/ThumbnailGenerators/IThumbnailGenerator.php index 3a317c09..8581457b 100644 --- a/src/Services/ThumbnailGenerators/IThumbnailGenerator.php +++ b/src/Services/ThumbnailGenerators/IThumbnailGenerator.php @@ -3,5 +3,5 @@ namespace Szurubooru\Services\ThumbnailGenerators; interface IThumbnailGenerator { - public function generateFromFile($srcPath, $dstPath, $width, $height); + public function generate($srcPath, $dstPath, $width, $height); } diff --git a/src/Services/ThumbnailGenerators/ImageGdThumbnailGenerator.php b/src/Services/ThumbnailGenerators/ImageGdThumbnailGenerator.php index c63586ad..c8f5ff80 100644 --- a/src/Services/ThumbnailGenerators/ImageGdThumbnailGenerator.php +++ b/src/Services/ThumbnailGenerators/ImageGdThumbnailGenerator.php @@ -10,7 +10,7 @@ class ImageGdThumbnailGenerator implements IThumbnailGenerator $this->config = $config; } - public function generateFromFile($srcPath, $dstPath, $width, $height) + public function generate($srcPath, $dstPath, $width, $height) { if (!file_exists($srcPath)) throw new \InvalidArgumentException($srcPath . ' does not exist'); diff --git a/src/Services/ThumbnailGenerators/ImageImagickThumbnailGenerator.php b/src/Services/ThumbnailGenerators/ImageImagickThumbnailGenerator.php index 7a37f819..6df5dccf 100644 --- a/src/Services/ThumbnailGenerators/ImageImagickThumbnailGenerator.php +++ b/src/Services/ThumbnailGenerators/ImageImagickThumbnailGenerator.php @@ -10,7 +10,7 @@ class ImageImagickThumbnailGenerator implements IThumbnailGenerator $this->config = $config; } - public function generateFromFile($srcPath, $dstPath, $width, $height) + public function generate($srcPath, $dstPath, $width, $height) { if (!file_exists($srcPath)) throw new \InvalidArgumentException($srcPath . ' does not exist'); diff --git a/src/Services/ThumbnailGenerators/ImageThumbnailGenerator.php b/src/Services/ThumbnailGenerators/ImageThumbnailGenerator.php index c8eff9b2..b6a3a7e0 100644 --- a/src/Services/ThumbnailGenerators/ImageThumbnailGenerator.php +++ b/src/Services/ThumbnailGenerators/ImageThumbnailGenerator.php @@ -14,7 +14,7 @@ class ImageThumbnailGenerator implements IThumbnailGenerator $this->imageGdThumbnailGenerator = $imageGdThumbnailGenerator; } - public function generateFromFile($srcPath, $dstPath, $width, $height) + public function generate($srcPath, $dstPath, $width, $height) { if (extension_loaded('imagick')) $strategy = $this->imageImagickThumbnailGenerator; @@ -23,6 +23,6 @@ class ImageThumbnailGenerator implements IThumbnailGenerator else throw new \Exception('Both imagick and gd extensions are disabled'); - return $strategy->generateFromFile($srcPath, $dstPath, $width, $height); + return $strategy->generate($srcPath, $dstPath, $width, $height); } } diff --git a/src/Services/ThumbnailService.php b/src/Services/ThumbnailService.php index 52dda27b..2dd340dc 100644 --- a/src/Services/ThumbnailService.php +++ b/src/Services/ThumbnailService.php @@ -14,17 +14,58 @@ class ThumbnailService $this->thumbnailGenerator = $thumbnailGenerator; } - public function generateFromFile($source, $width, $height) + public function getOrGenerate($source, $width, $height) { - $target = $source . '-thumb' . $width . 'x' . $height . '.jpg'; + $target = $this->getPath($source, $width, $height); if (!$this->fileService->exists($target)) - { - $fullSource = $this->fileService->getFullPath($source); - $fullTarget = $this->fileService->getFullPath($target); - $this->thumbnailGenerator->generateFromFile($fullSource, $fullTarget, $width, $height); - } + $this->generate($source, $width, $height); return $target; } + + public function deleteUsedThumbnails($source) + { + foreach ($this->getUsedThumbnailSizes() as $size) + { + list ($width, $height) = $size; + $target = $this->getPath($source, $width, $height); + if ($this->fileService->exists($target)) + $this->fileService->delete($target); + } + } + + public function generate($source, $width, $height) + { + $target = $this->getPath($source, $width, $height); + + $fullSource = $this->fileService->getFullPath($source); + $fullTarget = $this->fileService->getFullPath($target); + $this->fileService->createFolders(dirname($target)); + $this->thumbnailGenerator->generate($fullSource, $fullTarget, $width, $height); + + return $target; + } + + public function getUsedThumbnailSizes() + { + foreach (glob($this->fileService->getFullPath('thumbnails') . DIRECTORY_SEPARATOR . '*x*') as $fn) + { + if (!is_dir($fn)) + continue; + + preg_match('/(?P\d+)x(?P\d+)/', $fn, $matches); + if ($matches) + { + $width = intval($matches['width']); + $height = intval($matches['height']); + yield [$width, $height]; + } + } + } + + private function getPath($source, $width, $height) + { + return 'thumbnails' . DIRECTORY_SEPARATOR . $width . 'x' . $height . DIRECTORY_SEPARATOR . $source; + } } diff --git a/src/Services/UserService.php b/src/Services/UserService.php index c0e6a8ee..246346d0 100644 --- a/src/Services/UserService.php +++ b/src/Services/UserService.php @@ -10,6 +10,7 @@ class UserService private $passwordService; private $emailService; private $fileService; + private $thumbnailService; private $timeService; private $tokenService; @@ -21,6 +22,7 @@ class UserService \Szurubooru\Services\PasswordService $passwordService, \Szurubooru\Services\EmailService $emailService, \Szurubooru\Services\FileService $fileService, + \Szurubooru\Services\ThumbnailService $thumbnailService, \Szurubooru\Services\TimeService $timeService, \Szurubooru\Services\TokenService $tokenService) { @@ -31,6 +33,7 @@ class UserService $this->passwordService = $passwordService; $this->emailService = $emailService; $this->fileService = $fileService; + $this->thumbnailService = $thumbnailService; $this->timeService = $timeService; $this->tokenService = $tokenService; } @@ -106,7 +109,11 @@ class UserService { $user->avatarStyle = \Szurubooru\Helpers\EnumHelper::avatarStyleFromString($formData->avatarStyle); if ($formData->avatarContent) - $this->fileService->saveFromBase64($formData->avatarContent, $this->getCustomAvatarSourcePath($user)); + { + $target = $this->getCustomAvatarSourcePath($user); + $this->fileService->saveFromBase64($formData->avatarContent, $target); + $this->thumbnailService->deleteUsedThumbnails($target); + } } if ($formData->userName !== null and $formData->userName !== $user->name) @@ -155,7 +162,10 @@ class UserService public function deleteUser(\Szurubooru\Entities\User $user) { $this->userDao->deleteById($user->id); - $this->fileService->delete($this->getCustomAvatarSourcePath($user)); + + $avatarSource = $this->getCustomAvatarSourcePath($user); + $this->fileService->delete($avatarSource); + $this->thumbnailService->deleteUsedThumbnails($avatarSource); } public function getCustomAvatarSourcePath(\Szurubooru\Entities\User $user) diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php index 0589a432..c9f9cadd 100644 --- a/tests/AbstractTestCase.php +++ b/tests/AbstractTestCase.php @@ -13,9 +13,12 @@ abstract class AbstractTestCase extends \PHPUnit_Framework_TestCase return new ConfigMock(); } - public function getTestDirectory() + public function createTestDirectory() { - return __DIR__ . DIRECTORY_SEPARATOR . 'files'; + $path = $this->getTestDirectoryPath(); + if (!file_exists($path)) + mkdir($path, 0777, true); + return $path; } protected function tearDown() @@ -23,11 +26,31 @@ abstract class AbstractTestCase extends \PHPUnit_Framework_TestCase $this->cleanTestDirectory(); } + private function getTestDirectoryPath() + { + return __DIR__ . DIRECTORY_SEPARATOR . 'files'; + } + private function cleanTestDirectory() { - foreach (scandir($this->getTestDirectory()) as $fn) - if ($fn{0} != '.') - unlink($this->getTestDirectory() . DIRECTORY_SEPARATOR . $fn); + if (!file_exists($this->getTestDirectoryPath())) + return; + + $dirIterator = new \RecursiveDirectoryIterator( + $this->getTestDirectoryPath(), + \RecursiveDirectoryIterator::SKIP_DOTS); + + $files = new \RecursiveIteratorIterator( + $dirIterator, + \RecursiveIteratorIterator::CHILD_FIRST); + + foreach ($files as $fileInfo) + { + if ($fileInfo->isDir()) + rmdir($fileInfo->getRealPath()); + else + unlink($fileInfo->getRealPath()); + } } } diff --git a/tests/Services/FileServiceTest.php b/tests/Services/FileServiceTest.php index e4dd7173..94b5edb8 100644 --- a/tests/Services/FileServiceTest.php +++ b/tests/Services/FileServiceTest.php @@ -5,12 +5,13 @@ class FileServiceTest extends \Szurubooru\Tests\AbstractTestCase { public function testSaving() { + $testDirectory = $this->createTestDirectory(); $httpHelper = $this->mock( \Szurubooru\Helpers\HttpHelper::class); - $fileService = new \Szurubooru\Services\FileService($this->getTestDirectory(), $httpHelper); + $fileService = new \Szurubooru\Services\FileService($testDirectory, $httpHelper); $input = 'data:text/plain,YXdlc29tZSBkb2c='; $fileService->saveFromBase64($input, 'dog.txt'); $expected = 'awesome dog'; - $actual = file_get_contents($this->getTestDirectory() . DIRECTORY_SEPARATOR . 'dog.txt'); + $actual = file_get_contents($testDirectory . DIRECTORY_SEPARATOR . 'dog.txt'); $this->assertEquals($expected, $actual); } } diff --git a/tests/Services/ThumbnailServiceTest.php b/tests/Services/ThumbnailServiceTest.php new file mode 100644 index 00000000..a0d0c93a --- /dev/null +++ b/tests/Services/ThumbnailServiceTest.php @@ -0,0 +1,51 @@ +createTestDirectory(); + mkdir($tempDirectory . DS . 'thumbnails'); + mkdir($tempDirectory . DS . 'thumbnails' . DS . '5x5'); + mkdir($tempDirectory . DS . 'thumbnails' . DS . '10x10'); + touch($tempDirectory . DS . 'thumbnails' . DS . '5x5' . DS . 'remove'); + touch($tempDirectory . DS . 'thumbnails' . DS . '5x5' . DS . 'keep'); + touch($tempDirectory . DS . 'thumbnails' . DS . '10x10' . DS . 'remove'); + + $httpHelperMock = $this->mock(\Szurubooru\Helpers\HttpHelper::class); + $fileService = new \Szurubooru\Services\FileService($tempDirectory, $httpHelperMock); + $thumbnailGeneratorMock = $this->mock(\Szurubooru\Services\ThumbnailGenerators\SmartThumbnailGenerator::class); + + $thumbnailService = new \Szurubooru\Services\ThumbnailService($fileService, $thumbnailGeneratorMock); + $thumbnailService->deleteUsedThumbnails('remove'); + + $this->assertFalse(file_exists($tempDirectory . DS . 'thumbnails' . DS . '5x5' . DS . 'remove')); + $this->assertTrue(file_exists($tempDirectory . DS . 'thumbnails' . DS . '5x5' . DS . 'keep')); + $this->assertFalse(file_exists($tempDirectory . DS . 'thumbnails' . DS . '10x10' . DS . 'remove')); + } + + public function testGetUsedThumbnailSizes() + { + $tempDirectory = $this->createTestDirectory(); + mkdir($tempDirectory . DIRECTORY_SEPARATOR . '5x5'); + mkdir($tempDirectory . DIRECTORY_SEPARATOR . '10x10'); + mkdir($tempDirectory . DIRECTORY_SEPARATOR . 'something unexpected'); + touch($tempDirectory . DIRECTORY_SEPARATOR . '15x15'); + + $fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class); + $fileServiceMock->expects($this->once())->method('getFullPath')->willReturn($tempDirectory); + $thumbnailGeneratorMock = $this->mock(\Szurubooru\Services\ThumbnailGenerators\SmartThumbnailGenerator::class); + + $thumbnailService = new \Szurubooru\Services\ThumbnailService($fileServiceMock, $thumbnailGeneratorMock); + + $expected = [[5, 5], [10, 10]]; + $actual = iterator_to_array($thumbnailService->getUsedThumbnailSizes()); + + $this->assertEquals(count($expected), count($actual)); + foreach ($expected as $v) + $this->assertContains($v, $actual); + } +} diff --git a/tests/Services/UserServiceTest.php b/tests/Services/UserServiceTest.php index cb9527d0..c617288d 100644 --- a/tests/Services/UserServiceTest.php +++ b/tests/Services/UserServiceTest.php @@ -10,6 +10,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase private $passwordServiceMock; private $emailServiceMock; private $fileServiceMock; + private $thumbnailServiceMock; private $timeServiceMock; private $tokenServiceMock; @@ -22,6 +23,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase $this->passwordServiceMock = $this->mock(\Szurubooru\Services\PasswordService::class); $this->emailServiceMock = $this->mock(\Szurubooru\Services\EmailService::class); $this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class); + $this->thumbnailServiceMock = $this->mock(\Szurubooru\Services\ThumbnailService::class); $this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class); $this->tokenServiceMock = $this->mock(\Szurubooru\Services\TokenService::class); } @@ -179,6 +181,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase $this->passwordServiceMock, $this->emailServiceMock, $this->fileServiceMock, + $this->thumbnailServiceMock, $this->timeServiceMock, $this->tokenServiceMock); }