diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml
index 606cb3b5bb1..21baeb9a33b 100644
--- a/build/psalm-baseline.xml
+++ b/build/psalm-baseline.xml
@@ -2708,17 +2708,6 @@
timeFactory->getTime()]]>
-
-
-
-
-
-
-
-
-
-
-
diff --git a/build/stubs/php-polyfill.php b/build/stubs/php-polyfill.php
new file mode 100644
index 00000000000..606a21f8dfe
--- /dev/null
+++ b/build/stubs/php-polyfill.php
@@ -0,0 +1,9 @@
+doRun($argument);
- } catch (\Throwable $exception) {
- echo $exception->getMessage();
- throw $exception;
- }
- }
-
- private function doRun($argument): void {
if ($this->appConfig->getValueBool('core', 'previewMovedDone')) {
return;
}
@@ -59,14 +54,13 @@ class MovePreviewJob extends TimedJob {
$startTime = time();
while (true) {
- $previewFolders = [];
-
// Check new hierarchical preview folders first
if (!$emptyHierarchicalPreviewFolders) {
$qb = $this->connection->getQueryBuilder();
$qb->select('*')
->from('filecache')
->where($qb->expr()->like('path', $qb->createNamedParameter('appdata_%/preview/%/%/%/%/%/%/%/%/%')))
+ ->hintShardKey('storage', $this->rootFolder->getMountPoint()->getNumericStorageId())
->setMaxResults(100);
$result = $qb->executeQuery();
@@ -74,12 +68,7 @@ class MovePreviewJob extends TimedJob {
$pathSplit = explode('/', $row['path']);
assert(count($pathSplit) >= 2);
$fileId = $pathSplit[count($pathSplit) - 2];
- $previewFolders[$fileId][] = $row['path'];
- }
-
- if (!empty($previewFolders)) {
- $this->processPreviews($previewFolders, false);
- continue;
+ $this->processPreviews($fileId, false);
}
}
@@ -89,6 +78,7 @@ class MovePreviewJob extends TimedJob {
$qb->select('*')
->from('filecache')
->where($qb->expr()->like('path', $qb->createNamedParameter('appdata_%/preview/%/%.%')))
+ ->hintShardKey('storage', $this->rootFolder->getMountPoint()->getNumericStorageId())
->setMaxResults(100);
$result = $qb->executeQuery();
@@ -98,18 +88,7 @@ class MovePreviewJob extends TimedJob {
$fileId = $pathSplit[count($pathSplit) - 2];
array_pop($pathSplit);
$path = implode('/', $pathSplit);
- if (!isset($previewFolders[$fileId])) {
- $previewFolders[$fileId] = [];
- }
- if (!in_array($path, $previewFolders[$fileId])) {
- $previewFolders[$fileId][] = $path;
- }
- }
-
- if (empty($previewFolders)) {
- break;
- } else {
- $this->processPreviews($previewFolders, true);
+ $this->processPreviews($fileId, true);
}
// Stop if execution time is more than one hour.
@@ -118,97 +97,117 @@ class MovePreviewJob extends TimedJob {
}
}
- // Delete any leftover preview directory
- $this->appData->getFolder('.')->delete();
+ try {
+ // Delete any leftover preview directory
+ $this->appData->getFolder('.')->delete();
+ } catch (NotFoundException) {
+ // ignore
+ }
$this->appConfig->setValueBool('core', 'previewMovedDone', true);
}
/**
* @param array $previewFolders
*/
- private function processPreviews(array $previewFolders, bool $simplePaths): void {
- foreach ($previewFolders as $fileId => $previewFolder) {
- $internalPath = $this->getInternalFolder((string)$fileId, $simplePaths);
- $folder = $this->appData->getFolder($internalPath);
+ private function processPreviews(int|string $fileId, bool $simplePaths): void {
+ $internalPath = $this->getInternalFolder((string)$fileId, $simplePaths);
+ $folder = $this->appData->getFolder($internalPath);
- /**
- * @var list $previewFiles
- */
- $previewFiles = [];
+ /**
+ * @var list $previewFiles
+ */
+ $previewFiles = [];
- foreach ($folder->getDirectoryListing() as $previewFile) {
- /** @var SimpleFile $previewFile */
- [0 => $baseName, 1 => $extension] = explode('.', $previewFile->getName());
- $nameSplit = explode('-', $baseName);
+ foreach ($folder->getDirectoryListing() as $previewFile) {
+ /** @var SimpleFile $previewFile */
+ [0 => $baseName, 1 => $extension] = explode('.', $previewFile->getName());
+ $nameSplit = explode('-', $baseName);
- // TODO VERSION/PREFIX extraction
-
- $width = $nameSplit[0];
- $height = $nameSplit[1];
-
- if (isset($nameSplit[2])) {
- $crop = $nameSplit[2] === 'crop';
- $max = $nameSplit[2] === 'max';
- }
-
- $previewFiles[] = [
- 'file' => $previewFile,
- 'width' => $width,
- 'height' => $height,
- 'crop' => $crop,
- 'max' => $max,
- 'extension' => $extension,
- 'size' => $previewFile->getSize(),
- 'mtime' => $previewFile->getMTime(),
- ];
+ $offset = 0;
+ $version = null;
+ if (count($nameSplit) === 4 || (count($nameSplit) === 3 && is_numeric($nameSplit[2]))) {
+ $offset = 1;
+ $version = (int)$nameSplit[0];
}
- $qb = $this->connection->getQueryBuilder();
- $qb->select('*')
- ->from('filecache')
- ->where($qb->expr()->like('fileid', $qb->createNamedParameter($fileId)));
+ $width = (int)$nameSplit[$offset + 0];
+ $height = (int)$nameSplit[$offset + 1];
- $result = $qb->executeQuery();
- $result = $result->fetchAll();
-
- if (count($result) > 0) {
- foreach ($previewFiles as $previewFile) {
- $preview = new Preview();
- $preview->setFileId((int)$fileId);
- $preview->setOldFileId($previewFile['file']->getId());
- $preview->setEtag($result[0]['etag']);
- $preview->setMtime($previewFile['mtime']);
- $preview->setWidth($previewFile['width']);
- $preview->setHeight($previewFile['height']);
- $preview->setCrop($previewFile['crop']);
- $preview->setIsMax($previewFile['max']);
- $preview->setMimetype(match ($previewFile['extension']) {
- 'png' => IPreview::MIMETYPE_PNG,
- 'webp' => IPreview::MIMETYPE_WEBP,
- 'gif' => IPreview::MIMETYPE_GIF,
- default => IPreview::MIMETYPE_JPEG,
- });
- $preview->setSize($previewFile['size']);
- try {
- $preview = $this->previewMapper->insert($preview);
- } catch (Exception $e) {
- // We already have this preview in the preview table, skip
- continue;
- }
-
- try {
- $this->storageFactory->migratePreview($preview, $previewFile['file']);
- $previewFile['file']->delete();
- } catch (\Exception $e) {
- $this->previewMapper->delete($preview);
- throw $e;
- }
-
- }
+ $crop = false;
+ $max = false;
+ if (isset($nameSplit[$offset + 2])) {
+ $crop = $nameSplit[$offset + 2] === 'crop';
+ $max = $nameSplit[$offset + 2] === 'max';
}
- $this->deleteFolder($internalPath, $folder);
+ $previewFiles[] = [
+ 'file' => $previewFile,
+ 'width' => $width,
+ 'height' => $height,
+ 'crop' => $crop,
+ 'version' => $version,
+ 'max' => $max,
+ 'extension' => $extension,
+ 'size' => $previewFile->getSize(),
+ 'mtime' => $previewFile->getMTime(),
+ ];
}
+
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('*')
+ ->from('filecache')
+ ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($fileId)))
+ ->setMaxResults(1);
+
+ $result = $qb->executeQuery();
+ $result = $result->fetchAll();
+
+ if (count($result) > 0) {
+ foreach ($previewFiles as $previewFile) {
+ $preview = new Preview();
+ $preview->setFileId((int)$fileId);
+ /** @var SimpleFile $file */
+ $file = $previewFile['file'];
+ $preview->setOldFileId($file->getId());
+ $preview->setStorageId($result[0]['storage']);
+ $preview->setEtag($result[0]['etag']);
+ $preview->setMtime($previewFile['mtime']);
+ $preview->setWidth($previewFile['width']);
+ $preview->setHeight($previewFile['height']);
+ $preview->setCropped($previewFile['crop']);
+ $preview->setVersion($previewFile['version']);
+ $preview->setMax($previewFile['max']);
+ $preview->setEncrypted(false);
+ $preview->setMimetype(match ($previewFile['extension']) {
+ 'png' => IPreview::MIMETYPE_PNG,
+ 'webp' => IPreview::MIMETYPE_WEBP,
+ 'gif' => IPreview::MIMETYPE_GIF,
+ default => IPreview::MIMETYPE_JPEG,
+ });
+ $preview->setSize($previewFile['size']);
+ try {
+ $preview = $this->previewMapper->insert($preview);
+ } catch (Exception $e) {
+ // We already have this preview in the preview table, skip
+ continue;
+ }
+
+ try {
+ $this->storageFactory->migratePreview($preview, $file);
+ $qb->delete('filecache')
+ ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($file->getId())))
+ ->executeStatement();
+ // Do not call $file->delete() as this will also delete the file from the file system
+ } catch (\Exception $e) {
+ $this->previewMapper->delete($preview);
+ throw $e;
+ }
+ }
+ }
+
+ $this->deleteFolder($internalPath, $folder);
}
public static function getInternalFolder(string $name, bool $simplePaths): string {
diff --git a/core/Command/Preview/Repair.php b/core/Command/Preview/Repair.php
deleted file mode 100644
index a92a4cf8ed0..00000000000
--- a/core/Command/Preview/Repair.php
+++ /dev/null
@@ -1,293 +0,0 @@
-memoryLimit = (int)$phpIni->getBytes('memory_limit');
- $this->memoryTreshold = $this->memoryLimit - 25 * 1024 * 1024;
-
- parent::__construct();
- }
-
- protected function configure() {
- $this
- ->setName('preview:repair')
- ->setDescription('distributes the existing previews into subfolders')
- ->addOption('batch', 'b', InputOption::VALUE_NONE, 'Batch mode - will not ask to start the migration and start it right away.')
- ->addOption('dry', 'd', InputOption::VALUE_NONE, 'Dry mode - will not create, move or delete any files - in combination with the verbose mode one could check the operations.')
- ->addOption('delete', null, InputOption::VALUE_NONE, 'Delete instead of migrating them. Usefull if too many entries to migrate.');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- if ($this->memoryLimit !== -1) {
- $limitInMiB = round($this->memoryLimit / 1024 / 1024, 1);
- $thresholdInMiB = round($this->memoryTreshold / 1024 / 1024, 1);
- $output->writeln("Memory limit is $limitInMiB MiB");
- $output->writeln("Memory threshold is $thresholdInMiB MiB");
- $output->writeln('');
- $memoryCheckEnabled = true;
- } else {
- $output->writeln('No memory limit in place - disabled memory check. Set a PHP memory limit to automatically stop the execution of this migration script once memory consumption is close to this limit.');
- $output->writeln('');
- $memoryCheckEnabled = false;
- }
-
- $dryMode = $input->getOption('dry');
- $deleteMode = $input->getOption('delete');
-
-
- if ($dryMode) {
- $output->writeln('INFO: The migration is run in dry mode and will not modify anything.');
- $output->writeln('');
- } elseif ($deleteMode) {
- $output->writeln('WARN: The migration will _DELETE_ old previews.');
- $output->writeln('');
- }
-
- $instanceId = $this->config->getSystemValueString('instanceid');
-
- $output->writeln('This will migrate all previews from the old preview location to the new one.');
- $output->writeln('');
-
- $output->writeln('Fetching previews that need to be migrated …');
- /** @var Folder $currentPreviewFolder */
- $currentPreviewFolder = $this->rootFolder->get("appdata_$instanceId/preview");
-
- $directoryListing = $currentPreviewFolder->getDirectoryListing();
-
- $total = count($directoryListing);
- /**
- * by default there could be 0-9 a-f and the old-multibucket folder which are all fine
- */
- if ($total < 18) {
- $directoryListing = array_filter($directoryListing, function ($dir) {
- if ($dir->getName() === 'old-multibucket') {
- return false;
- }
-
- // a-f can't be a file ID -> removing from migration
- if (preg_match('!^[a-f]$!', $dir->getName())) {
- return false;
- }
-
- if (preg_match('!^[0-9]$!', $dir->getName())) {
- // ignore folders that only has folders in them
- if ($dir instanceof Folder) {
- foreach ($dir->getDirectoryListing() as $entry) {
- if (!$entry instanceof Folder) {
- return true;
- }
- }
- return false;
- }
- }
- return true;
- });
- $total = count($directoryListing);
- }
-
- if ($total === 0) {
- $output->writeln('All previews are already migrated.');
- return 0;
- }
-
- $output->writeln("A total of $total preview files need to be migrated.");
- $output->writeln('');
- $output->writeln('The migration will always migrate all previews of a single file in a batch. After each batch the process can be canceled by pressing CTRL-C. This will finish the current batch and then stop the migration. This migration can then just be started and it will continue.');
-
- if ($input->getOption('batch')) {
- $output->writeln('Batch mode active: migration is started right away.');
- } else {
- /** @var QuestionHelper $helper */
- $helper = $this->getHelper('question');
- $question = new ConfirmationQuestion('Should the migration be started? (y/[n]) ', false);
-
- if (!$helper->ask($input, $output, $question)) {
- return 0;
- }
- }
-
- // register the SIGINT listener late in here to be able to exit in the early process of this command
- pcntl_signal(SIGINT, [$this, 'sigIntHandler']);
-
- $output->writeln('');
- $output->writeln('');
- $section1 = $output->section();
- $section2 = $output->section();
- $progressBar = new ProgressBar($section2, $total);
- $progressBar->setFormat('%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% Used Memory: %memory:6s%');
- $time = (new \DateTime())->format('H:i:s');
- $progressBar->setMessage("$time Starting …");
- $progressBar->maxSecondsBetweenRedraws(0.2);
- $progressBar->start();
-
- foreach ($directoryListing as $oldPreviewFolder) {
- pcntl_signal_dispatch();
- $name = $oldPreviewFolder->getName();
- $time = (new \DateTime())->format('H:i:s');
- $section1->writeln("$time Migrating previews of file with fileId $name …");
- $progressBar->display();
-
- if ($this->stopSignalReceived) {
- $section1->writeln("$time Stopping migration …");
- return 0;
- }
- if (!$oldPreviewFolder instanceof Folder) {
- $section1->writeln(" Skipping non-folder $name …");
- $progressBar->advance();
- continue;
- }
- if ($name === 'old-multibucket') {
- $section1->writeln(" Skipping fallback mount point $name …");
- $progressBar->advance();
- continue;
- }
- if (in_array($name, ['a', 'b', 'c', 'd', 'e', 'f'])) {
- $section1->writeln(" Skipping hex-digit folder $name …");
- $progressBar->advance();
- continue;
- }
- if (!preg_match('!^\d+$!', $name)) {
- $section1->writeln(" Skipping non-numeric folder $name …");
- $progressBar->advance();
- continue;
- }
-
- $newFoldername = Root::getInternalFolder($name);
-
- $memoryUsage = memory_get_usage();
- if ($memoryCheckEnabled && $memoryUsage > $this->memoryTreshold) {
- $section1->writeln('');
- $section1->writeln('');
- $section1->writeln('');
- $section1->writeln(' Stopped process 25 MB before reaching the memory limit to avoid a hard crash.');
- $time = (new \DateTime())->format('H:i:s');
- $section1->writeln("$time Reached memory limit and stopped to avoid hard crash.");
- return 1;
- }
-
- $lockName = 'occ preview:repair lock ' . $oldPreviewFolder->getId();
- try {
- $section1->writeln(" Locking \"$lockName\" …", OutputInterface::VERBOSITY_VERBOSE);
- $this->lockingProvider->acquireLock($lockName, ILockingProvider::LOCK_EXCLUSIVE);
- } catch (LockedException $e) {
- $section1->writeln(' Skipping because it is locked - another process seems to work on this …');
- continue;
- }
-
- $previews = $oldPreviewFolder->getDirectoryListing();
- if ($previews !== []) {
- try {
- $this->rootFolder->get("appdata_$instanceId/preview/$newFoldername");
- } catch (NotFoundException $e) {
- $section1->writeln(" Create folder preview/$newFoldername", OutputInterface::VERBOSITY_VERBOSE);
- if (!$dryMode) {
- $this->rootFolder->newFolder("appdata_$instanceId/preview/$newFoldername");
- }
- }
-
- foreach ($previews as $preview) {
- pcntl_signal_dispatch();
- $previewName = $preview->getName();
-
- if ($preview instanceof Folder) {
- $section1->writeln(" Skipping folder $name/$previewName …");
- $progressBar->advance();
- continue;
- }
-
- // Execute process
- if (!$dryMode) {
- // Delete preview instead of moving
- if ($deleteMode) {
- try {
- $section1->writeln(" Delete preview/$name/$previewName", OutputInterface::VERBOSITY_VERBOSE);
- $preview->delete();
- } catch (\Exception $e) {
- $this->logger->error("Failed to delete preview at preview/$name/$previewName", [
- 'app' => 'core',
- 'exception' => $e,
- ]);
- }
- } else {
- try {
- $section1->writeln(" Move preview/$name/$previewName to preview/$newFoldername", OutputInterface::VERBOSITY_VERBOSE);
- $preview->move("appdata_$instanceId/preview/$newFoldername/$previewName");
- } catch (\Exception $e) {
- $this->logger->error("Failed to move preview from preview/$name/$previewName to preview/$newFoldername", [
- 'app' => 'core',
- 'exception' => $e,
- ]);
- }
- }
- }
- }
- }
-
- if ($oldPreviewFolder->getDirectoryListing() === []) {
- $section1->writeln(" Delete empty folder preview/$name", OutputInterface::VERBOSITY_VERBOSE);
- if (!$dryMode) {
- try {
- $oldPreviewFolder->delete();
- } catch (\Exception $e) {
- $this->logger->error("Failed to delete empty folder preview/$name", [
- 'app' => 'core',
- 'exception' => $e,
- ]);
- }
- }
- }
-
- $this->lockingProvider->releaseLock($lockName, ILockingProvider::LOCK_EXCLUSIVE);
- $section1->writeln(' Unlocked', OutputInterface::VERBOSITY_VERBOSE);
-
- $section1->writeln(" Finished migrating previews of file with fileId $name …");
- $progressBar->advance();
- }
-
- $progressBar->finish();
- $output->writeln('');
- return 0;
- }
-
- protected function sigIntHandler() {
- echo "\nSignal received - will finish the step and then stop the migration.\n\n\n";
- $this->stopSignalReceived = true;
- }
-}
diff --git a/core/Command/Preview/ResetRenderedTexts.php b/core/Command/Preview/ResetRenderedTexts.php
index ce874e15406..fb7fb9fa620 100644
--- a/core/Command/Preview/ResetRenderedTexts.php
+++ b/core/Command/Preview/ResetRenderedTexts.php
@@ -8,8 +8,8 @@ declare(strict_types=1);
*/
namespace OC\Core\Command\Preview;
-use OC\Preview\Storage\Root;
-use OCP\DB\QueryBuilder\IQueryBuilder;
+use OC\Preview\Db\Preview;
+use OC\Preview\PreviewService;
use OCP\Files\IMimeTypeLoader;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
@@ -23,16 +23,16 @@ use Symfony\Component\Console\Output\OutputInterface;
class ResetRenderedTexts extends Command {
public function __construct(
- protected IDBConnection $connection,
- protected IUserManager $userManager,
- protected IAvatarManager $avatarManager,
- private Root $previewFolder,
- private IMimeTypeLoader $mimeTypeLoader,
+ protected readonly IDBConnection $connection,
+ protected readonly IUserManager $userManager,
+ protected readonly IAvatarManager $avatarManager,
+ private readonly PreviewService $previewService,
+ private readonly IMimeTypeLoader $mimeTypeLoader,
) {
parent::__construct();
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('preview:reset-rendered-texts')
->setDescription('Deletes all generated avatars and previews of text and md files')
@@ -91,7 +91,7 @@ class ResetRenderedTexts extends Command {
private function deletePreviews(OutputInterface $output, bool $dryMode): void {
$previewsToDeleteCount = 0;
- foreach ($this->getPreviewsToDelete() as ['name' => $previewFileId, 'path' => $filePath]) {
+ foreach ($this->getPreviewsToDelete() as ['path' => $filePath, 'preview' => $preview]) {
$output->writeln('Deleting previews for ' . $filePath, OutputInterface::VERBOSITY_VERBOSE);
$previewsToDeleteCount++;
@@ -100,63 +100,33 @@ class ResetRenderedTexts extends Command {
continue;
}
- try {
- $preview = $this->previewFolder->getFolder((string)$previewFileId);
- $preview->delete();
- } catch (NotFoundException $e) {
- // continue
- } catch (NotPermittedException $e) {
- // continue
- }
+ $this->previewService->deletePreview($preview);
}
$output->writeln('Deleted ' . $previewsToDeleteCount . ' previews');
}
- // Copy pasted and adjusted from
- // "lib/private/Preview/BackgroundCleanupJob.php".
+ /**
+ * @return \Iterator
+ */
private function getPreviewsToDelete(): \Iterator {
$qb = $this->connection->getQueryBuilder();
- $qb->select('path', 'mimetype')
+ $qb->select('fileid', 'path')
->from('filecache')
- ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($this->previewFolder->getId())));
- $cursor = $qb->executeQuery();
- $data = $cursor->fetch();
- $cursor->closeCursor();
-
- if ($data === null) {
- return [];
- }
-
- /*
- * This lovely like is the result of the way the new previews are stored
- * We take the md5 of the name (fileid) and split the first 7 chars. That way
- * there are not a gazillion files in the root of the preview appdata.
- */
- $like = $this->connection->escapeLikeParameter($data['path']) . '/_/_/_/_/_/_/_/%';
-
- $qb = $this->connection->getQueryBuilder();
- $qb->select('a.name', 'b.path')
- ->from('filecache', 'a')
- ->leftJoin('a', 'filecache', 'b', $qb->expr()->eq(
- $qb->expr()->castColumn('a.name', IQueryBuilder::PARAM_INT), 'b.fileid'
- ))
->where(
- $qb->expr()->andX(
- $qb->expr()->like('a.path', $qb->createNamedParameter($like)),
- $qb->expr()->eq('a.mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('httpd/unix-directory'))),
- $qb->expr()->orX(
- $qb->expr()->eq('b.mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('text/plain'))),
- $qb->expr()->eq('b.mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('text/markdown'))),
- $qb->expr()->eq('b.mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('text/x-markdown')))
- )
+ $qb->expr()->orX(
+ $qb->expr()->eq('mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('text/plain'))),
+ $qb->expr()->eq('mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('text/markdown'))),
+ $qb->expr()->eq('mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('text/x-markdown')))
)
);
$cursor = $qb->executeQuery();
while ($row = $cursor->fetch()) {
- yield $row;
+ foreach ($this->previewService->getAvailablePreviewForFile($row['fileid']) as $preview) {
+ yield ['path' => $row['path'], 'preview' => $preview];
+ }
}
$cursor->closeCursor();
diff --git a/core/Migrations/Version33000Date20250819110529.php b/core/Migrations/Version33000Date20250819110529.php
index 5e607024ec7..27bf9ab89b7 100644
--- a/core/Migrations/Version33000Date20250819110529.php
+++ b/core/Migrations/Version33000Date20250819110529.php
@@ -41,6 +41,7 @@ class Version33000Date20250819110529 extends SimpleMigrationStep {
$table = $schema->createTable('previews');
$table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'length' => 20, 'unsigned' => true]);
$table->addColumn('file_id', Types::BIGINT, ['notnull' => true, 'length' => 20, 'unsigned' => true]);
+ $table->addColumn('storage_id', Types::BIGINT, ['notnull' => true, 'length' => 20, 'unsigned' => true]);
$table->addColumn('old_file_id', Types::BIGINT, ['notnull' => false, 'length' => 20, 'unsigned' => true]);
$table->addColumn('location_id', Types::BIGINT, ['notnull' => false, 'length' => 20, 'unsigned' => true]);
$table->addColumn('width', Types::INTEGER, ['notnull' => true, 'unsigned' => true]);
@@ -56,7 +57,7 @@ class Version33000Date20250819110529 extends SimpleMigrationStep {
$table->addColumn('version', Types::BIGINT, ['notnull' => true, 'default' => -1]); // can not be null otherwise unique index doesn't work
$table->setPrimaryKey(['id']);
$table->addIndex(['file_id']);
- $table->addUniqueIndex(['file_id', 'width', 'height', 'mimetype', 'crop', 'version'], 'previews_file_uniq_idx');
+ $table->addUniqueIndex(['file_id', 'width', 'height', 'mimetype', 'cropped', 'version'], 'previews_file_uniq_idx');
}
return $schema;
diff --git a/core/register_command.php b/core/register_command.php
index 9fd5b9b611e..f6c0b9466b5 100644
--- a/core/register_command.php
+++ b/core/register_command.php
@@ -206,7 +206,6 @@ if ($config->getSystemValueBool('installed', false)) {
$application->add(Server::get(Command\Preview\Cleanup::class));
$application->add(Server::get(Generate::class));
- $application->add(Server::get(Command\Preview\Repair::class));
$application->add(Server::get(ResetRenderedTexts::class));
$application->add(Server::get(Add::class));
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 5c168a25de3..4c2e473240b 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -1344,7 +1344,6 @@ return array(
'OC\\Core\\Command\\Memcache\\RedisCommand' => $baseDir . '/core/Command/Memcache/RedisCommand.php',
'OC\\Core\\Command\\Preview\\Cleanup' => $baseDir . '/core/Command/Preview/Cleanup.php',
'OC\\Core\\Command\\Preview\\Generate' => $baseDir . '/core/Command/Preview/Generate.php',
- 'OC\\Core\\Command\\Preview\\Repair' => $baseDir . '/core/Command/Preview/Repair.php',
'OC\\Core\\Command\\Preview\\ResetRenderedTexts' => $baseDir . '/core/Command/Preview/ResetRenderedTexts.php',
'OC\\Core\\Command\\Router\\ListRoutes' => $baseDir . '/core/Command/Router/ListRoutes.php',
'OC\\Core\\Command\\Router\\MatchRoute' => $baseDir . '/core/Command/Router/MatchRoute.php',
@@ -1909,6 +1908,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\\PreviewService' => $baseDir . '/lib/private/Preview/PreviewService.php',
'OC\\Preview\\Provider' => $baseDir . '/lib/private/Preview/Provider.php',
'OC\\Preview\\ProviderV1Adapter' => $baseDir . '/lib/private/Preview/ProviderV1Adapter.php',
'OC\\Preview\\ProviderV2' => $baseDir . '/lib/private/Preview/ProviderV2.php',
@@ -1919,7 +1919,6 @@ return array(
'OC\\Preview\\Storage\\LocalPreviewStorage' => $baseDir . '/lib/private/Preview/Storage/LocalPreviewStorage.php',
'OC\\Preview\\Storage\\ObjectStorePreviewStorage' => $baseDir . '/lib/private/Preview/Storage/ObjectStorePreviewStorage.php',
'OC\\Preview\\Storage\\PreviewFile' => $baseDir . '/lib/private/Preview/Storage/PreviewFile.php',
- 'OC\\Preview\\Storage\\Root' => $baseDir . '/lib/private/Preview/Storage/Root.php',
'OC\\Preview\\Storage\\StorageFactory' => $baseDir . '/lib/private/Preview/Storage/StorageFactory.php',
'OC\\Preview\\TGA' => $baseDir . '/lib/private/Preview/TGA.php',
'OC\\Preview\\TIFF' => $baseDir . '/lib/private/Preview/TIFF.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 423b4cc6b57..2cb14099c01 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -11,32 +11,32 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
);
public static $prefixLengthsPsr4 = array (
- 'O' =>
+ 'O' =>
array (
'OC\\Core\\' => 8,
'OC\\' => 3,
'OCP\\' => 4,
),
- 'N' =>
+ 'N' =>
array (
'NCU\\' => 4,
),
);
public static $prefixDirsPsr4 = array (
- 'OC\\Core\\' =>
+ 'OC\\Core\\' =>
array (
0 => __DIR__ . '/../../..' . '/core',
),
- 'OC\\' =>
+ 'OC\\' =>
array (
0 => __DIR__ . '/../../..' . '/lib/private',
),
- 'OCP\\' =>
+ 'OCP\\' =>
array (
0 => __DIR__ . '/../../..' . '/lib/public',
),
- 'NCU\\' =>
+ 'NCU\\' =>
array (
0 => __DIR__ . '/../../..' . '/lib/unstable',
),
@@ -1385,7 +1385,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Command\\Memcache\\RedisCommand' => __DIR__ . '/../../..' . '/core/Command/Memcache/RedisCommand.php',
'OC\\Core\\Command\\Preview\\Cleanup' => __DIR__ . '/../../..' . '/core/Command/Preview/Cleanup.php',
'OC\\Core\\Command\\Preview\\Generate' => __DIR__ . '/../../..' . '/core/Command/Preview/Generate.php',
- 'OC\\Core\\Command\\Preview\\Repair' => __DIR__ . '/../../..' . '/core/Command/Preview/Repair.php',
'OC\\Core\\Command\\Preview\\ResetRenderedTexts' => __DIR__ . '/../../..' . '/core/Command/Preview/ResetRenderedTexts.php',
'OC\\Core\\Command\\Router\\ListRoutes' => __DIR__ . '/../../..' . '/core/Command/Router/ListRoutes.php',
'OC\\Core\\Command\\Router\\MatchRoute' => __DIR__ . '/../../..' . '/core/Command/Router/MatchRoute.php',
@@ -1950,6 +1949,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\\PreviewService' => __DIR__ . '/../../..' . '/lib/private/Preview/PreviewService.php',
'OC\\Preview\\Provider' => __DIR__ . '/../../..' . '/lib/private/Preview/Provider.php',
'OC\\Preview\\ProviderV1Adapter' => __DIR__ . '/../../..' . '/lib/private/Preview/ProviderV1Adapter.php',
'OC\\Preview\\ProviderV2' => __DIR__ . '/../../..' . '/lib/private/Preview/ProviderV2.php',
@@ -1960,7 +1960,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Preview\\Storage\\LocalPreviewStorage' => __DIR__ . '/../../..' . '/lib/private/Preview/Storage/LocalPreviewStorage.php',
'OC\\Preview\\Storage\\ObjectStorePreviewStorage' => __DIR__ . '/../../..' . '/lib/private/Preview/Storage/ObjectStorePreviewStorage.php',
'OC\\Preview\\Storage\\PreviewFile' => __DIR__ . '/../../..' . '/lib/private/Preview/Storage/PreviewFile.php',
- 'OC\\Preview\\Storage\\Root' => __DIR__ . '/../../..' . '/lib/private/Preview/Storage/Root.php',
'OC\\Preview\\Storage\\StorageFactory' => __DIR__ . '/../../..' . '/lib/private/Preview/Storage/StorageFactory.php',
'OC\\Preview\\TGA' => __DIR__ . '/../../..' . '/lib/private/Preview/TGA.php',
'OC\\Preview\\TIFF' => __DIR__ . '/../../..' . '/lib/private/Preview/TIFF.php',
diff --git a/lib/private/BackgroundJob/JobList.php b/lib/private/BackgroundJob/JobList.php
index 302bee22cd5..c00a51e3851 100644
--- a/lib/private/BackgroundJob/JobList.php
+++ b/lib/private/BackgroundJob/JobList.php
@@ -321,7 +321,6 @@ class JobList implements IJobList {
/** @var IJob $job */
$job = \OCP\Server::get($row['class']);
} catch (QueryException $e) {
- echo $e->getMessage();
if (class_exists($row['class'])) {
$class = $row['class'];
$job = new $class();
diff --git a/lib/private/Preview/BackgroundCleanupJob.php b/lib/private/Preview/BackgroundCleanupJob.php
index d65b740745d..257bebb1d5d 100644
--- a/lib/private/Preview/BackgroundCleanupJob.php
+++ b/lib/private/Preview/BackgroundCleanupJob.php
@@ -8,8 +8,7 @@ declare(strict_types=1);
*/
namespace OC\Preview;
-use OC\Preview\Db\PreviewMapper;
-use OC\Preview\Storage\StorageFactory;
+use OC\Preview\Db\Preview;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\DB\QueryBuilder\IQueryBuilder;
@@ -24,8 +23,7 @@ class BackgroundCleanupJob extends TimedJob {
public function __construct(
ITimeFactory $timeFactory,
readonly private IDBConnection $connection,
- readonly private PreviewMapper $previewMapper,
- readonly private StorageFactory $storageFactory,
+ readonly private PreviewService $previewService,
readonly private bool $isCLI,
) {
parent::__construct($timeFactory);
@@ -37,11 +35,9 @@ class BackgroundCleanupJob extends TimedJob {
public function run($argument): void {
foreach ($this->getDeletedFiles() as $fileId) {
$previewIds = [];
- foreach ($this->previewMapper->getByFileId($fileId) as $preview) {
- $previewIds[] = $preview->getId();
- $this->storageFactory->deletePreview($preview);
+ foreach ($this->previewService->getAvailablePreviewForFile($fileId) as $preview) {
+ $this->previewService->deletePreview($preview);
}
- $this->previewMapper->deleteByIds($previewIds);
}
}
@@ -50,13 +46,12 @@ class BackgroundCleanupJob extends TimedJob {
*/
private function getDeletedFiles(): \Iterator {
if ($this->connection->getShardDefinition('filecache')) {
- $chunks = $this->getAllPreviewIds(1000);
- foreach ($chunks as $chunk) {
- foreach ($chunk as $storage => $preview) {
- yield [$storage => $this->findMissingSources($storage, $preview)];
+ foreach ($this->previewService->getAvailableFileIds() as $availableFileIdGroup) {
+ $fileIds = $this->findMissingSources($availableFileIdGroup['storageId'], $availableFileIdGroup['fileIds']);
+ foreach ($fileIds as $fileId) {
+ yield $fileId;
}
}
-
return;
}
@@ -89,35 +84,11 @@ class BackgroundCleanupJob extends TimedJob {
$cursor = $qb->executeQuery();
while ($row = $cursor->fetch()) {
- yield $row['file_id'];
+ yield (int)$row['file_id'];
}
$cursor->closeCursor();
}
- /**
- * @return \Iterator
- */
- private function getAllPreviewIds(int $chunkSize): \Iterator {
- $qb = $this->connection->getQueryBuilder();
- $qb->select('id', 'file_id', 'storage_id')
- ->from('previews')
- ->where(
- $qb->expr()->gt('id', $qb->createParameter('min_id')),
- )
- ->orderBy('id', 'ASC')
- ->setMaxResults($chunkSize);
-
- $minId = 0;
- while (true) {
- $qb->setParameter('min_id', $minId);
- $cursor = $qb->executeQuery();
- while ($row = $cursor->fetch()) {
- yield $row['file_id'];
- }
- $cursor->closeCursor();
- }
- }
-
/**
* @param FileId[] $ids
* @return FileId[]
diff --git a/lib/private/Preview/Db/Preview.php b/lib/private/Preview/Db/Preview.php
index b4ebe8b4805..b6f2ef2942e 100644
--- a/lib/private/Preview/Db/Preview.php
+++ b/lib/private/Preview/Db/Preview.php
@@ -15,64 +15,63 @@ use OCP\DB\Types;
use OCP\IPreview;
/**
- * @method \int getFileId()
+ * Preview entity mapped to the oc_previews and oc_preview_locations table.
+ *
+ * @method int getFileId() Get the file id of the original file.
* @method void setFileId(int $fileId)
- * @method \int getOldFileId() // Old location in the file-cache table, for legacy compatibility
- * @method void setOldFileId(int $fileId)
- * @method \int getLocationId()
+ * @method int getStorageId() Get the storage id of the original file.
+ * @method void setStorageId(int $fileId)
+ * @method int getOldFileId() Get the old location in the file-cache table, for legacy compatibility.
+ * @method void setOldFileId(int $oldFileId)
+ * @method int getLocationId() Get the location id in the preview_locations table. Only set when using an object store as primary storage.
* @method void setLocationId(int $locationId)
- * @method \string getBucketName()
- * @method \string getObjectStoreName()
- * @method \int getWidth()
+ * @method string getBucketName() Get the bucket name where the preview is stored. This is stored in the preview_locations table.
+ * @method string getObjectStoreName() Get the object store name where the preview is stored. This is stored in the preview_locations table.
+ * @method int getWidth() Get the width of the preview.
* @method void setWidth(int $width)
- * @method \int getHeight()
+ * @method int getHeight() Get the height of the preview.
* @method void setHeight(int $height)
- * @method \int getMode()
- * @method void setMode(int $mode)
- * @method \bool getCrop()
- * @method void setCrop(bool $crop)
- * @method void setMimetype(int $mimetype)
- * @method IPreview::MIMETYPE_* getMimetype()
- * @method \int getMtime()
+ * @method bool isCropped() Get whether the preview is cropped or not.
+ * @method void setCropped(bool $cropped)
+ * @method void setMimetype(int $mimetype) Set the mimetype of the preview.
+ * @method int getMimetype() Get the mimetype of the preview.
+ * @method int getMtime() Get the modification time of the preview.
* @method void setMtime(int $mtime)
- * @method \int getSize()
+ * @method int getSize() Get the size of the preview.
* @method void setSize(int $size)
- * @method \bool getIsMax()
- * @method void setIsMax(bool $max)
- * @method \string getEtag()
+ * @method bool isMax() Get whether the preview is the biggest one which is then used to generate the smaller previews.
+ * @method void setMax(bool $max)
+ * @method string getEtag() Get the etag of the preview.
* @method void setEtag(string $etag)
- * @method ?\int getVersion()
+ * @method int|null getVersion() Get the version for files_versions_s3
* @method void setVersion(?int $version)
+ * @method bool|null getIs() Get the version for files_versions_s3
+ * @method bool isEncrypted() Get whether the preview is encrypted. At the moment every preview is unencrypted.
+ * @method void setEncrypted(bool $encrypted)
+ *
+ * @see PreviewMapper
*/
class Preview extends Entity {
protected ?int $fileId = null;
-
protected ?int $oldFileId = null;
-
+ protected ?int $storageId = null;
protected ?int $locationId = null;
protected ?string $bucketName = null;
protected ?string $objectStoreName = null;
-
protected ?int $width = null;
-
protected ?int $height = null;
-
protected ?int $mimetype = null;
-
protected ?int $mtime = null;
-
protected ?int $size = null;
-
- protected ?bool $isMax = null;
-
- protected ?bool $crop = null;
-
+ protected ?bool $max = null;
+ protected ?bool $cropped = null;
protected ?string $etag = null;
-
protected ?int $version = null;
+ protected ?bool $encrypted = null;
public function __construct() {
$this->addType('fileId', Types::BIGINT);
+ $this->addType('storageId', Types::BIGINT);
$this->addType('oldFileId', Types::BIGINT);
$this->addType('locationId', Types::BIGINT);
$this->addType('width', Types::INTEGER);
@@ -80,18 +79,19 @@ class Preview extends Entity {
$this->addType('mimetype', Types::INTEGER);
$this->addType('mtime', Types::INTEGER);
$this->addType('size', Types::INTEGER);
- $this->addType('isMax', Types::BOOLEAN);
- $this->addType('crop', Types::BOOLEAN);
+ $this->addType('max', Types::BOOLEAN);
+ $this->addType('cropped', Types::BOOLEAN);
+ $this->addType('encrypted', Types::BOOLEAN);
$this->addType('etag', Types::STRING);
$this->addType('version', Types::BIGINT);
}
public function getName(): string {
$path = ($this->getVersion() > -1 ? $this->getVersion() . '-' : '') . $this->getWidth() . '-' . $this->getHeight();
- if ($this->getCrop()) {
+ if ($this->isCropped()) {
$path .= '-crop';
}
- if ($this->getIsMax()) {
+ if ($this->isMax()) {
$path .= '-max';
}
diff --git a/lib/private/Preview/Db/PreviewMapper.php b/lib/private/Preview/Db/PreviewMapper.php
index dba62c5a163..ec3a0fa7e1f 100644
--- a/lib/private/Preview/Db/PreviewMapper.php
+++ b/lib/private/Preview/Db/PreviewMapper.php
@@ -9,7 +9,6 @@ declare(strict_types=1);
namespace OC\Preview\Db;
-use OC\Preview\Generator;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
@@ -123,4 +122,21 @@ class PreviewMapper extends QBMapper {
return $qb->getLastInsertId();
}
}
+
+ public function deleteAll(): void {
+ $delete = $this->db->getQueryBuilder();
+ $delete->delete($this->getTableName());
+ }
+
+ /**
+ * @return \Generator
+ */
+ public function getPreviews(int $lastId, int $limit = 1000): \Generator {
+ $qb = $this->db->getQueryBuilder();
+ $this->joinLocation($qb)
+ ->where($qb->expr()->gt('p.id', $qb->createNamedParameter($lastId, IQueryBuilder::PARAM_INT)))
+ ->setMaxResults($limit);
+ return $this->yieldEntities($qb);
+
+ }
}
diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php
index 6d3626bb2fc..c6e49a1c6ce 100644
--- a/lib/private/Preview/Generator.php
+++ b/lib/private/Preview/Generator.php
@@ -158,7 +158,7 @@ class Generator {
try {
$preview = array_find($previews, fn (Preview $preview): bool => $preview->getWidth() === $width
&& $preview->getHeight() === $height && $preview->getMimetype() === $maxPreview->getMimetype()
- && $preview->getVersion() === $previewVersion && $preview->getCrop() === $crop);
+ && $preview->getVersion() === $previewVersion && $preview->isCropped() === $crop);
if ($preview) {
$previewFile = new PreviewFile($preview, $this->storageFactory, $this->previewMapper);
@@ -300,7 +300,7 @@ class Generator {
// We don't know the max preview size, so we can't use getCachedPreview.
// It might have been generated with a higher resolution than the current value.
foreach ($previews as $preview) {
- if ($preview->getIsMax() && ($version == $preview->getVersion())) {
+ if ($preview->isMax() && ($version === $preview->getVersion())) {
return $preview;
}
}
@@ -539,11 +539,13 @@ class Generator {
public function savePreview(File $file, int $width, int $height, bool $crop, bool $max, IImage $preview, int $version): Preview {
$previewEntry = new Preview();
$previewEntry->setFileId($file->getId());
+ $previewEntry->setStorageId($file->getMountPoint()->getNumericStorageId());
$previewEntry->setWidth($width);
$previewEntry->setHeight($height);
$previewEntry->setVersion($version);
- $previewEntry->setIsMax($max);
- $previewEntry->setCrop($crop);
+ $previewEntry->setMax($max);
+ $previewEntry->setCropped($crop);
+ $previewEntry->setEncrypted(false);
switch ($preview->dataMimeType()) {
case 'image/jpeg':
$previewEntry->setMimetype(IPreview::MIMETYPE_JPEG);
diff --git a/lib/private/Preview/PreviewService.php b/lib/private/Preview/PreviewService.php
new file mode 100644
index 00000000000..67d6b011416
--- /dev/null
+++ b/lib/private/Preview/PreviewService.php
@@ -0,0 +1,101 @@
+storageFactory->deletePreview($preview);
+ $this->previewMapper->delete($preview);
+ }
+
+ /**
+ * Get storageId and fileIds for which we have at least one preview.
+ *
+ * @return \Generator
+ */
+ public function getAvailableFileIds(): \Generator {
+ $maxQb = $this->connection->getQueryBuilder();
+ $maxQb->select($maxQb->func()->max('id'))
+ ->from($this->previewMapper->getTableName())
+ ->groupBy('file_id');
+
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('file_id', 'storage_id')
+ ->from($this->previewMapper->getTableName())
+ ->where($qb->expr()->in('id', $qb->createFunction($maxQb->getSQL())));
+
+ $result = $qb->executeQuery();
+
+ $lastStorageId = -1;
+ /** @var int[] $fileIds */
+ $fileIds = [];
+
+ // Previews next to each others in the database are likely in the same storage, so group them
+ while ($row = $result->fetch()) {
+ if ($lastStorageId !== $row['storage_id']) {
+ if ($lastStorageId !== -1) {
+ yield ['storageId' => $lastStorageId, 'fileIds' => $fileIds];
+ $fileIds = [];
+ }
+ $lastStorageId = (int)$row['storage_id'];
+ }
+ $fileIds[] = (int)$row['file_id'];
+ }
+
+ if (count($fileIds) > 0) {
+ yield ['storageId' => $lastStorageId, 'fileIds' => $fileIds];
+ }
+ }
+
+ /**
+ * @return \Generator
+ */
+ public function getAvailablePreviewForFile(int $fileId): \Generator {
+ yield from $this->previewMapper->getAvailablePreviewForFile($fileId);
+ }
+
+ public function deleteAll(): void {
+ $lastId = 0;
+ while (true) {
+ $previews = $this->previewMapper->getPreviews($lastId, 1000);
+ $i = 0;
+ foreach ($previews as $preview) {
+ $this->deletePreview($preview);
+ $i++;
+ $lastId = $preview->getId();
+ }
+
+ if ($i !== 1000) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * @param int[] $fileIds
+ * @return array
+ */
+ public function getAvailablePreviews(array $fileIds): array {
+ return $this->previewMapper->getAvailablePreviews($fileIds);
+ }
+}
diff --git a/lib/private/Preview/Storage/IPreviewStorage.php b/lib/private/Preview/Storage/IPreviewStorage.php
index 787518e53a4..933fc2850b0 100644
--- a/lib/private/Preview/Storage/IPreviewStorage.php
+++ b/lib/private/Preview/Storage/IPreviewStorage.php
@@ -1,5 +1,13 @@
rootFolder = $this->config->getSystemValue('datadirectory', OC::$SERVERROOT . '/data');
}
- public function writePreview(Preview $preview, $stream): false|int {
+ public function writePreview(Preview $preview, mixed $stream): false|int {
$previewPath = $this->constructPath($preview);
if (!$this->createParentFiles($previewPath)) {
return false;
@@ -35,11 +35,11 @@ class LocalPreviewStorage implements IPreviewStorage {
return file_put_contents($previewPath, $stream);
}
- public function readPreview(Preview $preview) {
+ public function readPreview(Preview $preview): mixed {
return @fopen($this->constructPath($preview), 'r');
}
- public function deletePreview(Preview $preview) {
+ public function deletePreview(Preview $preview): void {
@unlink($this->constructPath($preview));
}
diff --git a/lib/private/Preview/Storage/ObjectStorePreviewStorage.php b/lib/private/Preview/Storage/ObjectStorePreviewStorage.php
index 7963c870dd8..0a735e5a912 100644
--- a/lib/private/Preview/Storage/ObjectStorePreviewStorage.php
+++ b/lib/private/Preview/Storage/ObjectStorePreviewStorage.php
@@ -15,7 +15,6 @@ use OC\Files\ObjectStore\PrimaryObjectStoreConfig;
use OC\Files\SimpleFS\SimpleFile;
use OC\Preview\Db\Preview;
use OC\Preview\Db\PreviewMapper;
-use OCP\Files\NotFoundException;
use OCP\Files\ObjectStore\IObjectStore;
use OCP\IConfig;
@@ -26,7 +25,7 @@ use OCP\IConfig;
class ObjectStorePreviewStorage implements IPreviewStorage {
/**
- * @var array>
+ * @var array>
*/
private array $objectStoreCache = [];
@@ -40,7 +39,7 @@ class ObjectStorePreviewStorage implements IPreviewStorage {
$this->isMultibucketPreviewDistributionEnabled = $config->getSystemValueBool('objectstore.multibucket.preview-distribution');
}
- public function writePreview(Preview $preview, $stream): false|int {
+ public function writePreview(Preview $preview, mixed $stream): false|int {
if (!is_resource($stream)) {
$fh = fopen('php://temp', 'w+');
fwrite($fh, $stream);
@@ -64,7 +63,7 @@ class ObjectStorePreviewStorage implements IPreviewStorage {
return $size;
}
- public function readPreview(Preview $preview) {
+ public function readPreview(Preview $preview): mixed {
[
'objectPrefix' => $objectPrefix,
'store' => $store,
@@ -72,12 +71,12 @@ class ObjectStorePreviewStorage implements IPreviewStorage {
return $store->readObject($this->constructUrn($objectPrefix, $preview->getId()));
}
- public function deletePreview(Preview $preview) {
+ public function deletePreview(Preview $preview): void {
[
'objectPrefix' => $objectPrefix,
'store' => $store,
] = $this->getObjectStoreForPreview($preview);
- return $store->deleteObject($this->constructUrn($objectPrefix, $preview->getId()));
+ $store->deleteObject($this->constructUrn($objectPrefix, $preview->getId()));
}
public function migratePreview(Preview $preview, SimpleFile $file): void {
diff --git a/lib/private/Preview/Storage/StorageFactory.php b/lib/private/Preview/Storage/StorageFactory.php
index 0755bacd0b9..3051ad5869f 100644
--- a/lib/private/Preview/Storage/StorageFactory.php
+++ b/lib/private/Preview/Storage/StorageFactory.php
@@ -1,5 +1,13 @@
getBackend()->writePreview($preview, $stream);
}
- public function readPreview(Preview $preview) {
+ public function readPreview(Preview $preview): mixed {
return $this->getBackend()->readPreview($preview);
}
diff --git a/lib/private/Preview/Watcher.php b/lib/private/Preview/Watcher.php
index bbbbcb835fa..ea0f72796ae 100644
--- a/lib/private/Preview/Watcher.php
+++ b/lib/private/Preview/Watcher.php
@@ -41,11 +41,12 @@ class Watcher {
return;
}
- if (is_null($node->getId())) {
+ $nodeId = $node->getId();
+ if (is_null($nodeId)) {
return;
}
- [$node->getId() => $previews] = $this->previewMapper->getAvailablePreviews([$node->getId()]);
+ [$node->getId() => $previews] = $this->previewMapper->getAvailablePreviews([$nodeId]);
foreach ($previews as $preview) {
$this->storageFactory->deletePreview($preview);
}
diff --git a/lib/private/PreviewManager.php b/lib/private/PreviewManager.php
index 912d2b3fe5b..fb884092872 100644
--- a/lib/private/PreviewManager.php
+++ b/lib/private/PreviewManager.php
@@ -16,7 +16,6 @@ use OC\Preview\Storage\StorageFactory;
use OCP\AppFramework\QueryException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
-use OCP\Files\IAppData;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
diff --git a/psalm.xml b/psalm.xml
index 648b250c38b..abea7c5a443 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -92,6 +92,7 @@
+
diff --git a/tests/Core/Command/Preview/RepairTest.php b/tests/Core/Command/Preview/RepairTest.php
deleted file mode 100644
index 9b9cde6dd95..00000000000
--- a/tests/Core/Command/Preview/RepairTest.php
+++ /dev/null
@@ -1,153 +0,0 @@
-config = $this->getMockBuilder(IConfig::class)
- ->getMock();
- $this->rootFolder = $this->getMockBuilder(IRootFolder::class)
- ->getMock();
- $this->logger = $this->getMockBuilder(LoggerInterface::class)
- ->getMock();
- $this->iniGetWrapper = $this->getMockBuilder(IniGetWrapper::class)
- ->getMock();
- $this->repair = new Repair(
- $this->config,
- $this->rootFolder,
- $this->logger,
- $this->iniGetWrapper,
- $this->createMock(ILockingProvider::class)
- );
- $this->input = $this->createMock(InputInterface::class);
- $this->input->expects($this->any())
- ->method('getOption')
- ->willReturnCallback(function ($parameter) {
- if ($parameter === 'batch') {
- return true;
- }
- return null;
- });
- $this->output = $this->getMockBuilder(ConsoleOutput::class)
- ->onlyMethods(['section', 'writeln', 'getFormatter'])
- ->getMock();
- $self = $this;
-
- /* We need format method to return a string */
- $outputFormatter = $this->createMock(OutputFormatterInterface::class);
- $outputFormatter->method('isDecorated')->willReturn(false);
- $outputFormatter->method('format')->willReturnArgument(0);
-
- $this->output->expects($this->any())
- ->method('getFormatter')
- ->willReturn($outputFormatter);
- $this->output->expects($this->any())
- ->method('writeln')
- ->willReturnCallback(function ($line) use ($self): void {
- $self->outputLines .= $line . "\n";
- });
- }
-
- public static function dataEmptyTest(): array {
- /** directoryNames, expectedOutput */
- return [
- [
- [],
- 'All previews are already migrated.'
- ],
- [
- [['name' => 'a'], ['name' => 'b'], ['name' => 'c']],
- 'All previews are already migrated.'
- ],
- [
- [['name' => '0', 'content' => ['folder', 'folder']], ['name' => 'b'], ['name' => 'c']],
- 'All previews are already migrated.'
- ],
- [
- [['name' => '0', 'content' => ['file', 'folder', 'folder']], ['name' => 'b'], ['name' => 'c']],
- 'A total of 1 preview files need to be migrated.'
- ],
- [
- [['name' => '23'], ['name' => 'b'], ['name' => 'c']],
- 'A total of 1 preview files need to be migrated.'
- ],
- ];
- }
-
- #[\PHPUnit\Framework\Attributes\DataProvider('dataEmptyTest')]
- public function testEmptyExecute($directoryNames, $expectedOutput): void {
- $previewFolder = $this->getMockBuilder(Folder::class)
- ->getMock();
- $directories = array_map(function ($element) {
- $dir = $this->getMockBuilder(Folder::class)
- ->getMock();
- $dir->expects($this->any())
- ->method('getName')
- ->willReturn($element['name']);
- if (isset($element['content'])) {
- $list = [];
- foreach ($element['content'] as $item) {
- if ($item === 'file') {
- $list[] = $this->getMockBuilder(Node::class)
- ->getMock();
- } elseif ($item === 'folder') {
- $list[] = $this->getMockBuilder(Folder::class)
- ->getMock();
- }
- }
- $dir->expects($this->once())
- ->method('getDirectoryListing')
- ->willReturn($list);
- }
- return $dir;
- }, $directoryNames);
- $previewFolder->expects($this->once())
- ->method('getDirectoryListing')
- ->willReturn($directories);
- $this->rootFolder->expects($this->once())
- ->method('get')
- ->with('appdata_/preview')
- ->willReturn($previewFolder);
-
- $this->repair->run($this->input, $this->output);
-
- $this->assertStringContainsString($expectedOutput, $this->outputLines);
- }
-}
diff --git a/tests/lib/Files/ObjectStore/PrimaryObjectStoreConfigTest.php b/tests/lib/Files/ObjectStore/PrimaryObjectStoreConfigTest.php
index c13b170ca18..07e58d56f11 100644
--- a/tests/lib/Files/ObjectStore/PrimaryObjectStoreConfigTest.php
+++ b/tests/lib/Files/ObjectStore/PrimaryObjectStoreConfigTest.php
@@ -212,6 +212,7 @@ class PrimaryObjectStoreConfigTest extends TestCase {
$this->assertEquals([
'default' => 'server1',
'root' => 'server1',
+ 'preview' => 'server1',
'server1' => [
'class' => StorageObjectStore::class,
'arguments' => [
@@ -235,6 +236,7 @@ class PrimaryObjectStoreConfigTest extends TestCase {
$this->assertEquals([
'default' => 'server1',
'root' => 'server1',
+ 'preview' => 'server1',
'server1' => [
'class' => StorageObjectStore::class,
'arguments' => [
@@ -270,6 +272,7 @@ class PrimaryObjectStoreConfigTest extends TestCase {
$this->setConfig('objectstore', [
'default' => 'server1',
'root' => 'server2',
+ 'preview' => 'server1',
'server1' => [
'class' => StorageObjectStore::class,
'arguments' => [
diff --git a/tests/lib/Preview/BackgroundCleanupJobTest.php b/tests/lib/Preview/BackgroundCleanupJobTest.php
index ea08b58955d..a2c72cbad57 100644
--- a/tests/lib/Preview/BackgroundCleanupJobTest.php
+++ b/tests/lib/Preview/BackgroundCleanupJobTest.php
@@ -9,19 +9,13 @@ namespace Test\Preview;
use OC\Files\Storage\Temporary;
use OC\Preview\BackgroundCleanupJob;
-use OC\Preview\Db\Preview;
-use OC\Preview\Db\PreviewMapper;
-use OC\Preview\Storage\Root;
-use OC\Preview\Storage\StorageFactory;
+use OC\Preview\PreviewService;
use OC\PreviewManager;
-use OC\SystemConfig;
use OCP\App\IAppManager;
use OCP\AppFramework\Utility\ITimeFactory;
-use OCP\Files\AppData\IAppDataFactory;
use OCP\Files\File;
use OCP\Files\IMimeTypeLoader;
use OCP\Files\IRootFolder;
-use OCP\Files\NotFoundException;
use OCP\IDBConnection;
use OCP\IPreview;
use OCP\Server;
@@ -45,8 +39,7 @@ class BackgroundCleanupJobTest extends \Test\TestCase {
private IRootFolder $rootFolder;
private IMimeTypeLoader $mimeTypeLoader;
private ITimeFactory $timeFactory;
- private PreviewMapper $previewMapper;
- private StorageFactory $previewStorageFactory;
+ private PreviewService $previewService;
protected function setUp(): void {
parent::setUp();
@@ -70,8 +63,7 @@ class BackgroundCleanupJobTest extends \Test\TestCase {
$this->rootFolder = Server::get(IRootFolder::class);
$this->mimeTypeLoader = Server::get(IMimeTypeLoader::class);
$this->timeFactory = Server::get(ITimeFactory::class);
- $this->previewMapper = Server::get(PreviewMapper::class);
- $this->previewStorageFactory = Server::get(StorageFactory::class);
+ $this->previewService = Server::get(PreviewService::class);
}
protected function tearDown(): void {
@@ -82,9 +74,8 @@ class BackgroundCleanupJobTest extends \Test\TestCase {
$this->logout();
- foreach ($this->previewMapper->getAvailablePreviews(5) as $preview) {
- $this->previewStorageFactory->deletePreview($preview);
- $this->previewMapper->delete($preview);
+ foreach ($this->previewService->getAvailablePreviewForFile(5) as $preview) {
+ $this->previewService->deletePreview($preview);
}
parent::tearDown();
@@ -104,8 +95,8 @@ class BackgroundCleanupJobTest extends \Test\TestCase {
return $files;
}
- private function countPreviews(PreviewMapper $previewMapper, array $fileIds): int {
- $previews = $previewMapper->getAvailablePreviews($fileIds);
+ private function countPreviews(PreviewService $previewService, array $fileIds): int {
+ $previews = $previewService->getAvailablePreviews($fileIds);
return array_reduce($previews, fn (int $result, array $previews) => $result + count($previews), 0);
}
@@ -113,18 +104,18 @@ class BackgroundCleanupJobTest extends \Test\TestCase {
$files = $this->setup11Previews();
$fileIds = array_map(fn (File $f): int => $f->getId(), $files);
- $this->assertSame(11, $this->countPreviews($this->previewMapper, $fileIds));
- $job = new BackgroundCleanupJob($this->timeFactory, $this->connection, $this->previewMapper, $this->previewStorageFactory, true);
+ $this->assertSame(11, $this->countPreviews($this->previewService, $fileIds));
+ $job = new BackgroundCleanupJob($this->timeFactory, $this->connection, $this->previewService, true);
$job->run([]);
foreach ($files as $file) {
$file->delete();
}
- $this->assertSame(11, $this->countPreviews($this->previewMapper, $fileIds));
+ $this->assertSame(11, $this->countPreviews($this->previewService, $fileIds));
$job->run([]);
- $this->assertSame(0, $this->countPreviews($this->previewMapper, $fileIds));
+ $this->assertSame(0, $this->countPreviews($this->previewService, $fileIds));
}
public function testCleanupAjax(): void {
@@ -134,20 +125,20 @@ class BackgroundCleanupJobTest extends \Test\TestCase {
$files = $this->setup11Previews();
$fileIds = array_map(fn (File $f): int => $f->getId(), $files);
- $this->assertSame(11, $this->countPreviews($this->previewMapper, $fileIds));
- $job = new BackgroundCleanupJob($this->timeFactory, $this->connection, $this->previewMapper, $this->previewStorageFactory, false);
+ $this->assertSame(11, $this->countPreviews($this->previewService, $fileIds));
+ $job = new BackgroundCleanupJob($this->timeFactory, $this->connection, $this->previewService, false);
$job->run([]);
foreach ($files as $file) {
$file->delete();
}
- $this->assertSame(11, $this->countPreviews($this->previewMapper, $fileIds));
+ $this->assertSame(11, $this->countPreviews($this->previewService, $fileIds));
$job->run([]);
- $this->assertSame(1, $this->countPreviews($this->previewMapper, $fileIds));
+ $this->assertSame(1, $this->countPreviews($this->previewService, $fileIds));
$job->run([]);
- $this->assertSame(0, $this->countPreviews($this->previewMapper, $fileIds));
+ $this->assertSame(0, $this->countPreviews($this->previewService, $fileIds));
}
}
diff --git a/tests/lib/Preview/GeneratorTest.php b/tests/lib/Preview/GeneratorTest.php
index 91fa3537468..54d28747cf3 100644
--- a/tests/lib/Preview/GeneratorTest.php
+++ b/tests/lib/Preview/GeneratorTest.php
@@ -16,8 +16,6 @@ use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\NotFoundException;
-use OCP\Files\SimpleFS\ISimpleFile;
-use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\IConfig;
use OCP\IImage;
use OCP\IPreview;
@@ -84,19 +82,21 @@ class GeneratorTest extends TestCase {
$maxPreview = new Preview();
$maxPreview->setWidth(1000);
$maxPreview->setHeight(1000);
- $maxPreview->setIsMax(true);
+ $maxPreview->setMax(true);
$maxPreview->setSize(1000);
$maxPreview->setVersion(-1);
- $maxPreview->setCrop(false);
+ $maxPreview->setCropped(false);
+ $maxPreview->setStorageId(1);
$maxPreview->setMimetype(IPreview::MIMETYPE_PNG);
$previewFile = new Preview();
$previewFile->setWidth(256);
$previewFile->setHeight(256);
- $previewFile->setIsMax(false);
+ $previewFile->setMax(false);
$previewFile->setSize(1000);
$previewFile->setVersion(-1);
- $previewFile->setCrop(false);
+ $previewFile->setCropped(false);
+ $previewFile->setStorageId(1);
$previewFile->setMimetype(IPreview::MIMETYPE_PNG);
$this->previewMapper->method('getAvailablePreviews')
@@ -190,7 +190,7 @@ class GeneratorTest extends TestCase {
$maxPreview = new Preview();
$maxPreview->setWidth(2048);
$maxPreview->setHeight(2048);
- $maxPreview->setIsMax(true);
+ $maxPreview->setMax(true);
$maxPreview->setSize(1000);
$maxPreview->setMimetype(IPreview::MIMETYPE_PNG);
@@ -210,7 +210,7 @@ class GeneratorTest extends TestCase {
$this->assertSame('my resized data', $data);
return 1000;
}
- $this->fail("file name is wrong:". $preview->getName());
+ $this->fail('file name is wrong:' . $preview->getName());
});
$image = $this->getMockImage(2048, 2048, 'my resized data');
@@ -238,7 +238,7 @@ class GeneratorTest extends TestCase {
$maxPreview = new Preview();
$maxPreview->setWidth(2048);
$maxPreview->setHeight(2048);
- $maxPreview->setIsMax(true);
+ $maxPreview->setMax(true);
$maxPreview->setSize(1000);
$maxPreview->setVersion(-1);
$maxPreview->setMimetype(IPreview::MIMETYPE_PNG);
@@ -262,7 +262,7 @@ class GeneratorTest extends TestCase {
$maxPreview = new Preview();
$maxPreview->setWidth(2048);
$maxPreview->setHeight(2048);
- $maxPreview->setIsMax(true);
+ $maxPreview->setMax(true);
$maxPreview->setSize(1000);
$maxPreview->setVersion(-1);
$maxPreview->setMimetype(IPreview::MIMETYPE_PNG);
@@ -270,9 +270,9 @@ class GeneratorTest extends TestCase {
$previewFile = new Preview();
$previewFile->setWidth(1024);
$previewFile->setHeight(512);
- $previewFile->setIsMax(false);
+ $previewFile->setMax(false);
$previewFile->setSize(1000);
- $previewFile->setCrop(true);
+ $previewFile->setCropped(true);
$previewFile->setVersion(-1);
$previewFile->setMimetype(IPreview::MIMETYPE_PNG);
@@ -380,7 +380,7 @@ class GeneratorTest extends TestCase {
$maxPreview = new Preview();
$maxPreview->setWidth($maxX);
$maxPreview->setHeight($maxY);
- $maxPreview->setIsMax(true);
+ $maxPreview->setMax(true);
$maxPreview->setSize(1000);
$maxPreview->setVersion(-1);
$maxPreview->setMimetype(IPreview::MIMETYPE_PNG);
diff --git a/tests/lib/Preview/MovePreviewJobTest.php b/tests/lib/Preview/MovePreviewJobTest.php
index a226b611c5a..cd9f3253654 100644
--- a/tests/lib/Preview/MovePreviewJobTest.php
+++ b/tests/lib/Preview/MovePreviewJobTest.php
@@ -1,17 +1,26 @@
appConfig->expects($this->any())
->method('getValueBool')
->willReturn(false);
+ $this->appConfig->expects($this->any())
+ ->method('setValueBool')
+ ->willReturn(true);
$this->storageFactory = Server::get(StorageFactory::class);
+ $this->previewService = Server::get(PreviewService::class);
+ $this->db = Server::get(IDBConnection::class);
+
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete('filecache')
+ ->where($qb->expr()->eq('fileid', $qb->createNamedParameter(5)))
+ ->executeStatement();
+
+ $qb = $this->db->getQueryBuilder();
+ $qb->insert('filecache')
+ ->values([
+ 'fileid' => $qb->createNamedParameter(5),
+ 'storage' => $qb->createNamedParameter(1),
+ 'path' => $qb->createNamedParameter('test/abc'),
+ 'path_hash' => $qb->createNamedParameter(md5('test')),
+ 'parent' => $qb->createNamedParameter(0),
+ 'name' => $qb->createNamedParameter('abc'),
+ 'mimetype' => $qb->createNamedParameter(0),
+ 'size' => $qb->createNamedParameter(1000),
+ 'mtime' => $qb->createNamedParameter(1000),
+ 'storage_mtime' => $qb->createNamedParameter(1000),
+ 'encrypted' => $qb->createNamedParameter(0),
+ 'unencrypted_size' => $qb->createNamedParameter(0),
+ 'etag' => $qb->createNamedParameter('abcdefg'),
+ 'permissions' => $qb->createNamedParameter(0),
+ 'checksum' => $qb->createNamedParameter('abcdefg'),
+ ])->executeStatement();
}
public function tearDown(): void {
- foreach ($this->previewMapper->getAvailablePreviewForFile(5) as $preview) {
- $this->storageFactory->deletePreview($preview);
- $this->previewMapper->delete($preview);
- }
-
foreach ($this->previewAppData->getDirectoryListing() as $folder) {
$folder->delete();
}
+ $this->previewService->deleteAll();
+
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete('filecache')
+ ->where($qb->expr()->eq('fileid', $qb->createNamedParameter(5)))
+ ->executeStatement();
}
- #[TestDox("Test the migration from the legacy flat hierarchy to the new database format")]
- function testMigrationLegacyPath(): void {
- $folder = $this->previewAppData->newFolder(5);
+ #[TestDox('Test the migration from the legacy flat hierarchy to the new database format')]
+ public function testMigrationLegacyPath(): void {
+ $folder = $this->previewAppData->newFolder('5');
$folder->newFile('64-64-crop.jpg', 'abcdefg');
$folder->newFile('128-128-crop.png', 'abcdefg');
$this->assertEquals(1, count($this->previewAppData->getDirectoryListing()));
@@ -63,6 +104,7 @@ class MovePreviewJobTest extends TestCase {
$this->previewMapper,
$this->storageFactory,
Server::get(IDBConnection::class),
+ Server::get(IRootFolder::class),
Server::get(IAppDataFactory::class)
);
$this->invokePrivate($job, 'run', [[]]);
@@ -75,12 +117,12 @@ class MovePreviewJobTest extends TestCase {
}
#[TestDox("Test the migration from the 'new' nested hierarchy to the database format")]
- function testMigrationPath(): void {
- $folder = $this->previewAppData->newFolder(self::getInternalFolder(5));
+ public function testMigrationPath(): void {
+ $folder = $this->previewAppData->newFolder(self::getInternalFolder((string)5));
$folder->newFile('64-64-crop.jpg', 'abcdefg');
$folder->newFile('128-128-crop.png', 'abcdefg');
- $folder = $this->previewAppData->getFolder(self::getInternalFolder(5));
+ $folder = $this->previewAppData->getFolder(self::getInternalFolder((string)5));
$this->assertEquals(2, count($folder->getDirectoryListing()));
$this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewForFile(5))));
@@ -90,10 +132,65 @@ class MovePreviewJobTest extends TestCase {
$this->previewMapper,
$this->storageFactory,
Server::get(IDBConnection::class),
+ Server::get(IRootFolder::class),
Server::get(IAppDataFactory::class)
);
$this->invokePrivate($job, 'run', [[]]);
$this->assertEquals(0, count($this->previewAppData->getDirectoryListing()));
$this->assertEquals(2, count(iterator_to_array($this->previewMapper->getAvailablePreviewForFile(5))));
}
+
+ #[TestDox("Test the migration from the 'new' nested hierarchy to the database format")]
+ public function testMigrationPathWithVersion(): void {
+ $folder = $this->previewAppData->newFolder(self::getInternalFolder((string)5));
+ // No version
+ $folder->newFile('128-128-crop.png', 'abcdefg');
+ $folder->newFile('256-256-max.png', 'abcdefg');
+ $folder->newFile('128-128.png', 'abcdefg');
+
+ // Version 1000
+ $folder->newFile('1000-128-128-crop.png', 'abcdefg');
+ $folder->newFile('1000-256-256-max.png', 'abcdefg');
+ $folder->newFile('1000-128-128.png', 'abcdefg');
+
+ // Version 1001
+ $folder->newFile('1001-128-128-crop.png', 'abcdefg');
+ $folder->newFile('1001-256-256-max.png', 'abcdefg');
+ $folder->newFile('1001-128-128.png', 'abcdefg');
+
+ $folder = $this->previewAppData->getFolder(self::getInternalFolder((string)5));
+ $this->assertEquals(9, count($folder->getDirectoryListing()));
+ $this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewForFile(5))));
+
+ $job = new MovePreviewJob(
+ Server::get(ITimeFactory::class),
+ $this->appConfig,
+ $this->previewMapper,
+ $this->storageFactory,
+ Server::get(IDBConnection::class),
+ Server::get(IRootFolder::class),
+ Server::get(IAppDataFactory::class)
+ );
+ $this->invokePrivate($job, 'run', [[]]);
+ $this->assertEquals(0, count($this->previewAppData->getDirectoryListing()));
+ $previews = iterator_to_array($this->previewMapper->getAvailablePreviewForFile(5));
+ $this->assertEquals(9, count($previews));
+
+ $nameVersionMapping = [];
+ foreach ($previews as $preview) {
+ $nameVersionMapping[$preview->getName()] = $preview->getVersion();
+ }
+
+ $this->assertEquals([
+ '1000-128-128.png' => 1000,
+ '1000-128-128-crop.png' => 1000,
+ '1000-256-256-max.png' => 1000,
+ '1001-128-128.png' => 1001,
+ '1001-128-128-crop.png' => 1001,
+ '1001-256-256-max.png' => 1001,
+ '128-128.png' => -1,
+ '128-128-crop.png' => -1,
+ '256-256-max.png' => -1,
+ ], $nameVersionMapping);
+ }
}
diff --git a/tests/lib/Preview/PreviewMapperTest.php b/tests/lib/Preview/PreviewMapperTest.php
index d6925641d4e..6f59c6f8802 100644
--- a/tests/lib/Preview/PreviewMapperTest.php
+++ b/tests/lib/Preview/PreviewMapperTest.php
@@ -29,7 +29,7 @@ class PreviewMapperTest extends TestCase {
$this->connection = Server::get(IDBConnection::class);
}
- public function testGetAvailablePreviews() {
+ public function testGetAvailablePreviews(): void {
// Empty
$this->assertEquals([], $this->previewMapper->getAvailablePreviews([]));
@@ -50,7 +50,8 @@ class PreviewMapperTest extends TestCase {
$this->assertEquals('default', $previews[43][0]->getObjectStoreName());
}
- private function createPreviewForFileId(int $fileId, ?int $bucket = null) {
+ private function createPreviewForFileId(int $fileId, ?int $bucket = null): void {
+ $locationId = null;
if ($bucket) {
$qb = $this->connection->getQueryBuilder();
$qb->insert('preview_locations')
@@ -58,20 +59,22 @@ class PreviewMapperTest extends TestCase {
'bucket_name' => $qb->createNamedParameter('preview-' . $bucket),
'object_store_name' => $qb->createNamedParameter('default'),
]);
- $locationId = $qb->executeStatement();
+ $qb->executeStatement();
+ $locationId = $qb->getLastInsertId();
}
$preview = new Preview();
$preview->setFileId($fileId);
- $preview->setCrop(true);
- $preview->setIsMax(true);
+ $preview->setStorageId(1);
+ $preview->setCropped(true);
+ $preview->setMax(true);
$preview->setWidth(100);
$preview->setHeight(100);
$preview->setSize(100);
$preview->setMtime(time());
$preview->setMimetype(IPreview::MIMETYPE_PNG);
- $preview->setEtag("abcdefg");
+ $preview->setEtag('abcdefg');
- if ($locationId) {
+ if ($locationId !== null) {
$preview->setLocationId($locationId);
}
$this->previewMapper->insert($preview);
diff --git a/tests/lib/Preview/PreviewServiceTest.php b/tests/lib/Preview/PreviewServiceTest.php
new file mode 100644
index 00000000000..01bcff29a5a
--- /dev/null
+++ b/tests/lib/Preview/PreviewServiceTest.php
@@ -0,0 +1,59 @@
+previewService = Server::get(PreviewService::class);
+ $this->previewMapper = Server::get(PreviewMapper::class);
+ $this->previewService->deleteAll();
+ }
+
+ public function tearDown(): void {
+ $this->previewService->deleteAll();
+ }
+
+ public function testGetAvailableFileIds(): void {
+ foreach (range(1, 20) as $i) {
+ $preview = new Preview();
+ $preview->setFileId($i % 10);
+ $preview->setStorageId(1);
+ $preview->setWidth($i);
+ $preview->setHeight($i);
+ $preview->setMax(true);
+ $preview->setCropped(true);
+ $preview->setEncrypted(false);
+ $preview->setMimetype(IPreview::MIMETYPE_JPEG);
+ $preview->setEtag('abc');
+ $preview->setMtime((new \DateTime())->getTimestamp());
+ $preview->setSize(0);
+ $this->previewMapper->insert($preview);
+ }
+
+ $files = iterator_to_array($this->previewService->getAvailableFileIds());
+ $this->assertCount(1, $files);
+ $this->assertCount(10, $files[0]['fileIds']);
+ }
+}