diff options
Diffstat (limited to 'lib/private/Preview')
-rw-r--r-- | lib/private/Preview/BackgroundCleanupJob.php | 105 | ||||
-rw-r--r-- | lib/private/Preview/Bitmap.php | 2 | ||||
-rw-r--r-- | lib/private/Preview/Bundled.php | 1 | ||||
-rw-r--r-- | lib/private/Preview/Generator.php | 118 | ||||
-rw-r--r-- | lib/private/Preview/GeneratorHelper.php | 1 | ||||
-rw-r--r-- | lib/private/Preview/HEIC.php | 9 | ||||
-rw-r--r-- | lib/private/Preview/Image.php | 9 | ||||
-rw-r--r-- | lib/private/Preview/Imaginary.php | 3 | ||||
-rw-r--r-- | lib/private/Preview/ImaginaryPDF.php | 14 | ||||
-rw-r--r-- | lib/private/Preview/Krita.php | 1 | ||||
-rw-r--r-- | lib/private/Preview/MP3.php | 9 | ||||
-rw-r--r-- | lib/private/Preview/MarkDown.php | 2 | ||||
-rw-r--r-- | lib/private/Preview/MimeIconProvider.php | 9 | ||||
-rw-r--r-- | lib/private/Preview/Movie.php | 122 | ||||
-rw-r--r-- | lib/private/Preview/Office.php | 8 | ||||
-rw-r--r-- | lib/private/Preview/ProviderV2.php | 40 | ||||
-rw-r--r-- | lib/private/Preview/SGI.php | 1 | ||||
-rw-r--r-- | lib/private/Preview/SVG.php | 2 | ||||
-rw-r--r-- | lib/private/Preview/TGA.php | 1 | ||||
-rw-r--r-- | lib/private/Preview/TXT.php | 4 | ||||
-rw-r--r-- | lib/private/Preview/Watcher.php | 3 | ||||
-rw-r--r-- | lib/private/Preview/WatcherConnector.php | 32 |
22 files changed, 319 insertions, 177 deletions
diff --git a/lib/private/Preview/BackgroundCleanupJob.php b/lib/private/Preview/BackgroundCleanupJob.php index 49ff01486a3..3138abb1bf9 100644 --- a/lib/private/Preview/BackgroundCleanupJob.php +++ b/lib/private/Preview/BackgroundCleanupJob.php @@ -18,31 +18,18 @@ use OCP\Files\NotPermittedException; use OCP\IDBConnection; class BackgroundCleanupJob extends TimedJob { - /** @var IDBConnection */ - private $connection; - /** @var Root */ - private $previewFolder; - - /** @var bool */ - private $isCLI; - - /** @var IMimeTypeLoader */ - private $mimeTypeLoader; - - public function __construct(ITimeFactory $timeFactory, - IDBConnection $connection, - Root $previewFolder, - IMimeTypeLoader $mimeTypeLoader, - bool $isCLI) { + public function __construct( + ITimeFactory $timeFactory, + private IDBConnection $connection, + private Root $previewFolder, + private IMimeTypeLoader $mimeTypeLoader, + private bool $isCLI, + ) { parent::__construct($timeFactory); // Run at most once an hour - $this->setInterval(3600); - - $this->connection = $connection; - $this->previewFolder = $previewFolder; - $this->isCLI = $isCLI; - $this->mimeTypeLoader = $mimeTypeLoader; + $this->setInterval(60 * 60); + $this->setTimeSensitivity(self::TIME_INSENSITIVE); } public function run($argument) { @@ -64,6 +51,11 @@ class BackgroundCleanupJob extends TimedJob { } private function getOldPreviewLocations(): \Iterator { + if ($this->connection->getShardDefinition('filecache')) { + // sharding is new enough that we don't need to support this + return; + } + $qb = $this->connection->getQueryBuilder(); $qb->select('a.name') ->from('filecache', 'a') @@ -71,18 +63,20 @@ class BackgroundCleanupJob extends TimedJob { $qb->expr()->castColumn('a.name', IQueryBuilder::PARAM_INT), 'b.fileid' )) ->where( - $qb->expr()->isNull('b.fileid') - )->andWhere( - $qb->expr()->eq('a.parent', $qb->createNamedParameter($this->previewFolder->getId())) - )->andWhere( - $qb->expr()->like('a.name', $qb->createNamedParameter('__%')) + $qb->expr()->andX( + $qb->expr()->isNull('b.fileid'), + $qb->expr()->eq('a.storage', $qb->createNamedParameter($this->previewFolder->getStorageId())), + $qb->expr()->eq('a.parent', $qb->createNamedParameter($this->previewFolder->getId())), + $qb->expr()->like('a.name', $qb->createNamedParameter('__%')), + $qb->expr()->eq('a.mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('httpd/unix-directory'))) + ) ); if (!$this->isCLI) { $qb->setMaxResults(10); } - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); while ($row = $cursor->fetch()) { yield $row['name']; @@ -96,7 +90,7 @@ class BackgroundCleanupJob extends TimedJob { $qb->select('path', 'mimetype') ->from('filecache') ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($this->previewFolder->getId()))); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); $data = $cursor->fetch(); $cursor->closeCursor(); @@ -104,6 +98,15 @@ class BackgroundCleanupJob extends TimedJob { return []; } + if ($this->connection->getShardDefinition('filecache')) { + $chunks = $this->getAllPreviewIds($data['path'], 1000); + foreach ($chunks as $chunk) { + yield from $this->findMissingSources($chunk); + } + + 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 @@ -145,7 +148,7 @@ class BackgroundCleanupJob extends TimedJob { $qb->setMaxResults(10); } - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); while ($row = $cursor->fetch()) { yield $row['name']; @@ -153,4 +156,46 @@ class BackgroundCleanupJob extends TimedJob { $cursor->closeCursor(); } + + private function getAllPreviewIds(string $previewRoot, int $chunkSize): \Iterator { + // See `getNewPreviewLocations` for some more info about the logic here + $like = $this->connection->escapeLikeParameter($previewRoot) . '/_/_/_/_/_/_/_/%'; + + $qb = $this->connection->getQueryBuilder(); + $qb->select('name', 'fileid') + ->from('filecache') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('storage', $qb->createNamedParameter($this->previewFolder->getStorageId())), + $qb->expr()->like('path', $qb->createNamedParameter($like)), + $qb->expr()->eq('mimetype', $qb->createNamedParameter($this->mimeTypeLoader->getId('httpd/unix-directory'))), + $qb->expr()->gt('fileid', $qb->createParameter('min_id')), + ) + ) + ->orderBy('fileid', 'ASC') + ->setMaxResults($chunkSize); + + $minId = 0; + while (true) { + $qb->setParameter('min_id', $minId); + $rows = $qb->executeQuery()->fetchAll(); + if (count($rows) > 0) { + $minId = $rows[count($rows) - 1]['fileid']; + yield array_map(function ($row) { + return (int)$row['name']; + }, $rows); + } else { + break; + } + } + } + + private function findMissingSources(array $ids): array { + $qb = $this->connection->getQueryBuilder(); + $qb->select('fileid') + ->from('filecache') + ->where($qb->expr()->in('fileid', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))); + $found = $qb->executeQuery()->fetchAll(\PDO::FETCH_COLUMN); + return array_diff($ids, $found); + } } diff --git a/lib/private/Preview/Bitmap.php b/lib/private/Preview/Bitmap.php index c9fad50841f..a3d5fbfd4ec 100644 --- a/lib/private/Preview/Bitmap.php +++ b/lib/private/Preview/Bitmap.php @@ -61,7 +61,7 @@ abstract class Bitmap extends ProviderV2 { //new bitmap image object $image = new \OCP\Image(); - $image->loadFromData((string) $bp); + $image->loadFromData((string)$bp); //check if image object is valid return $image->valid() ? $image : null; } diff --git a/lib/private/Preview/Bundled.php b/lib/private/Preview/Bundled.php index 836dc4bd357..6100e8262a4 100644 --- a/lib/private/Preview/Bundled.php +++ b/lib/private/Preview/Bundled.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php index 4083b9f4f61..4a7341896ef 100644 --- a/lib/private/Preview/Generator.php +++ b/lib/private/Preview/Generator.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -11,6 +12,7 @@ use OCP\Files\IAppData; use OCP\Files\InvalidPathException; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; +use OCP\Files\SimpleFS\InMemoryFile; use OCP\Files\SimpleFS\ISimpleFile; use OCP\Files\SimpleFS\ISimpleFolder; use OCP\IConfig; @@ -20,34 +22,20 @@ use OCP\IStreamImage; use OCP\Preview\BeforePreviewFetchedEvent; use OCP\Preview\IProviderV2; use OCP\Preview\IVersionedPreviewFile; +use Psr\Log\LoggerInterface; class Generator { public const SEMAPHORE_ID_ALL = 0x0a11; public const SEMAPHORE_ID_NEW = 0x07ea; - /** @var IPreview */ - private $previewManager; - /** @var IConfig */ - private $config; - /** @var IAppData */ - private $appData; - /** @var GeneratorHelper */ - private $helper; - /** @var IEventDispatcher */ - private $eventDispatcher; - public function __construct( - IConfig $config, - IPreview $previewManager, - IAppData $appData, - GeneratorHelper $helper, - IEventDispatcher $eventDispatcher + private IConfig $config, + private IPreview $previewManager, + private IAppData $appData, + private GeneratorHelper $helper, + private IEventDispatcher $eventDispatcher, + private LoggerInterface $logger, ) { - $this->config = $config; - $this->previewManager = $previewManager; - $this->appData = $appData; - $this->helper = $helper; - $this->eventDispatcher = $eventDispatcher; } /** @@ -56,17 +44,19 @@ class Generator { * The cache is searched first and if nothing usable was found then a preview is * generated by one of the providers * - * @param File $file - * @param int $width - * @param int $height - * @param bool $crop - * @param string $mode - * @param string|null $mimeType * @return ISimpleFile * @throws NotFoundException * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) */ - public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null) { + public function getPreview( + File $file, + int $width = -1, + int $height = -1, + bool $crop = false, + string $mode = IPreview::MODE_FILL, + ?string $mimeType = null, + bool $cacheResult = true, + ): ISimpleFile { $specification = [ 'width' => $width, 'height' => $height, @@ -80,25 +70,33 @@ class Generator { $height, $crop, $mode, + $mimeType, )); + $this->logger->debug('Requesting preview for {path} with width={width}, height={height}, crop={crop}, mode={mode}, mimeType={mimeType}', [ + 'path' => $file->getPath(), + 'width' => $width, + 'height' => $height, + 'crop' => $crop, + 'mode' => $mode, + 'mimeType' => $mimeType, + ]); + + // since we only ask for one preview, and the generate method return the last one it created, it returns the one we want - return $this->generatePreviews($file, [$specification], $mimeType); + return $this->generatePreviews($file, [$specification], $mimeType, $cacheResult); } /** * Generates previews of a file * - * @param File $file - * @param non-empty-array $specifications - * @param string $mimeType - * @return ISimpleFile the last preview that was generated * @throws NotFoundException * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) */ - public function generatePreviews(File $file, array $specifications, $mimeType = null) { + public function generatePreviews(File $file, array $specifications, ?string $mimeType = null, bool $cacheResult = true): ISimpleFile { //Make sure that we can read the file if (!$file->isReadable()) { + $this->logger->warning('Cannot read file: {path}, skipping preview generation.', ['path' => $file->getPath()]); throw new NotFoundException('Cannot read file'); } @@ -120,6 +118,7 @@ class Generator { $maxPreviewImage = null; // only load the image when we need it if ($maxPreview->getSize() === 0) { $maxPreview->delete(); + $this->logger->error('Max preview generated for file {path} has size 0, deleting and throwing exception.', ['path' => $file->getPath()]); throw new NotFoundException('Max preview size 0, invalid!'); } @@ -166,12 +165,13 @@ class Generator { $maxPreviewImage = $this->helper->getImage($maxPreview); } - $preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion); + $this->logger->debug('Cached preview not found for file {path}, generating a new preview.', ['path' => $file->getPath()]); + $preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion, $cacheResult); // New file, augment our array $previewFiles[] = $preview; } } catch (\InvalidArgumentException $e) { - throw new NotFoundException("", 0, $e); + throw new NotFoundException('', 0, $e); } if ($preview->getSize() === 0) { @@ -272,13 +272,13 @@ class Generator { $hardwareConcurrency = self::getHardwareConcurrency(); switch ($type) { - case "preview_concurrency_all": + case 'preview_concurrency_all': $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency * 2 : 8; $concurrency_all = $this->config->getSystemValueInt($type, $fallback); - $concurrency_new = $this->getNumConcurrentPreviews("preview_concurrency_new"); + $concurrency_new = $this->getNumConcurrentPreviews('preview_concurrency_new'); $cached[$type] = max($concurrency_all, $concurrency_new); break; - case "preview_concurrency_new": + case 'preview_concurrency_new': $fallback = $hardwareConcurrency > 0 ? $hardwareConcurrency : 4; $cached[$type] = $this->config->getSystemValueInt($type, $fallback); break; @@ -334,6 +334,11 @@ class Generator { $previewConcurrency = $this->getNumConcurrentPreviews('preview_concurrency_new'); $sem = self::guardWithSemaphore(self::SEMAPHORE_ID_NEW, $previewConcurrency); try { + $this->logger->debug('Calling preview provider for {mimeType} with width={width}, height={height}', [ + 'mimeType' => $mimeType, + 'width' => $width, + 'height' => $height, + ]); $preview = $this->helper->getThumbnail($provider, $file, $width, $height); } finally { self::unguardWithSemaphore($sem); @@ -345,11 +350,10 @@ class Generator { $path = $this->generatePath($preview->width(), $preview->height(), $crop, $max, $preview->dataMimeType(), $prefix); try { - $file = $previewFolder->newFile($path); if ($preview instanceof IStreamImage) { - $file->putContent($preview->resource()); + return $previewFolder->newFile($path, $preview->resource()); } else { - $file->putContent($preview->data()); + return $previewFolder->newFile($path, $preview->data()); } } catch (NotPermittedException $e) { throw new NotFoundException(); @@ -484,19 +488,20 @@ class Generator { } /** - * @param ISimpleFolder $previewFolder - * @param ISimpleFile $maxPreview - * @param int $width - * @param int $height - * @param bool $crop - * @param int $maxWidth - * @param int $maxHeight - * @param string $prefix - * @return ISimpleFile * @throws NotFoundException * @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid) */ - private function generatePreview(ISimpleFolder $previewFolder, IImage $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight, $prefix) { + private function generatePreview( + ISimpleFolder $previewFolder, + IImage $maxPreview, + int $width, + int $height, + bool $crop, + int $maxWidth, + int $maxHeight, + string $prefix, + bool $cacheResult, + ): ISimpleFile { $preview = $maxPreview; if (!$preview->valid()) { throw new \InvalidArgumentException('Failed to generate preview, failed to load image'); @@ -533,12 +538,14 @@ class Generator { $path = $this->generatePath($width, $height, $crop, false, $preview->dataMimeType(), $prefix); try { - $file = $previewFolder->newFile($path); - $file->putContent($preview->data()); + if ($cacheResult) { + return $previewFolder->newFile($path, $preview->data()); + } else { + return new InMemoryFile($path, $preview->data()); + } } catch (NotPermittedException $e) { throw new NotFoundException(); } - return $file; } @@ -557,6 +564,7 @@ class Generator { $path = $this->generatePath($width, $height, $crop, false, $mimeType, $prefix); foreach ($files as $file) { if ($file->getName() === $path) { + $this->logger->debug('Found cached preview: {path}', ['path' => $path]); return $file; } } diff --git a/lib/private/Preview/GeneratorHelper.php b/lib/private/Preview/GeneratorHelper.php index 5f43c94b624..e914dcc2002 100644 --- a/lib/private/Preview/GeneratorHelper.php +++ b/lib/private/Preview/GeneratorHelper.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Preview/HEIC.php b/lib/private/Preview/HEIC.php index d198f11fdef..64eb48e58df 100644 --- a/lib/private/Preview/HEIC.php +++ b/lib/private/Preview/HEIC.php @@ -12,6 +12,7 @@ namespace OC\Preview; use OCP\Files\File; use OCP\Files\FileInfo; use OCP\IImage; +use OCP\Server; use Psr\Log\LoggerInterface; /** @@ -31,7 +32,7 @@ class HEIC extends ProviderV2 { * {@inheritDoc} */ public function isAvailable(FileInfo $file): bool { - return in_array('HEIC', \Imagick::queryFormats("HEI*")); + return in_array('HEIC', \Imagick::queryFormats('HEI*')); } /** @@ -44,8 +45,8 @@ class HEIC extends ProviderV2 { $tmpPath = $this->getLocalFile($file); if ($tmpPath === false) { - \OC::$server->get(LoggerInterface::class)->error( - 'Failed to get thumbnail for: ' . $file->getPath(), + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $file->getPath(), ['app' => 'core'] ); return null; @@ -70,7 +71,7 @@ class HEIC extends ProviderV2 { //new bitmap image object $image = new \OCP\Image(); - $image->loadFromData((string) $bp); + $image->loadFromData((string)$bp); //check if image object is valid return $image->valid() ? $image : null; } diff --git a/lib/private/Preview/Image.php b/lib/private/Preview/Image.php index 69841f07929..78a402c636a 100644 --- a/lib/private/Preview/Image.php +++ b/lib/private/Preview/Image.php @@ -9,6 +9,8 @@ namespace OC\Preview; use OCP\Files\File; use OCP\IImage; +use OCP\Server; +use Psr\Log\LoggerInterface; abstract class Image extends ProviderV2 { /** @@ -25,6 +27,13 @@ abstract class Image extends ProviderV2 { $image = new \OCP\Image(); $fileName = $this->getLocalFile($file); + if ($fileName === false) { + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $file->getPath(), + ['app' => 'core'] + ); + return null; + } $image->loadFromFile($fileName); $image->fixOrientation(); diff --git a/lib/private/Preview/Imaginary.php b/lib/private/Preview/Imaginary.php index 23459be3b51..d421da74ac8 100644 --- a/lib/private/Preview/Imaginary.php +++ b/lib/private/Preview/Imaginary.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -40,7 +41,7 @@ class Imaginary extends ProviderV2 { } public static function supportedMimeTypes(): string { - return '/(image\/(bmp|x-bitmap|png|jpeg|gif|heic|heif|svg\+xml|tiff|webp)|application\/(pdf|illustrator))/'; + return '/(image\/(bmp|x-bitmap|png|jpeg|gif|heic|heif|svg\+xml|tiff|webp)|application\/illustrator)/'; } public function getCroppedThumbnail(File $file, int $maxX, int $maxY, bool $crop): ?IImage { diff --git a/lib/private/Preview/ImaginaryPDF.php b/lib/private/Preview/ImaginaryPDF.php new file mode 100644 index 00000000000..d26c6d1b7ff --- /dev/null +++ b/lib/private/Preview/ImaginaryPDF.php @@ -0,0 +1,14 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Preview; + +class ImaginaryPDF extends Imaginary { + public static function supportedMimeTypes(): string { + return '/application\/pdf/'; + } +} diff --git a/lib/private/Preview/Krita.php b/lib/private/Preview/Krita.php index 2e77c7befd2..e96fac993aa 100644 --- a/lib/private/Preview/Krita.php +++ b/lib/private/Preview/Krita.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Preview/MP3.php b/lib/private/Preview/MP3.php index 105b182b415..add0028738e 100644 --- a/lib/private/Preview/MP3.php +++ b/lib/private/Preview/MP3.php @@ -9,6 +9,8 @@ namespace OC\Preview; use OCP\Files\File; use OCP\IImage; +use OCP\Server; +use Psr\Log\LoggerInterface; use wapmorgan\Mp3Info\Mp3Info; use function OCP\Log\logger; @@ -25,6 +27,13 @@ class MP3 extends ProviderV2 { */ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage { $tmpPath = $this->getLocalFile($file); + if ($tmpPath === false) { + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $file->getPath(), + ['app' => 'core'] + ); + return null; + } try { $audio = new Mp3Info($tmpPath, true); diff --git a/lib/private/Preview/MarkDown.php b/lib/private/Preview/MarkDown.php index 41a79455042..c20433a1ac0 100644 --- a/lib/private/Preview/MarkDown.php +++ b/lib/private/Preview/MarkDown.php @@ -52,7 +52,7 @@ class MarkDown extends TXT { $lines = preg_split("/\r\n|\n|\r/", $content); // Define text size of text file preview - $fontSize = $maxX ? (int) ((1 / ($maxX >= 512 ? 60 : 40) * $maxX)) : 10; + $fontSize = $maxX ? (int)((1 / ($maxX >= 512 ? 60 : 40) * $maxX)) : 10; $image = imagecreate($maxX, $maxY); imagecolorallocate($image, 255, 255, 255); diff --git a/lib/private/Preview/MimeIconProvider.php b/lib/private/Preview/MimeIconProvider.php index 167dd21b262..d1963fe882b 100644 --- a/lib/private/Preview/MimeIconProvider.php +++ b/lib/private/Preview/MimeIconProvider.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -22,7 +23,7 @@ class MimeIconProvider implements IMimeIconProvider { ) { } - public function getMimeIconUrl(string $mime): null|string { + public function getMimeIconUrl(string $mime): ?string { if (!$mime) { return null; } @@ -54,8 +55,8 @@ class MimeIconProvider implements IMimeIconProvider { return null; } - - private function searchfileName(string $fileName): null|string { + + private function searchfileName(string $fileName): ?string { // If the file exists in the current enabled legacy // custom theme, let's return it $theme = $this->config->getSystemValue('theme', ''); @@ -65,7 +66,7 @@ class MimeIconProvider implements IMimeIconProvider { return $this->urlGenerator->getAbsoluteURL($path); } } - + // Previously, we used to pass this through Theming // But it was only used to colour icons containing // 0082c9. Since with vue we moved to inline svg icons, diff --git a/lib/private/Preview/Movie.php b/lib/private/Preview/Movie.php index cfc05b8cce9..47895f999d8 100644 --- a/lib/private/Preview/Movie.php +++ b/lib/private/Preview/Movie.php @@ -5,32 +5,27 @@ * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OC\Preview; use OCP\Files\File; use OCP\Files\FileInfo; +use OCP\IConfig; use OCP\IImage; +use OCP\ITempManager; +use OCP\Server; use Psr\Log\LoggerInterface; class Movie extends ProviderV2 { - /** - * @deprecated 23.0.0 pass option to \OCP\Preview\ProviderV2 - * @var string - */ - public static $avconvBinary; + private IConfig $config; - /** - * @deprecated 23.0.0 pass option to \OCP\Preview\ProviderV2 - * @var string - */ - public static $ffmpegBinary; + private ?string $binary = null; - /** @var string */ - private $binary; + public function __construct(array $options = []) { + parent::__construct($options); + $this->config = Server::get(IConfig::class); + } - /** - * {@inheritDoc} - */ public function getMimeType(): string { return '/video\/.*/'; } @@ -39,14 +34,9 @@ class Movie extends ProviderV2 { * {@inheritDoc} */ public function isAvailable(FileInfo $file): bool { - // TODO: remove when avconv is dropped if (is_null($this->binary)) { if (isset($this->options['movieBinary'])) { $this->binary = $this->options['movieBinary']; - } elseif (is_string(self::$avconvBinary)) { - $this->binary = self::$avconvBinary; - } elseif (is_string(self::$ffmpegBinary)) { - $this->binary = self::$ffmpegBinary; } } return is_string($this->binary); @@ -64,10 +54,15 @@ class Movie extends ProviderV2 { $result = null; if ($this->useTempFile($file)) { - // try downloading 5 MB first as it's likely that the first frames are present there - // in some cases this doesn't work for example when the moov atom is at the - // end of the file, so if it fails we fall back to getting the full file - $sizeAttempts = [5242880, null]; + // Try downloading 5 MB first, as it's likely that the first frames are present there. + // In some cases this doesn't work, for example when the moov atom is at the + // end of the file, so if it fails we fall back to getting the full file. + // Unless the file is not local (e.g. S3) as we do not want to download the whole (e.g. 37Gb) file + if ($file->getStorage()->isLocal()) { + $sizeAttempts = [5242880, null]; + } else { + $sizeAttempts = [5242880]; + } } else { // size is irrelevant, only attempt once $sizeAttempts = [null]; @@ -75,15 +70,19 @@ class Movie extends ProviderV2 { foreach ($sizeAttempts as $size) { $absPath = $this->getLocalFile($file, $size); + if ($absPath === false) { + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $file->getPath(), + ['app' => 'core'] + ); + return null; + } - $result = null; - if (is_string($absPath)) { - $result = $this->generateThumbNail($maxX, $maxY, $absPath, 5); + $result = $this->generateThumbNail($maxX, $maxY, $absPath, 5); + if ($result === null) { + $result = $this->generateThumbNail($maxX, $maxY, $absPath, 1); if ($result === null) { - $result = $this->generateThumbNail($maxX, $maxY, $absPath, 1); - if ($result === null) { - $result = $this->generateThumbNail($maxX, $maxY, $absPath, 0); - } + $result = $this->generateThumbNail($maxX, $maxY, $absPath, 0); } } @@ -97,8 +96,42 @@ class Movie extends ProviderV2 { return $result; } + private function useHdr(string $absPath): bool { + // load ffprobe path from configuration, otherwise generate binary path using ffmpeg binary path + $ffprobe_binary = $this->config->getSystemValue('preview_ffprobe_path', null) ?? (pathinfo($this->binary, PATHINFO_DIRNAME) . '/ffprobe'); + // run ffprobe on the video file to get value of "color_transfer" + $test_hdr_cmd = [$ffprobe_binary,'-select_streams', 'v:0', + '-show_entries', 'stream=color_transfer', + '-of', 'default=noprint_wrappers=1:nokey=1', + $absPath]; + $test_hdr_proc = proc_open($test_hdr_cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $test_hdr_pipes); + if ($test_hdr_proc === false) { + return false; + } + $test_hdr_stdout = trim(stream_get_contents($test_hdr_pipes[1])); + $test_hdr_stderr = trim(stream_get_contents($test_hdr_pipes[2])); + proc_close($test_hdr_proc); + // search build options for libzimg (provides zscale filter) + $ffmpeg_libzimg_installed = strpos($test_hdr_stderr, '--enable-libzimg'); + // Only values of "smpte2084" and "arib-std-b67" indicate an HDR video. + // Only return true if video is detected as HDR and libzimg is installed. + if (($test_hdr_stdout === 'smpte2084' || $test_hdr_stdout === 'arib-std-b67') && $ffmpeg_libzimg_installed !== false) { + return true; + } else { + return false; + } + } + private function generateThumbNail(int $maxX, int $maxY, string $absPath, int $second): ?IImage { - $tmpPath = \OC::$server->getTempManager()->getTemporaryFile(); + $tmpPath = Server::get(ITempManager::class)->getTemporaryFile(); + + if ($tmpPath === false) { + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $absPath, + ['app' => 'core'] + ); + return null; + } $binaryType = substr(strrchr($this->binary, '/'), 1); @@ -108,10 +141,21 @@ class Movie extends ProviderV2 { '-an', '-f', 'mjpeg', '-vframes', '1', '-vsync', '1', $tmpPath]; } elseif ($binaryType === 'ffmpeg') { - $cmd = [$this->binary, '-y', '-ss', (string)$second, - '-i', $absPath, - '-f', 'mjpeg', '-vframes', '1', - $tmpPath]; + if ($this->useHdr($absPath)) { + // Force colorspace to '2020_ncl' because some videos are + // tagged incorrectly as 'reserved' resulting in fail if not forced. + $cmd = [$this->binary, '-y', '-ss', (string)$second, + '-i', $absPath, + '-f', 'mjpeg', '-vframes', '1', + '-vf', 'zscale=min=2020_ncl:t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p', + $tmpPath]; + } else { + // always default to generating preview using non-HDR command + $cmd = [$this->binary, '-y', '-ss', (string)$second, + '-i', $absPath, + '-f', 'mjpeg', '-vframes', '1', + $tmpPath]; + } } else { // Not supported unlink($tmpPath); @@ -120,10 +164,10 @@ class Movie extends ProviderV2 { $proc = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); $returnCode = -1; - $output = ""; + $output = ''; if (is_resource($proc)) { - $stdout = trim(stream_get_contents($pipes[1])); $stderr = trim(stream_get_contents($pipes[2])); + $stdout = trim(stream_get_contents($pipes[1])); $returnCode = proc_close($proc); $output = $stdout . $stderr; } @@ -140,7 +184,7 @@ class Movie extends ProviderV2 { } if ($second === 0) { - $logger = \OC::$server->get(LoggerInterface::class); + $logger = Server::get(LoggerInterface::class); $logger->info('Movie preview generation failed Output: {output}', ['app' => 'core', 'output' => $output]); } diff --git a/lib/private/Preview/Office.php b/lib/private/Preview/Office.php index 20fbef6eb23..ffba0211de2 100644 --- a/lib/private/Preview/Office.php +++ b/lib/private/Preview/Office.php @@ -12,6 +12,7 @@ use OCP\Files\FileInfo; use OCP\IImage; use OCP\ITempManager; use OCP\Server; +use Psr\Log\LoggerInterface; abstract class Office extends ProviderV2 { /** @@ -33,6 +34,13 @@ abstract class Office extends ProviderV2 { // The file to generate the preview for. $absPath = $this->getLocalFile($file); + if ($absPath === false) { + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $file->getPath(), + ['app' => 'core'] + ); + return null; + } // The destination for the LibreOffice user profile. // LibreOffice can rune once per user profile and therefore instance id and file id are included. diff --git a/lib/private/Preview/ProviderV2.php b/lib/private/Preview/ProviderV2.php index ca10aa67b36..556d1099d2d 100644 --- a/lib/private/Preview/ProviderV2.php +++ b/lib/private/Preview/ProviderV2.php @@ -6,27 +6,23 @@ declare(strict_types=1); * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OC\Preview; use OCP\Files\File; use OCP\Files\FileInfo; use OCP\IImage; +use OCP\ITempManager; use OCP\Preview\IProviderV2; +use OCP\Server; +use Psr\Log\LoggerInterface; abstract class ProviderV2 implements IProviderV2 { - /** @var array */ - protected $options; - - /** @var array */ - protected $tmpFiles = []; + protected array $tmpFiles = []; - /** - * Constructor - * - * @param array $options - */ - public function __construct(array $options = []) { - $this->options = $options; + public function __construct( + protected array $options = [], + ) { } /** @@ -50,7 +46,7 @@ abstract class ProviderV2 implements IProviderV2 { * @param File $file * @param int $maxX The maximum X size of the thumbnail. It can be smaller depending on the shape of the image * @param int $maxY The maximum Y size of the thumbnail. It can be smaller depending on the shape of the image - * @return null|\OCP\IImage false if no preview was generated + * @return null|\OCP\IImage null if no preview was generated * @since 17.0.0 */ abstract public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage; @@ -63,14 +59,24 @@ abstract class ProviderV2 implements IProviderV2 { * Get a path to either the local file or temporary file * * @param File $file - * @param int $maxSize maximum size for temporary files - * @return string|false + * @param ?int $maxSize maximum size for temporary files */ - protected function getLocalFile(File $file, ?int $maxSize = null) { + protected function getLocalFile(File $file, ?int $maxSize = null): string|false { if ($this->useTempFile($file)) { - $absPath = \OC::$server->getTempManager()->getTemporaryFile(); + $absPath = Server::get(ITempManager::class)->getTemporaryFile(); + + if ($absPath === false) { + Server::get(LoggerInterface::class)->error( + 'Failed to get local file to generate thumbnail for: ' . $file->getPath(), + ['app' => 'core'] + ); + return false; + } $content = $file->fopen('r'); + if ($content === false) { + return false; + } if ($maxSize) { $content = stream_get_contents($content, $maxSize); diff --git a/lib/private/Preview/SGI.php b/lib/private/Preview/SGI.php index 06ea9c0c69a..78b1ea5828a 100644 --- a/lib/private/Preview/SGI.php +++ b/lib/private/Preview/SGI.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Preview/SVG.php b/lib/private/Preview/SVG.php index 73dc0488bf1..d9f7701f411 100644 --- a/lib/private/Preview/SVG.php +++ b/lib/private/Preview/SVG.php @@ -58,7 +58,7 @@ class SVG extends ProviderV2 { //new image object $image = new \OCP\Image(); - $image->loadFromData((string) $svg); + $image->loadFromData((string)$svg); //check if image object is valid if ($image->valid()) { $image->scaleDownToFit($maxX, $maxY); diff --git a/lib/private/Preview/TGA.php b/lib/private/Preview/TGA.php index 62e5aadc2af..675907b4e49 100644 --- a/lib/private/Preview/TGA.php +++ b/lib/private/Preview/TGA.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Preview/TXT.php b/lib/private/Preview/TXT.php index 68597f8dbd0..1a1d64f3e08 100644 --- a/lib/private/Preview/TXT.php +++ b/lib/private/Preview/TXT.php @@ -50,7 +50,7 @@ class TXT extends ProviderV2 { $lines = preg_split("/\r\n|\n|\r/", $content); // Define text size of text file preview - $fontSize = $maxX ? (int) ((1 / 32) * $maxX) : 5; //5px + $fontSize = $maxX ? (int)((1 / 32) * $maxX) : 5; //5px $lineSize = ceil($fontSize * 1.5); $image = imagecreate($maxX, $maxY); @@ -67,7 +67,7 @@ class TXT extends ProviderV2 { $index = $index + 1; $x = 1; - $y = (int) ($index * $lineSize); + $y = (int)($index * $lineSize); if ($canUseTTF === true) { imagettftext($image, $fontSize, 0, $x, $y, $textColor, $fontFile, $line); diff --git a/lib/private/Preview/Watcher.php b/lib/private/Preview/Watcher.php index abddd7b5acb..21f040d8342 100644 --- a/lib/private/Preview/Watcher.php +++ b/lib/private/Preview/Watcher.php @@ -8,6 +8,7 @@ declare(strict_types=1); */ namespace OC\Preview; +use OCP\Files\FileInfo; use OCP\Files\Folder; use OCP\Files\IAppData; use OCP\Files\Node; @@ -37,7 +38,7 @@ class Watcher { $this->deleteNode($node); } - protected function deleteNode(Node $node) { + protected function deleteNode(FileInfo $node) { // We only handle files if ($node instanceof Folder) { return; diff --git a/lib/private/Preview/WatcherConnector.php b/lib/private/Preview/WatcherConnector.php index ae2a136ca78..c34dd1dde4d 100644 --- a/lib/private/Preview/WatcherConnector.php +++ b/lib/private/Preview/WatcherConnector.php @@ -9,43 +9,33 @@ declare(strict_types=1); namespace OC\Preview; use OC\SystemConfig; +use OCA\Files_Versions\Events\VersionRestoredEvent; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\IRootFolder; use OCP\Files\Node; class WatcherConnector { - /** @var IRootFolder */ - private $root; - - /** @var SystemConfig */ - private $config; - - /** - * WatcherConnector constructor. - * - * @param IRootFolder $root - * @param SystemConfig $config - */ - public function __construct(IRootFolder $root, - SystemConfig $config) { - $this->root = $root; - $this->config = $config; + public function __construct( + private IRootFolder $root, + private SystemConfig $config, + private IEventDispatcher $dispatcher, + ) { } - /** - * @return Watcher - */ private function getWatcher(): Watcher { return \OCP\Server::get(Watcher::class); } - public function connectWatcher() { + public function connectWatcher(): void { // Do not connect if we are not setup yet! if ($this->config->getValue('instanceid', null) !== null) { $this->root->listen('\OC\Files', 'postWrite', function (Node $node) { $this->getWatcher()->postWrite($node); }); - \OC_Hook::connect('\OCP\Versions', 'rollback', $this->getWatcher(), 'versionRollback'); + $this->dispatcher->addListener(VersionRestoredEvent::class, function (VersionRestoredEvent $event) { + $this->getWatcher()->versionRollback(['node' => $event->getVersion()->getSourceFile()]); + }); } } } |