mirror of
https://github.com/nextcloud/server.git
synced 2026-02-18 18:28:50 -05:00
refactor(preview): Cleanup the implementation of the new preview backend
Signed-off-by: Carl Schwan <carl.schwan@nextcloud.com>
This commit is contained in:
parent
6f56dcf73e
commit
324b54b863
29 changed files with 565 additions and 788 deletions
|
|
@ -2708,17 +2708,6 @@
|
|||
<code><![CDATA[$this->timeFactory->getTime()]]></code>
|
||||
</InvalidScalarArgument>
|
||||
</file>
|
||||
<file src="core/Command/Preview/Repair.php">
|
||||
<UndefinedInterfaceMethod>
|
||||
<code><![CDATA[section]]></code>
|
||||
<code><![CDATA[section]]></code>
|
||||
</UndefinedInterfaceMethod>
|
||||
</file>
|
||||
<file src="core/Command/Preview/ResetRenderedTexts.php">
|
||||
<InvalidReturnStatement>
|
||||
<code><![CDATA[[]]]></code>
|
||||
</InvalidReturnStatement>
|
||||
</file>
|
||||
<file src="core/Command/Security/BruteforceAttempts.php">
|
||||
<DeprecatedMethod>
|
||||
<code><![CDATA[getAttempts]]></code>
|
||||
|
|
|
|||
9
build/stubs/php-polyfill.php
Normal file
9
build/stubs/php-polyfill.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
/*
|
||||
* SPDX-FileCopyrightText: None
|
||||
* SPDX-License-Identifier: CC0-1.0
|
||||
*/
|
||||
|
||||
// PHP 8.4
|
||||
function array_find(array $array, callable $callback) {}
|
||||
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
|
||||
* SPDX-FileContributor: Carl Schwan
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
|
|
@ -17,7 +19,8 @@ use OCP\BackgroundJob\TimedJob;
|
|||
use OCP\DB\Exception;
|
||||
use OCP\Files\AppData\IAppDataFactory;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\SimpleFS\ISimpleFile;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\SimpleFS\ISimpleFolder;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IDBConnection;
|
||||
|
|
@ -28,10 +31,11 @@ class MovePreviewJob extends TimedJob {
|
|||
|
||||
public function __construct(
|
||||
ITimeFactory $time,
|
||||
private IAppConfig $appConfig,
|
||||
private PreviewMapper $previewMapper,
|
||||
private StorageFactory $storageFactory,
|
||||
private IDBConnection $connection,
|
||||
private readonly IAppConfig $appConfig,
|
||||
private readonly PreviewMapper $previewMapper,
|
||||
private readonly StorageFactory $storageFactory,
|
||||
private readonly IDBConnection $connection,
|
||||
private readonly IRootFolder $rootFolder,
|
||||
IAppDataFactory $appDataFactory,
|
||||
) {
|
||||
parent::__construct($time);
|
||||
|
|
@ -42,15 +46,6 @@ class MovePreviewJob extends TimedJob {
|
|||
}
|
||||
|
||||
protected function run(mixed $argument): void {
|
||||
try {
|
||||
$this->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<string|int, string[]> $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<array{file: SimpleFile, width: int, height: int, crop: bool, max: bool, extension: string, mtime: int, size: int}> $previewFiles
|
||||
*/
|
||||
$previewFiles = [];
|
||||
/**
|
||||
* @var list<array{
|
||||
* file: SimpleFile, width: int, height: int, crop: bool, max: bool, extension: string, mtime: int, size: int, version: ?int
|
||||
* }> $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 {
|
||||
|
|
|
|||
|
|
@ -1,293 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OC\Core\Command\Preview;
|
||||
|
||||
use bantu\IniGetWrapper\IniGetWrapper;
|
||||
use OC\Preview\Storage\Root;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\IConfig;
|
||||
use OCP\Lock\ILockingProvider;
|
||||
use OCP\Lock\LockedException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
|
||||
use function pcntl_signal;
|
||||
|
||||
class Repair extends Command {
|
||||
private bool $stopSignalReceived = false;
|
||||
private int $memoryLimit;
|
||||
private int $memoryTreshold;
|
||||
|
||||
public function __construct(
|
||||
protected IConfig $config,
|
||||
private IRootFolder $rootFolder,
|
||||
private LoggerInterface $logger,
|
||||
IniGetWrapper $phpIni,
|
||||
private ILockingProvider $lockingProvider,
|
||||
) {
|
||||
$this->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('<info>Should the migration be started? (y/[n]) </info>', 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<array{path: string, preview: Preview}>
|
||||
*/
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<FileId>
|
||||
*/
|
||||
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[]
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Preview>
|
||||
*/
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
101
lib/private/Preview/PreviewService.php
Normal file
101
lib/private/Preview/PreviewService.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
|
||||
* SPDX-FileContributor: Carl Schwan
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OC\Preview;
|
||||
|
||||
use OC\Preview\Db\Preview;
|
||||
use OC\Preview\Db\PreviewMapper;
|
||||
use OC\Preview\Storage\StorageFactory;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
class PreviewService {
|
||||
public function __construct(
|
||||
private readonly StorageFactory $storageFactory,
|
||||
private readonly PreviewMapper $previewMapper,
|
||||
private readonly IDBConnection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
public function deletePreview(Preview $preview): void {
|
||||
$this->storageFactory->deletePreview($preview);
|
||||
$this->previewMapper->delete($preview);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storageId and fileIds for which we have at least one preview.
|
||||
*
|
||||
* @return \Generator<array{storageId: int, fileIds: int[]}>
|
||||
*/
|
||||
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<Preview>
|
||||
*/
|
||||
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<int, Preview[]>
|
||||
*/
|
||||
public function getAvailablePreviews(array $fileIds): array {
|
||||
return $this->previewMapper->getAvailablePreviews($fileIds);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
|
||||
* SPDX-FileContributor: Carl Schwan
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OC\Preview\Storage;
|
||||
|
||||
use OC\Files\SimpleFS\SimpleFile;
|
||||
|
|
@ -11,15 +19,15 @@ interface IPreviewStorage {
|
|||
* @param resource|string $stream
|
||||
* @throws NotPermittedException
|
||||
*/
|
||||
public function writePreview(Preview $preview, $stream): false|int;
|
||||
public function writePreview(Preview $preview, mixed $stream): false|int;
|
||||
|
||||
/**
|
||||
* @param Preview $preview
|
||||
* @return resource|false
|
||||
*/
|
||||
public function readPreview(Preview $preview);
|
||||
public function readPreview(Preview $preview): mixed;
|
||||
|
||||
public function deletePreview(Preview $preview);
|
||||
public function deletePreview(Preview $preview): void;
|
||||
|
||||
/**
|
||||
* Migration helper
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class LocalPreviewStorage implements IPreviewStorage {
|
|||
$this->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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, array<int, ObjectStoreDefinition>>
|
||||
* @var array<string, array<string, ObjectStoreDefinition>>
|
||||
*/
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
|
||||
* SPDX-FileContributor: Carl Schwan
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OC\Preview\Storage;
|
||||
|
||||
use OC\Files\ObjectStore\PrimaryObjectStoreConfig;
|
||||
|
|
@ -18,11 +26,11 @@ class StorageFactory implements IPreviewStorage {
|
|||
) {
|
||||
}
|
||||
|
||||
public function writePreview(Preview $preview, $stream): false|int {
|
||||
public function writePreview(Preview $preview, mixed $stream): false|int {
|
||||
return $this->getBackend()->writePreview($preview, $stream);
|
||||
}
|
||||
|
||||
public function readPreview(Preview $preview) {
|
||||
public function readPreview(Preview $preview): mixed {
|
||||
return $this->getBackend()->readPreview($preview);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@
|
|||
<file name="build/stubs/psr_container.php"/>
|
||||
<file name="3rdparty/sabre/uri/lib/functions.php" />
|
||||
<file name="build/stubs/app_api.php" />
|
||||
<file name="build/stubs/php-polyfill.php" />
|
||||
</stubs>
|
||||
<issueHandlers>
|
||||
<LessSpecificReturnStatement errorLevel="error"/>
|
||||
|
|
|
|||
|
|
@ -1,153 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace Tests\Core\Command\Preview;
|
||||
|
||||
use bantu\IniGetWrapper\IniGetWrapper;
|
||||
use OC\Core\Command\Preview\Repair;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\Node;
|
||||
use OCP\IConfig;
|
||||
use OCP\Lock\ILockingProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
class RepairTest extends TestCase {
|
||||
/** @var IConfig|MockObject */
|
||||
private $config;
|
||||
/** @var IRootFolder|MockObject */
|
||||
private $rootFolder;
|
||||
/** @var LoggerInterface|MockObject */
|
||||
private $logger;
|
||||
/** @var IniGetWrapper|MockObject */
|
||||
private $iniGetWrapper;
|
||||
/** @var InputInterface|MockObject */
|
||||
private $input;
|
||||
/** @var OutputInterface|MockObject */
|
||||
private $output;
|
||||
/** @var string */
|
||||
private $outputLines = '';
|
||||
/** @var Repair */
|
||||
private $repair;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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' => [
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
|
||||
* SPDX-FileContributor: Carl Schwan
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace lib\Preview;
|
||||
|
||||
use OC\Core\BackgroundJobs\MovePreviewJob;
|
||||
use OC\Preview\Db\PreviewMapper;
|
||||
use OC\Preview\PreviewService;
|
||||
use OC\Preview\Storage\StorageFactory;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Files\AppData\IAppDataFactory;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\Server;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
|
@ -19,12 +28,13 @@ use Test\TestCase;
|
|||
/**
|
||||
* @group DB
|
||||
*/
|
||||
#[CoversClass(MovePreviewJob::class)]
|
||||
class MovePreviewJobTest extends TestCase {
|
||||
private IAppData $previewAppData;
|
||||
private PreviewMapper $previewMapper;
|
||||
private IAppConfig&MockObject $appConfig;
|
||||
private StorageFactory $storageFactory;
|
||||
private PreviewService $previewService;
|
||||
private IDBConnection $db;
|
||||
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
|
|
@ -34,23 +44,54 @@ class MovePreviewJobTest extends TestCase {
|
|||
$this->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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
59
tests/lib/Preview/PreviewServiceTest.php
Normal file
59
tests/lib/Preview/PreviewServiceTest.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
|
||||
* SPDX-FileContributor: Carl Schwan
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace Test\Preview;
|
||||
|
||||
use OC\Preview\Db\Preview;
|
||||
use OC\Preview\Db\PreviewMapper;
|
||||
use OC\Preview\PreviewService;
|
||||
use OCP\IPreview;
|
||||
use OCP\Server;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @group DB
|
||||
*/
|
||||
#[CoversClass(PreviewService::class)]
|
||||
class PreviewServiceTest extends TestCase {
|
||||
private PreviewService $previewService;
|
||||
|
||||
protected function setUp(): void {
|
||||
$this->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']);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue