From 62ed1062605c84d237b2e0bbcc005e389065f481 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 9 Oct 2025 15:12:54 +0200 Subject: [PATCH 1/2] feat(preview): On demand preview migration When requesting previews, which we don't find in oc_previews, search in IAppData first before creating them. Move the logic from MovepreviewJob to PreviewMigrationService and reuse that in the Preview Generator. At the same time rename MovePreviewJob to PreviewMigrationJob as it is a better name. Signed-off-by: Carl Schwan (cherry picked from commit 614916812914c36c63e2f96aebe6f45690983fb4) --- core/BackgroundJobs/PreviewMigrationJob.php | 107 +++++++++++++++++ lib/composer/composer/LICENSE | 2 - lib/composer/composer/autoload_classmap.php | 3 +- lib/composer/composer/autoload_static.php | 3 +- lib/private/Preview/Generator.php | 36 ++++-- .../Preview/PreviewMigrationService.php | 112 ++++-------------- lib/private/PreviewManager.php | 4 + lib/private/Repair/AddMovePreviewJob.php | 4 +- lib/private/Setup.php | 4 +- tests/lib/Preview/GeneratorTest.php | 66 +++++++++++ ...obTest.php => PreviewMigrationJobTest.php} | 62 ++++++---- 11 files changed, 278 insertions(+), 125 deletions(-) create mode 100644 core/BackgroundJobs/PreviewMigrationJob.php rename core/BackgroundJobs/MovePreviewJob.php => lib/private/Preview/PreviewMigrationService.php (67%) rename tests/lib/Preview/{MovePreviewJobTest.php => PreviewMigrationJobTest.php} (86%) diff --git a/core/BackgroundJobs/PreviewMigrationJob.php b/core/BackgroundJobs/PreviewMigrationJob.php new file mode 100644 index 00000000000..dc79db3b309 --- /dev/null +++ b/core/BackgroundJobs/PreviewMigrationJob.php @@ -0,0 +1,107 @@ +setTimeSensitivity(self::TIME_INSENSITIVE); + $this->setInterval(24 * 60 * 60); + $this->previewRootPath = 'appdata_' . $this->config->getSystemValueString('instanceid') . '/preview/'; + } + + #[Override] + protected function run(mixed $argument): void { + if ($this->appConfig->getValueBool('core', 'previewMovedDone')) { + return; + } + + $startTime = time(); + while (true) { + $qb = $this->connection->getQueryBuilder(); + $qb->select('path') + ->from('filecache') + // Hierarchical preview folder structure + ->where($qb->expr()->like('path', $qb->createNamedParameter($this->previewRootPath . '%/%/%/%/%/%/%/%/%'))) + // Legacy flat preview folder structure + ->orWhere($qb->expr()->like('path', $qb->createNamedParameter($this->previewRootPath . '%/%.%'))) + ->hintShardKey('storage', $this->rootFolder->getMountPoint()->getNumericStorageId()) + ->setMaxResults(100); + + $result = $qb->executeQuery(); + $foundPreviews = $this->processQueryResult($result); + + if (!$foundPreviews) { + break; + } + + // Stop if execution time is more than one hour. + if (time() - $startTime > 3600) { + return; + } + } + + $this->appConfig->setValueBool('core', 'previewMovedDone', true); + } + + private function processQueryResult(IResult $result): bool { + $foundPreview = false; + $fileIds = []; + $flatFileIds = []; + while ($row = $result->fetch()) { + $pathSplit = explode('/', $row['path']); + assert(count($pathSplit) >= 2); + $fileId = (int)$pathSplit[count($pathSplit) - 2]; + if (count($pathSplit) === 11) { + // Hierarchical structure + if (!in_array($fileId, $fileIds)) { + $fileIds[] = $fileId; + } + } else { + // Flat structure + if (!in_array($fileId, $flatFileIds)) { + $flatFileIds[] = $fileId; + } + } + $foundPreview = true; + } + + foreach ($fileIds as $fileId) { + $this->migrationService->migrateFileId($fileId, flatPath: false); + } + + foreach ($flatFileIds as $fileId) { + $this->migrationService->migrateFileId($fileId, flatPath: true); + } + return $foundPreview; + } +} diff --git a/lib/composer/composer/LICENSE b/lib/composer/composer/LICENSE index f27399a042d..62ecfd8d004 100644 --- a/lib/composer/composer/LICENSE +++ b/lib/composer/composer/LICENSE @@ -1,4 +1,3 @@ - Copyright (c) Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy @@ -18,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 247a140223d..d72e2726dc3 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1300,7 +1300,7 @@ return array( 'OC\\Core\\BackgroundJobs\\CleanupLoginFlowV2' => $baseDir . '/core/BackgroundJobs/CleanupLoginFlowV2.php', 'OC\\Core\\BackgroundJobs\\GenerateMetadataJob' => $baseDir . '/core/BackgroundJobs/GenerateMetadataJob.php', 'OC\\Core\\BackgroundJobs\\LookupServerSendCheckBackgroundJob' => $baseDir . '/core/BackgroundJobs/LookupServerSendCheckBackgroundJob.php', - 'OC\\Core\\BackgroundJobs\\MovePreviewJob' => $baseDir . '/core/BackgroundJobs/MovePreviewJob.php', + 'OC\\Core\\BackgroundJobs\\PreviewMigrationJob' => $baseDir . '/core/BackgroundJobs/PreviewMigrationJob.php', 'OC\\Core\\Command\\App\\Disable' => $baseDir . '/core/Command/App/Disable.php', 'OC\\Core\\Command\\App\\Enable' => $baseDir . '/core/Command/App/Enable.php', 'OC\\Core\\Command\\App\\GetPath' => $baseDir . '/core/Command/App/GetPath.php', @@ -1978,6 +1978,7 @@ return array( 'OC\\Preview\\PNG' => $baseDir . '/lib/private/Preview/PNG.php', 'OC\\Preview\\Photoshop' => $baseDir . '/lib/private/Preview/Photoshop.php', 'OC\\Preview\\Postscript' => $baseDir . '/lib/private/Preview/Postscript.php', + 'OC\\Preview\\PreviewMigrationService' => $baseDir . '/lib/private/Preview/PreviewMigrationService.php', 'OC\\Preview\\PreviewService' => $baseDir . '/lib/private/Preview/PreviewService.php', 'OC\\Preview\\ProviderV2' => $baseDir . '/lib/private/Preview/ProviderV2.php', 'OC\\Preview\\SGI' => $baseDir . '/lib/private/Preview/SGI.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 9f3d6c2c3dd..edea5a31ee5 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1341,7 +1341,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\BackgroundJobs\\CleanupLoginFlowV2' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CleanupLoginFlowV2.php', 'OC\\Core\\BackgroundJobs\\GenerateMetadataJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/GenerateMetadataJob.php', 'OC\\Core\\BackgroundJobs\\LookupServerSendCheckBackgroundJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/LookupServerSendCheckBackgroundJob.php', - 'OC\\Core\\BackgroundJobs\\MovePreviewJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/MovePreviewJob.php', + 'OC\\Core\\BackgroundJobs\\PreviewMigrationJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/PreviewMigrationJob.php', 'OC\\Core\\Command\\App\\Disable' => __DIR__ . '/../../..' . '/core/Command/App/Disable.php', 'OC\\Core\\Command\\App\\Enable' => __DIR__ . '/../../..' . '/core/Command/App/Enable.php', 'OC\\Core\\Command\\App\\GetPath' => __DIR__ . '/../../..' . '/core/Command/App/GetPath.php', @@ -2019,6 +2019,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Preview\\PNG' => __DIR__ . '/../../..' . '/lib/private/Preview/PNG.php', 'OC\\Preview\\Photoshop' => __DIR__ . '/../../..' . '/lib/private/Preview/Photoshop.php', 'OC\\Preview\\Postscript' => __DIR__ . '/../../..' . '/lib/private/Preview/Postscript.php', + 'OC\\Preview\\PreviewMigrationService' => __DIR__ . '/../../..' . '/lib/private/Preview/PreviewMigrationService.php', 'OC\\Preview\\PreviewService' => __DIR__ . '/../../..' . '/lib/private/Preview/PreviewService.php', 'OC\\Preview\\ProviderV2' => __DIR__ . '/../../..' . '/lib/private/Preview/ProviderV2.php', 'OC\\Preview\\SGI' => __DIR__ . '/../../..' . '/lib/private/Preview/SGI.php', diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php index a64bd115181..1ceff60d54b 100644 --- a/lib/private/Preview/Generator.php +++ b/lib/private/Preview/Generator.php @@ -17,6 +17,7 @@ use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\InMemoryFile; use OCP\Files\SimpleFS\ISimpleFile; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IImage; use OCP\IPreview; @@ -30,13 +31,15 @@ class Generator { public const SEMAPHORE_ID_NEW = 0x07ea; public function __construct( - private IConfig $config, - private IPreview $previewManager, - private GeneratorHelper $helper, - private IEventDispatcher $eventDispatcher, - private LoggerInterface $logger, - private PreviewMapper $previewMapper, - private StorageFactory $storageFactory, + private readonly IConfig $config, + private readonly IAppConfig $appConfig, + private readonly IPreview $previewManager, + private readonly GeneratorHelper $helper, + private readonly IEventDispatcher $eventDispatcher, + private readonly LoggerInterface $logger, + private readonly PreviewMapper $previewMapper, + private readonly StorageFactory $storageFactory, + private readonly PreviewMigrationService $migrationService, ) { } @@ -108,6 +111,10 @@ class Generator { [$file->getId() => $previews] = $this->previewMapper->getAvailablePreviews([$file->getId()]); + if (empty($previews)) { + $previews = $this->migrateOldPreviews($file->getId()); + } + $previewVersion = null; if ($file instanceof IVersionedPreviewFile) { $previewVersion = $file->getPreviewVersion(); @@ -193,6 +200,21 @@ class Generator { return $previewFile; } + /** + * @return array + */ + private function migrateOldPreviews(int $fileId): array { + if ($this->appConfig->getValueBool('core', 'previewMovedDone')) { + return []; + } + + $previews = $this->migrationService->migrateFileId($fileId, flatPath: false); + if (empty($previews)) { + $previews = $this->migrationService->migrateFileId($fileId, flatPath: true); + } + return $previews; + } + /** * Acquire a semaphore of the specified id and concurrency, blocking if necessary. * Return an identifier of the semaphore on success, which can be used to release it via diff --git a/core/BackgroundJobs/MovePreviewJob.php b/lib/private/Preview/PreviewMigrationService.php similarity index 67% rename from core/BackgroundJobs/MovePreviewJob.php rename to lib/private/Preview/PreviewMigrationService.php index 7ce1b3bb8a6..cacdb89dc31 100644 --- a/core/BackgroundJobs/MovePreviewJob.php +++ b/lib/private/Preview/PreviewMigrationService.php @@ -2,130 +2,60 @@ declare(strict_types=1); -/* +/** * SPDX-FileCopyrightText: 2025 Nextcloud GmbH * SPDX-FileContributor: Carl Schwan * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OC\Core\BackgroundJobs; +namespace OC\Preview; use OC\Files\SimpleFS\SimpleFile; use OC\Preview\Db\Preview; use OC\Preview\Db\PreviewMapper; use OC\Preview\Storage\StorageFactory; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\TimedJob; use OCP\DB\Exception; -use OCP\DB\IResult; use OCP\Files\AppData\IAppDataFactory; use OCP\Files\IAppData; use OCP\Files\IMimeTypeDetector; use OCP\Files\IMimeTypeLoader; use OCP\Files\IRootFolder; -use OCP\IAppConfig; +use OCP\Files\NotFoundException; use OCP\IConfig; use OCP\IDBConnection; -use Override; use Psr\Log\LoggerInterface; -class MovePreviewJob extends TimedJob { +class PreviewMigrationService { private IAppData $appData; private string $previewRootPath; public function __construct( - ITimeFactory $time, - private readonly IAppConfig $appConfig, private readonly IConfig $config, - private readonly PreviewMapper $previewMapper, - private readonly StorageFactory $storageFactory, - private readonly IDBConnection $connection, private readonly IRootFolder $rootFolder, + private readonly LoggerInterface $logger, private readonly IMimeTypeDetector $mimeTypeDetector, private readonly IMimeTypeLoader $mimeTypeLoader, - private readonly LoggerInterface $logger, + private readonly IDBConnection $connection, + private readonly PreviewMapper $previewMapper, + private readonly StorageFactory $storageFactory, IAppDataFactory $appDataFactory, ) { - parent::__construct($time); - $this->appData = $appDataFactory->get('preview'); - $this->setTimeSensitivity(self::TIME_INSENSITIVE); - $this->setInterval(24 * 60 * 60); $this->previewRootPath = 'appdata_' . $this->config->getSystemValueString('instanceid') . '/preview/'; } - #[Override] - protected function run(mixed $argument): void { - if ($this->appConfig->getValueBool('core', 'previewMovedDone')) { - return; - } - - $startTime = time(); - while (true) { - $qb = $this->connection->getQueryBuilder(); - $qb->select('path') - ->from('filecache') - // Hierarchical preview folder structure - ->where($qb->expr()->like('path', $qb->createNamedParameter($this->previewRootPath . '%/%/%/%/%/%/%/%/%'))) - // Legacy flat preview folder structure - ->orWhere($qb->expr()->like('path', $qb->createNamedParameter($this->previewRootPath . '%/%.%'))) - ->hintShardKey('storage', $this->rootFolder->getMountPoint()->getNumericStorageId()) - ->setMaxResults(100); - - $result = $qb->executeQuery(); - $foundPreviews = $this->processQueryResult($result); - - if (!$foundPreviews) { - break; - } - - // Stop if execution time is more than one hour. - if (time() - $startTime > 3600) { - return; - } - } - - $this->appConfig->setValueBool('core', 'previewMovedDone', true); - } - - private function processQueryResult(IResult $result): bool { - $foundPreview = false; - $fileIds = []; - $flatFileIds = []; - while ($row = $result->fetchAssociative()) { - $pathSplit = explode('/', $row['path']); - assert(count($pathSplit) >= 2); - $fileId = (int)$pathSplit[count($pathSplit) - 2]; - if (count($pathSplit) === 11) { - // Hierarchical structure - if (!in_array($fileId, $fileIds)) { - $fileIds[] = $fileId; - } - } else { - // Flat structure - if (!in_array($fileId, $flatFileIds)) { - $flatFileIds[] = $fileId; - } - } - $foundPreview = true; - } - - foreach ($fileIds as $fileId) { - $this->processPreviews($fileId, flatPath: false); - } - - foreach ($flatFileIds as $fileId) { - $this->processPreviews($fileId, flatPath: true); - } - return $foundPreview; - } - /** * @param array $previewFolders + * @return Preview[] */ - private function processPreviews(int $fileId, bool $flatPath): void { + public function migrateFileId(int $fileId, bool $flatPath): array { + $previews = []; $internalPath = $this->getInternalFolder((string)$fileId, $flatPath); - $folder = $this->appData->getFolder($internalPath); + try { + $folder = $this->appData->getFolder($internalPath); + } catch (NotFoundException) { + return []; + } /** * @var list $previewFiles @@ -152,6 +82,10 @@ class MovePreviewJob extends TimedJob { ]; } + if (empty($previewFiles)) { + return $previews; + } + $qb = $this->connection->getQueryBuilder(); $qb->select('storage', 'etag', 'mimetype') ->from('filecache') @@ -194,6 +128,8 @@ class MovePreviewJob extends TimedJob { $this->previewMapper->delete($preview); throw $e; } + + $previews[] = $preview; } } else { // No matching fileId, delete preview @@ -211,9 +147,11 @@ class MovePreviewJob extends TimedJob { } $this->deleteFolder($internalPath); + + return $previews; } - public static function getInternalFolder(string $name, bool $flatPath): string { + private static function getInternalFolder(string $name, bool $flatPath): string { if ($flatPath) { return $name; } diff --git a/lib/private/PreviewManager.php b/lib/private/PreviewManager.php index 9684ad240e1..8387485e6b2 100644 --- a/lib/private/PreviewManager.php +++ b/lib/private/PreviewManager.php @@ -13,12 +13,14 @@ use OC\Preview\Db\PreviewMapper; use OC\Preview\Generator; use OC\Preview\GeneratorHelper; use OC\Preview\IMagickSupport; +use OC\Preview\PreviewMigrationService; use OC\Preview\Storage\StorageFactory; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\Files\SimpleFS\ISimpleFile; +use OCP\IAppConfig; use OCP\IBinaryFinder; use OCP\IConfig; use OCP\IPreview; @@ -135,12 +137,14 @@ class PreviewManager implements IPreview { if ($this->generator === null) { $this->generator = new Generator( $this->config, + $this->container->get(IAppConfig::class), $this, new GeneratorHelper(), $this->eventDispatcher, $this->container->get(LoggerInterface::class), $this->container->get(PreviewMapper::class), $this->container->get(StorageFactory::class), + $this->container->get(PreviewMigrationService::class), ); } return $this->generator; diff --git a/lib/private/Repair/AddMovePreviewJob.php b/lib/private/Repair/AddMovePreviewJob.php index 5edcd64d46e..44a266f3787 100644 --- a/lib/private/Repair/AddMovePreviewJob.php +++ b/lib/private/Repair/AddMovePreviewJob.php @@ -6,7 +6,7 @@ */ namespace OC\Repair; -use OC\Core\BackgroundJobs\MovePreviewJob; +use OC\Core\BackgroundJobs\PreviewMigrationJob; use OCP\BackgroundJob\IJobList; use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; @@ -22,6 +22,6 @@ class AddMovePreviewJob implements IRepairStep { } public function run(IOutput $output) { - $this->jobList->add(MovePreviewJob::class); + $this->jobList->add(PreviewMigrationJob::class); } } diff --git a/lib/private/Setup.php b/lib/private/Setup.php index 5f91dc10692..7de6572dbe2 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -15,7 +15,7 @@ use InvalidArgumentException; use OC\Authentication\Token\PublicKeyTokenProvider; use OC\Authentication\Token\TokenCleanupJob; use OC\Core\BackgroundJobs\GenerateMetadataJob; -use OC\Core\BackgroundJobs\MovePreviewJob; +use OC\Core\BackgroundJobs\PreviewMigrationJob; use OC\Log\Rotate; use OC\Preview\BackgroundCleanupJob; use OC\TextProcessing\RemoveOldTasksBackgroundJob; @@ -506,7 +506,7 @@ class Setup { $jobList->add(RemoveOldTasksBackgroundJob::class); $jobList->add(CleanupDeletedUsers::class); $jobList->add(GenerateMetadataJob::class); - $jobList->add(MovePreviewJob::class); + $jobList->add(PreviewMigrationJob::class); } /** diff --git a/tests/lib/Preview/GeneratorTest.php b/tests/lib/Preview/GeneratorTest.php index ceaf483a658..f1fab3bf0a4 100644 --- a/tests/lib/Preview/GeneratorTest.php +++ b/tests/lib/Preview/GeneratorTest.php @@ -7,15 +7,18 @@ namespace Test\Preview; +use OC\Core\AppInfo\ConfigLexicon; use OC\Preview\Db\Preview; use OC\Preview\Db\PreviewMapper; use OC\Preview\Generator; use OC\Preview\GeneratorHelper; +use OC\Preview\PreviewMigrationService; use OC\Preview\Storage\StorageFactory; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; use OCP\Files\Mount\IMountPoint; use OCP\Files\NotFoundException; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IImage; use OCP\IPreview; @@ -34,6 +37,7 @@ abstract class VersionedPreviewFile implements IVersionedPreviewFile, File { class GeneratorTest extends TestCase { private IConfig&MockObject $config; + private IAppConfig&MockObject $appConfig; private IPreview&MockObject $previewManager; private GeneratorHelper&MockObject $helper; private IEventDispatcher&MockObject $eventDispatcher; @@ -41,26 +45,31 @@ class GeneratorTest extends TestCase { private LoggerInterface&MockObject $logger; private StorageFactory&MockObject $storageFactory; private PreviewMapper&MockObject $previewMapper; + private PreviewMigrationService&MockObject $migrationService; protected function setUp(): void { parent::setUp(); $this->config = $this->createMock(IConfig::class); + $this->appConfig = $this->createMock(IAppConfig::class); $this->previewManager = $this->createMock(IPreview::class); $this->helper = $this->createMock(GeneratorHelper::class); $this->eventDispatcher = $this->createMock(IEventDispatcher::class); $this->logger = $this->createMock(LoggerInterface::class); $this->previewMapper = $this->createMock(PreviewMapper::class); $this->storageFactory = $this->createMock(StorageFactory::class); + $this->migrationService = $this->createMock(PreviewMigrationService::class); $this->generator = new Generator( $this->config, + $this->appConfig, $this->previewManager, $this->helper, $this->eventDispatcher, $this->logger, $this->previewMapper, $this->storageFactory, + $this->migrationService, ); } @@ -247,6 +256,63 @@ class GeneratorTest extends TestCase { $this->assertSame(1000, $result->getSize()); } + + public function testMigrateOldPreview(): void { + $file = $this->getFile(42, 'myMimeType', false); + + $maxPreview = new Preview(); + $maxPreview->setWidth(1000); + $maxPreview->setHeight(1000); + $maxPreview->setMax(true); + $maxPreview->setSize(1000); + $maxPreview->setCropped(false); + $maxPreview->setStorageId(1); + $maxPreview->setVersion(null); + $maxPreview->setMimeType('image/png'); + + $previewFile = new Preview(); + $previewFile->setWidth(256); + $previewFile->setHeight(256); + $previewFile->setMax(false); + $previewFile->setSize(1000); + $previewFile->setVersion(null); + $previewFile->setCropped(false); + $previewFile->setStorageId(1); + $previewFile->setMimeType('image/png'); + + $this->previewManager->method('isMimeSupported') + ->with($this->equalTo('myMimeType')) + ->willReturn(true); + + $this->previewMapper->method('getAvailablePreviews') + ->with($this->equalTo([42])) + ->willReturn([42 => []]); + + $this->config->method('getSystemValueString') + ->willReturnCallback(function ($key, $default) { + return $default; + }); + + $this->config->method('getSystemValueInt') + ->willReturnCallback(function ($key, $default) { + return $default; + }); + + $this->appConfig->method('getValueBool') + ->willReturnCallback(fn ($app, $key, $default) => match ($key) { + ConfigLexicon::ON_DEMAND_PREVIEW_MIGRATION => true, + 'previewMovedDone' => false, + }); + + $this->migrationService->expects($this->exactly(1)) + ->method('migrateFileId') + ->willReturn([$maxPreview, $previewFile]); + + $result = $this->generator->getPreview($file, 100, 100); + $this->assertSame('256-256.png', $result->getName()); + $this->assertSame(1000, $result->getSize()); + } + public function testInvalidMimeType(): void { $this->expectException(NotFoundException::class); diff --git a/tests/lib/Preview/MovePreviewJobTest.php b/tests/lib/Preview/PreviewMigrationJobTest.php similarity index 86% rename from tests/lib/Preview/MovePreviewJobTest.php rename to tests/lib/Preview/PreviewMigrationJobTest.php index 69f730474e7..d8c46588032 100644 --- a/tests/lib/Preview/MovePreviewJobTest.php +++ b/tests/lib/Preview/PreviewMigrationJobTest.php @@ -10,8 +10,9 @@ declare(strict_types=1); namespace lib\Preview; -use OC\Core\BackgroundJobs\MovePreviewJob; +use OC\Core\BackgroundJobs\PreviewMigrationJob; use OC\Preview\Db\PreviewMapper; +use OC\Preview\PreviewMigrationService; use OC\Preview\PreviewService; use OC\Preview\Storage\StorageFactory; use OCP\AppFramework\Utility\ITimeFactory; @@ -30,7 +31,7 @@ use Psr\Log\LoggerInterface; use Test\TestCase; #[\PHPUnit\Framework\Attributes\Group('DB')] -class MovePreviewJobTest extends TestCase { +class PreviewMigrationJobTest extends TestCase { private IAppData $previewAppData; private PreviewMapper $previewMapper; private IAppConfig&MockObject $appConfig; @@ -112,18 +113,23 @@ class MovePreviewJobTest extends TestCase { $this->assertEquals(2, count($folder->getDirectoryListing())); $this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5)))); - $job = new MovePreviewJob( + $job = new PreviewMigrationJob( Server::get(ITimeFactory::class), $this->appConfig, $this->config, - $this->previewMapper, - $this->storageFactory, Server::get(IDBConnection::class), Server::get(IRootFolder::class), - $this->mimeTypeDetector, - $this->mimeTypeLoader, - $this->logger, - Server::get(IAppDataFactory::class), + new PreviewMigrationService( + $this->config, + Server::get(IRootFolder::class), + $this->logger, + $this->mimeTypeDetector, + $this->mimeTypeLoader, + Server::get(IDBConnection::class), + $this->previewMapper, + $this->storageFactory, + Server::get(IAppDataFactory::class), + ) ); $this->invokePrivate($job, 'run', [[]]); $this->assertEquals(0, count($this->previewAppData->getDirectoryListing())); @@ -144,18 +150,23 @@ class MovePreviewJobTest extends TestCase { $this->assertEquals(2, count($folder->getDirectoryListing())); $this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5)))); - $job = new MovePreviewJob( + $job = new PreviewMigrationJob( Server::get(ITimeFactory::class), $this->appConfig, $this->config, - $this->previewMapper, - $this->storageFactory, Server::get(IDBConnection::class), Server::get(IRootFolder::class), - $this->mimeTypeDetector, - $this->mimeTypeLoader, - $this->logger, - Server::get(IAppDataFactory::class) + new PreviewMigrationService( + $this->config, + Server::get(IRootFolder::class), + $this->logger, + $this->mimeTypeDetector, + $this->mimeTypeLoader, + Server::get(IDBConnection::class), + $this->previewMapper, + $this->storageFactory, + Server::get(IAppDataFactory::class), + ) ); $this->invokePrivate($job, 'run', [[]]); $this->assertEquals(0, count($this->previewAppData->getDirectoryListing())); @@ -184,18 +195,23 @@ class MovePreviewJobTest extends TestCase { $this->assertEquals(9, count($folder->getDirectoryListing())); $this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5)))); - $job = new MovePreviewJob( + $job = new PreviewMigrationJob( Server::get(ITimeFactory::class), $this->appConfig, $this->config, - $this->previewMapper, - $this->storageFactory, Server::get(IDBConnection::class), Server::get(IRootFolder::class), - $this->mimeTypeDetector, - $this->mimeTypeLoader, - $this->logger, - Server::get(IAppDataFactory::class) + new PreviewMigrationService( + $this->config, + Server::get(IRootFolder::class), + $this->logger, + $this->mimeTypeDetector, + $this->mimeTypeLoader, + Server::get(IDBConnection::class), + $this->previewMapper, + $this->storageFactory, + Server::get(IAppDataFactory::class), + ) ); $this->invokePrivate($job, 'run', [[]]); $previews = iterator_to_array($this->previewMapper->getAvailablePreviewsForFile(5)); From fe2a1ce7aad74d6012fa9b9f0963cbe506a08677 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 12 Jan 2026 13:54:58 +0100 Subject: [PATCH 2/2] feat(preview): Make it possible to disable on preview migration Signed-off-by: Carl Schwan (cherry picked from commit 7a025ffb0bf0b59bccd1bade41d98144babc3a31) --- core/AppInfo/ConfigLexicon.php | 8 ++++++++ lib/private/Preview/Generator.php | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/core/AppInfo/ConfigLexicon.php b/core/AppInfo/ConfigLexicon.php index 5d1f7269347..508a090e8a4 100644 --- a/core/AppInfo/ConfigLexicon.php +++ b/core/AppInfo/ConfigLexicon.php @@ -38,6 +38,8 @@ class ConfigLexicon implements ILexicon { public const LASTCRON_TIMESTAMP = 'lastcron'; + public const ON_DEMAND_PREVIEW_MIGRATION = 'on_demand_preview_migration'; + public function getStrictness(): Strictness { return Strictness::IGNORE; } @@ -93,6 +95,12 @@ class ConfigLexicon implements ILexicon { new Entry(self::OCM_INVITE_ACCEPT_DIALOG, ValueType::STRING, '', 'route to local invite accept dialog', note: 'set as empty string to disable feature'), new Entry(self::UNIFIED_SEARCH_MIN_SEARCH_LENGTH, ValueType::INT, 1, 'Minimum search length to trigger the request', rename: 'unified-search.min-search-length'), new Entry(self::UNIFIED_SEARCH_MAX_RESULTS_PER_REQUEST, ValueType::INT, 25, 'Maximum results returned per search request', rename: 'unified-search.max-results-per-request'), + new Entry( + key: self::ON_DEMAND_PREVIEW_MIGRATION, + type: ValueType::BOOL, + defaultRaw: true, + definition: 'Whether on demand preview migration is enabled.' + ), ]; } diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php index 1ceff60d54b..4627fad93b2 100644 --- a/lib/private/Preview/Generator.php +++ b/lib/private/Preview/Generator.php @@ -6,6 +6,7 @@ */ namespace OC\Preview; +use OC\Core\AppInfo\ConfigLexicon; use OC\Preview\Db\Preview; use OC\Preview\Db\PreviewMapper; use OC\Preview\Storage\PreviewFile; @@ -111,7 +112,7 @@ class Generator { [$file->getId() => $previews] = $this->previewMapper->getAvailablePreviews([$file->getId()]); - if (empty($previews)) { + if (empty($previews) && $this->appConfig->getValueBool('core', ConfigLexicon::ON_DEMAND_PREVIEW_MIGRATION)) { $previews = $this->migrateOldPreviews($file->getId()); } @@ -201,7 +202,7 @@ class Generator { } /** - * @return array + * @return Preview[] */ private function migrateOldPreviews(int $fileId): array { if ($this->appConfig->getValueBool('core', 'previewMovedDone')) {