diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index ca99f2d374d8..75cc5779100a 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -40,6 +40,7 @@ use OCA\DAV\Connector\Sabre\Exception\FileLocked; use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException; use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType; +use OCA\DAV\Files\IFileNode; use OCP\Encryption\Exceptions\GenericEncryptionException; use OCP\Events\EventEmitterTrait; use OCP\Files\EntityTooLargeException; @@ -61,7 +62,7 @@ use Sabre\DAV\IFile; use Symfony\Component\EventDispatcher\GenericEvent; -class File extends Node implements IFile { +class File extends Node implements IFile, IFileNode { use EventEmitterTrait; protected $request; @@ -714,4 +715,11 @@ public function getChecksum($algo = null) { protected function header($string) { \header($string); } + + /** + * @return \OCP\Files\Node + */ + public function getNode() { + return \OC::$server->getRootFolder()->get($this->getFileInfo()->getPath()); + } } diff --git a/apps/dav/lib/Files/IFileNode.php b/apps/dav/lib/Files/IFileNode.php new file mode 100644 index 000000000000..588dfcbd6d55 --- /dev/null +++ b/apps/dav/lib/Files/IFileNode.php @@ -0,0 +1,35 @@ + + * + * @copyright Copyright (c) 2017, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + + +namespace OCA\DAV\Files; + + +use OCP\Files\Node; + +interface IFileNode { + + /** + * @return Node + */ + public function getNode(); + +} diff --git a/apps/dav/lib/Files/PreviewPlugin.php b/apps/dav/lib/Files/PreviewPlugin.php new file mode 100644 index 000000000000..ad56f800f2df --- /dev/null +++ b/apps/dav/lib/Files/PreviewPlugin.php @@ -0,0 +1,117 @@ + + * + * @copyright Copyright (c) 2017, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Files; + +use OCP\Files\IPreviewNode; +use OCP\ILogger; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class PreviewPlugin extends ServerPlugin { + + /** @var Server */ + protected $server; + /** @var ILogger */ + private $logger; + + public function __construct(ILogger $logger) { + $this->logger = $logger; + } + + /** + * Initializes the plugin and registers event handlers + * + * @param Server $server + * @return void + */ + function initialize(Server $server) { + + $this->server = $server; + $this->server->on('method:GET', [$this, 'httpGet'], 90); + } + + /** + * Intercepts GET requests on node urls ending with ?preview. + * The node has to implement IPreviewNode + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + * @throws NotFound + */ + function httpGet(RequestInterface $request, ResponseInterface $response) { + + $queryParams = $request->getQueryParameters(); + if (!array_key_exists('preview', $queryParams)) { + return true; + } + + $path = $request->getPath(); + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof IFileNode) { + return false; + } + $fileNode = $node->getNode(); + if (!$fileNode instanceof IPreviewNode) { + return false; + } + + // Checking ACL, if available. + if ($aclPlugin = $this->server->getPlugin('acl')) { + /** @var \Sabre\DAVACL\Plugin $aclPlugin */ + $aclPlugin->checkPrivileges($path, '{DAV:}read'); + } + + if ($image = $fileNode->getThumbnail($queryParams)) { + if ($image === null || !$image->valid()) { + return false; + } + $type = $image->mimeType(); + if (!in_array($type, ['image/png', 'image/jpeg', 'image/gif'])) { + $type = 'application/octet-stream'; + } + + // Enable output buffering + ob_start(); + // Capture the output + $image->show(); + $imageData = ob_get_contents(); + // Clear the output buffer + ob_end_clean(); + + $response->setHeader('Content-Type', $type); + $response->setHeader('Content-Disposition', 'attachment'); + $response->setStatus(200); + + $response->setBody($imageData); + + // Returning false to break the event chain + return false; + } + // TODO: add forceIcon handling .... if still needed + throw new NotFound(); + } +} diff --git a/apps/dav/lib/Meta/MetaFile.php b/apps/dav/lib/Meta/MetaFile.php index b3e277b6361d..00cfa5a4cd0f 100644 --- a/apps/dav/lib/Meta/MetaFile.php +++ b/apps/dav/lib/Meta/MetaFile.php @@ -26,6 +26,8 @@ use OC\Files\Meta\MetaFileVersionNode; use OCA\DAV\Files\ICopySource; use OCA\DAV\Files\IProvidesAdditionalHeaders; +use OCA\DAV\Files\IFileNode; +use OCP\Files\Node; use Sabre\DAV\File; /** @@ -34,7 +36,7 @@ * * @package OCA\DAV\Meta */ -class MetaFile extends File implements ICopySource, IProvidesAdditionalHeaders { +class MetaFile extends File implements ICopySource, IFileNode, IProvidesAdditionalHeaders { /** @var \OCP\Files\File */ private $file; @@ -119,4 +121,11 @@ public function getContentDispositionFileName() { } return $this->getName(); } + + /** + * @return Node + */ + public function getNode() { + return $this->file; + } } diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 24d5b564c059..0daed1f1cc20 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -50,6 +50,7 @@ use OCA\DAV\DAV\MiscCustomPropertiesBackend; use OCA\DAV\DAV\PublicAuth; use OCA\DAV\Files\BrowserErrorPagePlugin; +use OCA\DAV\Files\PreviewPlugin; use OCA\DAV\SystemTag\SystemTagPlugin; use OCA\DAV\Upload\ChunkingPlugin; use OCP\IRequest; @@ -185,6 +186,7 @@ public function __construct(IRequest $request, $baseUri) { $this->server->addPlugin(new BrowserErrorPagePlugin()); } + $this->server->addPlugin(new PreviewPlugin($logger)); // wait with registering these until auth is handled and the filesystem is setup $this->server->on('beforeMethod', function () use ($root) { // custom properties plugin must be the last one diff --git a/lib/private/Files/Meta/MetaFileVersionNode.php b/lib/private/Files/Meta/MetaFileVersionNode.php index 4ab1b29320d7..de5fe6fe47b2 100644 --- a/lib/private/Files/Meta/MetaFileVersionNode.php +++ b/lib/private/Files/Meta/MetaFileVersionNode.php @@ -19,17 +19,18 @@ * */ - namespace OC\Files\Meta; - use OC\Files\Node\AbstractFile; use OC\Files\Node\File; use OCP\Files\ForbiddenException; use OCP\Files\IProvidesAdditionalHeaders; +use OC\Preview; use OCP\Files\IRootFolder; +use OCP\Files\IPreviewNode; use OCP\Files\Storage\IVersionedStorage; use OCP\Files\Storage; +use OCP\IImage; /** * Class MetaFileVersionNode - this class represents a version of a file in the @@ -37,7 +38,7 @@ * * @package OC\Files\Meta */ -class MetaFileVersionNode extends AbstractFile implements IProvidesAdditionalHeaders { +class MetaFileVersionNode extends AbstractFile implements IPreviewNode, IProvidesAdditionalHeaders { /** @var string */ private $versionId; @@ -143,4 +144,33 @@ public function getContentDispositionFileName() { return basename($this->internalPath); } + public function getId() { + return $this->parent->getId(); + } + + public function getPath() { + return $this->parent->getPath() . '/' . $this->getName(); + } + + /** + * @param array $options + * @return IImage + * @since 10.1.0 + */ + public function getThumbnail($options) { + $maxX = array_key_exists('x', $options) ? (int)$options['x'] : 32; + $maxY = array_key_exists('y', $options) ? (int)$options['y'] : 32; + $scalingUp = array_key_exists('scalingup', $options) ? (bool)$options['scalingup'] : true; + $keepAspect = array_key_exists('a', $options) ? true : false; + $mode = array_key_exists('mode', $options) ? $options['mode'] : 'fill'; + + $preview = new Preview(); + $preview->setFile($this, $this->versionId); + $preview->setMaxX($maxX); + $preview->setMaxY($maxY); + $preview->setScalingUp($scalingUp); + $preview->setMode($mode); + $preview->setKeepAspect($keepAspect); + return $preview->getPreview(); + } } diff --git a/lib/private/Files/Node/File.php b/lib/private/Files/Node/File.php index 5deb6a8efac9..9af1d2364667 100644 --- a/lib/private/Files/Node/File.php +++ b/lib/private/Files/Node/File.php @@ -26,9 +26,11 @@ namespace OC\Files\Node; +use OCP\Files\IPreviewNode; use OCP\Files\NotPermittedException; +use OCP\IImage; -class File extends Node implements \OCP\Files\File { +class File extends Node implements \OCP\Files\File, IPreviewNode { /** * Creates a Folder that represents a non-existing path * @@ -138,4 +140,26 @@ public function hash($type, $raw = false) { public function getChecksum() { return $this->getFileInfo()->getChecksum(); } + + /** + * @param array $options + * @return IImage + * @since 10.1.0 + */ + public function getThumbnail($options) { + $maxX = array_key_exists('x', $options) ? (int)$options['x'] : 32; + $maxY = array_key_exists('y', $options) ? (int)$options['y'] : 32; + $scalingUp = array_key_exists('scalingup', $options) ? (bool)$options['scalingup'] : true; + $keepAspect = array_key_exists('a', $options) ? true : false; + $mode = array_key_exists('mode', $options) ? $options['mode'] : 'fill'; + + $preview = new \OC\Preview(); + $preview->setFile($this->getInternalPath(), $this->getFileInfo()); + $preview->setMaxX($maxX); + $preview->setMaxY($maxY); + $preview->setScalingUp($scalingUp); + $preview->setMode($mode); + $preview->setKeepAspect($keepAspect); + return $preview->getPreview(); + } } diff --git a/lib/private/Preview.php b/lib/private/Preview.php index 685831f226fd..1b79f28511c1 100644 --- a/lib/private/Preview.php +++ b/lib/private/Preview.php @@ -31,7 +31,9 @@ */ namespace OC; +use OC\Files\View; use OC\Preview\Provider; +use OCP\Files\File; use OCP\Files\FileInfo; use OCP\Files\NotFoundException; @@ -54,6 +56,7 @@ class Preview { private $userView = null; //vars + /** @var File */ private $file; private $maxX; private $maxY; @@ -72,7 +75,7 @@ class Preview { /** @var int $previewHeight calculated height of the preview we're looking for */ private $previewHeight; - //filemapper used for deleting previews + // filemapper used for deleting previews // index is path, value is fileinfo static public $deleteFileMapper = []; static public $deleteChildrenMapper = []; @@ -83,18 +86,15 @@ class Preview { * @var \OCP\IImage */ private $preview; - - /** - * @var \OCP\Files\FileInfo - */ - protected $info; + /** @var string */ + private $versionId; /** * check if thumbnail or bigger version of thumbnail of file is cached * * @param string $user userid - if no user is given, OC_User::getUser will be used * @param string $root path of root - * @param string $file The path to the file where you want a thumbnail from + * @param File $file The path to the file where you want a thumbnail from * @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 @@ -109,16 +109,17 @@ class Preview { public function __construct( $user = '', $root = '/', - $file = '', $maxX = 1, + $file = null, $maxX = 1, $maxY = 1, - $scalingUp = true + $scalingUp = true, + $versionId = null ) { //init fileviews if ($user === '') { $user = \OC_User::getUser(); } - $this->fileView = new \OC\Files\View('/' . $user . '/' . $root); - $this->userView = new \OC\Files\View('/' . $user); + $this->fileView = new View('/' . $user . '/' . $root); + $this->userView = new View('/' . $user); //set config $sysConfig = \OC::$server->getConfig(); @@ -127,7 +128,9 @@ public function __construct( $this->maxScaleFactor = $sysConfig->getSystemValue('preview_max_scale_factor', 2); //save parameters - $this->setFile($file); + if ($file !== null) { + $this->setFile($file, $versionId); + } $this->setMaxX((int)$maxX); $this->setMaxY((int)$maxY); $this->setScalingup($scalingUp); @@ -148,7 +151,7 @@ public function __construct( /** * returns the path of the file you want a thumbnail from * - * @return string + * @return File */ public function getFile() { return $this->file; @@ -223,18 +226,9 @@ public function getConfigMaxY() { * @return false|Files\FileInfo|\OCP\Files\FileInfo */ protected function getFileInfo() { - $absPath = $this->fileView->getAbsolutePath($this->file); - $absPath = Files\Filesystem::normalizePath($absPath); - if (array_key_exists($absPath, self::$deleteFileMapper)) { - $this->info = self::$deleteFileMapper[$absPath]; - } else if (!$this->info) { - $this->info = $this->fileView->getFileInfo($this->file); - } - - return $this->info; + return $this->file; } - /** * @return array|null */ @@ -252,21 +246,16 @@ private function getChildren() { /** * Sets the path of the file you want a preview of * - * @param string $file + * @param File $file * @param \OCP\Files\FileInfo|null $info + * @param string $versionId * - * @return \OC\Preview + * @return Preview */ - public function setFile($file, $info = null) { + public function setFile(File $file, $versionId = null) { $this->file = $file; - $this->info = $info; - - if ($file !== '') { - $this->getFileInfo(); - if ($this->info instanceof \OCP\Files\FileInfo) { - $this->mimeType = $this->info->getMimetype(); - } - } + $this->versionId = $versionId; + $this->mimeType = $this->file->getMimetype(); return $this; } @@ -370,14 +359,14 @@ public function setKeepAspect($keepAspect) { */ public function isFileValid() { $file = $this->getFile(); - if ($file === '') { + if ($file === null) { \OCP\Util::writeLog('core', 'No filename passed', \OCP\Util::DEBUG); return false; } if (!$this->getFileInfo() instanceof FileInfo) { - \OCP\Util::writeLog('core', 'File:"' . $file . '" not found', \OCP\Util::DEBUG); + \OCP\Util::writeLog('core', 'File:"' . $file->getPath() . '" not found', \OCP\Util::DEBUG); return false; } @@ -395,9 +384,7 @@ public function isFileValid() { public function deletePreview() { $fileInfo = $this->getFileInfo(); if ($fileInfo !== null && $fileInfo !== false) { - $fileId = $fileInfo->getId(); - - $previewPath = $this->buildCachePath($fileId); + $previewPath = $this->buildCachePath(); if (!strpos($previewPath, 'max')) { return $this->userView->unlink($previewPath); } @@ -425,7 +412,7 @@ public function deleteAllPreviews() { // getId() might return null, e.g. when the file is a // .ocTransferId*.part file from chunked file upload. if (!empty($fileId)) { - $previewPath = $this->getPreviewPath($fileId); + $previewPath = $this->getPreviewPath(); $this->userView->rmdir($previewPath); } } @@ -443,11 +430,10 @@ public function deleteAllPreviews() { * thumbnail should be * * And finally, we look for a suitable candidate in the cache * - * @param int $fileId fileId of the original file we need a preview of - * * @return string|false path to the cached preview if it exists or false */ - public function isCached($fileId) { + public function isCached() { + $fileId = $this->getFileInfo()->getId(); if (is_null($fileId)) { return false; } @@ -455,7 +441,7 @@ public function isCached($fileId) { /** * Phase 1: Looking for the max preview */ - $previewPath = $this->getPreviewPath($fileId); + $previewPath = $this->getPreviewPath(); // We currently can't look for a single file due to bugs related to #16478 $allThumbnails = $this->userView->getDirectoryContent($previewPath); list($maxPreviewWidth, $maxPreviewHeight) = $this->getMaxPreviewSize($allThumbnails); @@ -482,7 +468,7 @@ public function isCached($fileId) { */ // This gives us a calculated path to a preview of asked dimensions // thumbnailFolder/fileId/-(-max|-with-aspect).png - $preview = $this->buildCachePath($fileId, $previewWidth, $previewHeight); + $preview = $this->buildCachePath($previewWidth, $previewHeight); // This checks if we have a preview of those exact dimensions in the cache if ($this->thumbnailSizeExists($allThumbnails, basename($preview))) { @@ -497,11 +483,11 @@ public function isCached($fileId) { ) { // The preview we-re looking for is the exact size or larger than the max preview, // so return that - return $this->buildCachePath($fileId, $maxPreviewWidth, $maxPreviewHeight); + return $this->buildCachePath($maxPreviewWidth, $maxPreviewHeight); } else { // The last resort is to look for something bigger than what we've calculated, // but still smaller than the max preview - return $this->isCachedBigger($fileId, $allThumbnails); + return $this->isCachedBigger($allThumbnails); } } @@ -648,12 +634,11 @@ private function fixSize($askedWidth, $askedHeight) { * Checks if a bigger version of a file preview is cached and if not * return the preview of max allowed dimensions * - * @param int $fileId fileId of the original image * @param FileInfo[] $allThumbnails the list of all our cached thumbnails * * @return string path to bigger thumbnail */ - private function isCachedBigger($fileId, $allThumbnails) { + private function isCachedBigger($allThumbnails) { // This is used to eliminate any thumbnail narrower than what we need $maxX = $this->getMaxX(); @@ -669,7 +654,7 @@ private function isCachedBigger($fileId, $allThumbnails) { } // At this stage, we didn't find a preview, so we return the max preview - return $this->buildCachePath($fileId, $this->maxPreviewWidth, $this->maxPreviewHeight); + return $this->buildCachePath($this->maxPreviewWidth, $this->maxPreviewHeight); } /** @@ -766,14 +751,13 @@ public function getPreview() { return new \OC_Image(); } - $fileId = $fileInfo->getId(); - $cached = $this->isCached($fileId); + $cached = $this->isCached(); if ($cached) { - $this->getCachedPreview($fileId, $cached); + $this->getCachedPreview($cached); } if (is_null($this->preview)) { - $this->generatePreview($fileId); + $this->generatePreview(); } // We still don't have a preview, so we send back an empty object @@ -813,10 +797,9 @@ public function showPreview($mimeTypeForHeaders = null) { /** * Retrieves the preview from the cache and resizes it if necessary * - * @param int $fileId fileId of the original image * @param string $cached the path to the cached preview */ - private function getCachedPreview($fileId, $cached) { + private function getCachedPreview($cached) { $stream = $this->userView->fopen($cached, 'r'); $this->preview = null; if ($stream) { @@ -835,7 +818,7 @@ private function getCachedPreview($fileId, $cached) { // We don't have an exact match if ($previewX !== $maxX || $previewY !== $maxY) { - $this->resizeAndStore($fileId); + $this->resizeAndStore(); } } @@ -845,10 +828,8 @@ private function getCachedPreview($fileId, $cached) { /** * Resizes, crops, fixes orientation and stores in the cache - * - * @param int $fileId fileId of the original image */ - private function resizeAndStore($fileId) { + private function resizeAndStore() { $image = $this->preview; if (!($image instanceof \OCP\IImage)) { \OCP\Util::writeLog( @@ -885,7 +866,7 @@ private function resizeAndStore($fileId) { // The preview has been resized and should now have the asked dimensions if ($newPreviewWidth === $askedWidth && $newPreviewHeight === $askedHeight) { - $this->storePreview($fileId, $newPreviewWidth, $newPreviewHeight); + $this->storePreview($newPreviewWidth, $newPreviewHeight); return; } @@ -897,7 +878,7 @@ private function resizeAndStore($fileId) { // It turns out the scaled preview is now too big, so we crop the image if ($newPreviewWidth >= $askedWidth && $newPreviewHeight >= $askedHeight) { $this->crop($image, $askedWidth, $askedHeight, $newPreviewWidth, $newPreviewHeight); - $this->storePreview($fileId, $askedWidth, $askedHeight); + $this->storePreview($askedWidth, $askedHeight); return; } @@ -908,13 +889,13 @@ private function resizeAndStore($fileId) { $this->cropAndFill( $image, $askedWidth, $askedHeight, $newPreviewWidth, $newPreviewHeight ); - $this->storePreview($fileId, $askedWidth, $askedHeight); + $this->storePreview($askedWidth, $askedHeight); return; } // The preview is smaller, but we can't touch it - $this->storePreview($fileId, $newPreviewWidth, $newPreviewHeight); + $this->storePreview($newPreviewWidth, $newPreviewHeight); } /** @@ -1038,11 +1019,10 @@ private function cropAndFill($image, $askedWidth, $askedHeight, $previewWidth, $ * * Do not nullify the preview as it might send the whole process in a loop * - * @param int $fileId fileId of the original image * @param int $previewWidth * @param int $previewHeight */ - private function storePreview($fileId, $previewWidth, $previewHeight) { + private function storePreview($previewWidth, $previewHeight) { if (empty($previewWidth) || empty($previewHeight)) { \OCP\Util::writeLog( 'core', 'Cannot save preview of dimension ' . $previewWidth . 'x' . $previewHeight, @@ -1050,7 +1030,7 @@ private function storePreview($fileId, $previewWidth, $previewHeight) { ); } else { - $cachePath = $this->buildCachePath($fileId, $previewWidth, $previewHeight); + $cachePath = $this->buildCachePath($previewWidth, $previewHeight); $this->userView->file_put_contents($cachePath, $this->preview->data()); } } @@ -1058,13 +1038,12 @@ private function storePreview($fileId, $previewWidth, $previewHeight) { /** * Returns the path to a preview based on its dimensions and aspect * - * @param int $fileId * @param int|null $maxX * @param int|null $maxY * * @return string */ - private function buildCachePath($fileId, $maxX = null, $maxY = null) { + private function buildCachePath($maxX = null, $maxY = null) { if (is_null($maxX)) { $maxX = $this->getMaxX(); } @@ -1072,7 +1051,7 @@ private function buildCachePath($fileId, $maxX = null, $maxY = null) { $maxY = $this->getMaxY(); } - $previewPath = $this->getPreviewPath($fileId); + $previewPath = $this->getPreviewPath(); $previewPath = $previewPath . strval($maxX) . '-' . strval($maxY); $isMaxPreview = ($maxX === $this->maxPreviewWidth && $maxY === $this->maxPreviewHeight) ? true : false; @@ -1093,11 +1072,14 @@ private function buildCachePath($fileId, $maxX = null, $maxY = null) { /** * Returns the path to the folder where the previews are stored, identified by the fileId * - * @param int $fileId - * * @return string */ - private function getPreviewPath($fileId) { + private function getPreviewPath() { + $fileId = $this->getFileInfo()->getId(); + if ($this->versionId !== null) { + $fileId .= '/'; + $fileId .= $this->versionId; + } return $this->getThumbnailsFolder() . '/' . $fileId . '/'; } @@ -1111,9 +1093,8 @@ private function getPreviewPath($fileId) { * We never upscale the original conversion as this will be done later by the resizing * operation * - * @param int $fileId fileId of the original image */ - private function generatePreview($fileId) { + private function generatePreview() { $file = $this->getFile(); $preview = null; @@ -1131,22 +1112,20 @@ private function generatePreview($fileId) { } \OCP\Util::writeLog( - 'core', 'Generating preview for "' . $file . '" with "' . get_class($provider) + 'core', 'Generating preview for "' . $file->getPath() . '" with "' . get_class($provider) . '"', \OCP\Util::DEBUG ); /** @var $provider Provider */ $preview = $provider->getThumbnail( - $file, $this->configMaxWidth, $this->configMaxHeight, $scalingUp = false, - $this->fileView - ); + $file, $this->configMaxWidth, $this->configMaxHeight, $scalingUp = false); if (!($preview instanceof \OCP\IImage)) { continue; } $this->preview = $preview; - $previewPath = $this->getPreviewPath($fileId); + $previewPath = $this->getPreviewPath(); if ($this->userView->is_dir($this->getThumbnailsFolder() . '/') === false) { $this->userView->mkdir($this->getThumbnailsFolder() . '/'); @@ -1165,7 +1144,7 @@ private function generatePreview($fileId) { // The providers have been kind enough to give us a preview if ($preview) { - $this->resizeAndStore($fileId); + $this->resizeAndStore(); } } @@ -1262,7 +1241,7 @@ public static function prepare_delete(array $args, $prefix = '') { $path = substr($path, 1); } - $view = new \OC\Files\View('/' . \OC_User::getUser() . '/' . $prefix); + $view = new View('/' . \OC_User::getUser() . '/' . $prefix); $absPath = Files\Filesystem::normalizePath($view->getAbsolutePath($path)); $fileInfo = $view->getFileInfo($path); @@ -1285,7 +1264,7 @@ private static function addPathToDeleteFileMapper($absolutePath, $info) { } /** - * @param \OC\Files\View $view + * @param View $view * @param string $path * * @return array diff --git a/lib/private/Preview/Provider.php b/lib/private/Preview/Provider.php index ede6a4781ba6..448071c39477 100644 --- a/lib/private/Preview/Provider.php +++ b/lib/private/Preview/Provider.php @@ -44,24 +44,14 @@ public function __construct(array $options = []) { abstract public function getMimeType(); /** - * Check if a preview can be generated for $path - * - * @param \OCP\Files\FileInfo $file - * @return bool + * @inheritdoc */ public function isAvailable(\OCP\Files\FileInfo $file) { return true; } /** - * Generates thumbnail which fits in $maxX and $maxY and keeps the aspect ratio, for file at path $path - * - * @param string $path Path of 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 - * @param bool $scalingup Disable/Enable upscaling of previews - * @param \OC\Files\View $fileview fileview object of user folder - * @return bool|\OCP\IImage false if no preview was generated + * @inheritdoc */ - abstract public function getThumbnail($path, $maxX, $maxY, $scalingup, $fileview); + abstract public function getThumbnail($path, $maxX, $maxY, $scalingup); } diff --git a/lib/private/Preview/TXT.php b/lib/private/Preview/TXT.php index e984f8adf5c7..39acddebb0ba 100644 --- a/lib/private/Preview/TXT.php +++ b/lib/private/Preview/TXT.php @@ -43,8 +43,8 @@ public function isAvailable(\OCP\Files\FileInfo $file) { /** * {@inheritDoc} */ - public function getThumbnail($path, $maxX, $maxY, $scalingup, $fileview) { - $stream = $fileview->fopen($path, 'r'); + public function getThumbnail($file, $maxX, $maxY, $scalingup) { + $stream = $file->fopen('r'); $content = stream_get_contents($stream,3000); fclose($stream); diff --git a/lib/public/Files/IPreviewNode.php b/lib/public/Files/IPreviewNode.php new file mode 100644 index 000000000000..b849d1eeb6c7 --- /dev/null +++ b/lib/public/Files/IPreviewNode.php @@ -0,0 +1,43 @@ + + * + * @copyright Copyright (c) 2017, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCP\Files; + +use OCP\IImage; + +/** + * Interface IPreviewNode - a node which can generate a preview will implement + * this interface. + * + * @package OCP\Files + * @since 10.1.0 + */ +interface IPreviewNode { + + /** + * Generates a preview image of the node + * + * @param array $options + * @return IImage + * @since 10.1.0 + */ + public function getThumbnail($options); +} diff --git a/lib/public/Preview/IProvider.php b/lib/public/Preview/IProvider.php index ae7c2d22c343..50bf5c325cd1 100644 --- a/lib/public/Preview/IProvider.php +++ b/lib/public/Preview/IProvider.php @@ -21,6 +21,8 @@ */ namespace OCP\Preview; +use OCP\Files\File; + /** * Interface IProvider * @@ -46,13 +48,12 @@ public function isAvailable(\OCP\Files\FileInfo $file); /** * get thumbnail for file at path $path * - * @param string $path Path of file + * @param File $path Path of 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 * @param bool $scalingup Disable/Enable upscaling of previews - * @param \OC\Files\View $fileview fileview object of user folder * @return bool|\OCP\IImage false if no preview was generated * @since 8.1.0 */ - public function getThumbnail($path, $maxX, $maxY, $scalingup, $fileview); + public function getThumbnail($path, $maxX, $maxY, $scalingup); } diff --git a/tests/lib/Files/Node/FileTest.php b/tests/lib/Files/Node/FileTest.php index 6ac5c8f0eb78..fc1b8457f2f9 100644 --- a/tests/lib/Files/Node/FileTest.php +++ b/tests/lib/Files/Node/FileTest.php @@ -7,6 +7,13 @@ */ namespace Test\Files\Node; +use OC\Files\Mount\Manager; +use OC\Files\Node\File; +use OC\Files\Node\NonExistingFile; +use OC\Files\Node\Root; +use OC\Files\View; +use OCP\Constants; +use OCP\IImage; /** * Class FileTest @@ -18,23 +25,21 @@ class FileTest extends NodeTest { public $viewDeleteMethod = 'unlink'; - public $nodeClass = '\OC\Files\Node\File'; - public $nonExistingNodeClass = '\OC\Files\Node\NonExistingFile'; + public $nodeClass = File::class; + public $nonExistingNodeClass = NonExistingFile::class; protected function createTestNode($root, $view, $path) { - return new \OC\Files\Node\File($root, $view, $path); + return new File($root, $view, $path); } public function testGetContent() { + /** @var Manager $manager */ + $manager = $this->createMock(Manager::class); /** - * @var \OC\Files\Mount\Manager $manager + * @var View | \PHPUnit_Framework_MockObject_MockObject $view */ - $manager = $this->createMock('\OC\Files\Mount\Manager'); - /** - * @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject $view - */ - $view = $this->createMock('\OC\Files\View'); - $root = new \OC\Files\Node\Root($manager, $view, $this->user); + $view = $this->createMock(View::class); + $root = new Root($manager, $view, $this->user); $hook = function ($file) { throw new \Exception('Hooks are not supposed to be called'); @@ -51,9 +56,9 @@ public function testGetContent() { $view->expects($this->once()) ->method('getFileInfo') ->with('/bar/foo') - ->will($this->returnValue($this->getFileInfo(['permissions' => \OCP\Constants::PERMISSION_READ]))); + ->will($this->returnValue($this->getFileInfo(['permissions' => Constants::PERMISSION_READ]))); - $node = new \OC\Files\Node\File($root, $view, '/bar/foo'); + $node = new File($root, $view, '/bar/foo'); $this->assertEquals('bar', $node->getContent()); } @@ -61,11 +66,10 @@ public function testGetContent() { * @expectedException \OCP\Files\NotPermittedException */ public function testGetContentNotPermitted() { - /** - * @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject $view - */ - $view = $this->createMock('\OC\Files\View'); - $root = $this->createMock('\OC\Files\Node\Root'); + /** @var View | \PHPUnit_Framework_MockObject_MockObject $view */ + $view = $this->createMock(View::class); + /** @var Root | \PHPUnit_Framework_MockObject_MockObject $root */ + $root = $this->createMock(Root::class); $root->expects($this->any()) ->method('getUser') @@ -76,16 +80,15 @@ public function testGetContentNotPermitted() { ->with('/bar/foo') ->will($this->returnValue($this->getFileInfo(['permissions' => 0]))); - $node = new \OC\Files\Node\File($root, $view, '/bar/foo'); + $node = new File($root, $view, '/bar/foo'); $node->getContent(); } public function testPutContent() { - /** - * @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject $view - */ - $view = $this->createMock('\OC\Files\View'); - $root = $this->createMock('\OC\Files\Node\Root'); + /** @var View | \PHPUnit_Framework_MockObject_MockObject $view */ + $view = $this->createMock(View::class); + /** @var Root | \PHPUnit_Framework_MockObject_MockObject $root */ + $root = $this->createMock(Root::class); $root->expects($this->any()) ->method('getUser') @@ -94,14 +97,14 @@ public function testPutContent() { $view->expects($this->once()) ->method('getFileInfo') ->with('/bar/foo') - ->will($this->returnValue($this->getFileInfo(['permissions' => \OCP\Constants::PERMISSION_ALL]))); + ->will($this->returnValue($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL]))); $view->expects($this->once()) ->method('file_put_contents') ->with('/bar/foo', 'bar') ->will($this->returnValue(true)); - $node = new \OC\Files\Node\File($root, $view, '/bar/foo'); + $node = new File($root, $view, '/bar/foo'); $node->putContent('bar'); } @@ -109,34 +112,32 @@ public function testPutContent() { * @expectedException \OCP\Files\NotPermittedException */ public function testPutContentNotPermitted() { - /** - * @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject $view - */ - $view = $this->createMock('\OC\Files\View'); - $root = $this->createMock('\OC\Files\Node\Root'); + /** @var View | \PHPUnit_Framework_MockObject_MockObject $view */ + $view = $this->createMock(View::class); + /** @var Root | \PHPUnit_Framework_MockObject_MockObject $root */ + $root = $this->createMock(Root::class); $view->expects($this->once()) ->method('getFileInfo') ->with('/bar/foo') - ->will($this->returnValue($this->getFileInfo(['permissions' => \OCP\Constants::PERMISSION_READ]))); + ->will($this->returnValue($this->getFileInfo(['permissions' => Constants::PERMISSION_READ]))); - $node = new \OC\Files\Node\File($root, $view, '/bar/foo'); + $node = new File($root, $view, '/bar/foo'); $node->putContent('bar'); } public function testGetMimeType() { - /** - * @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject $view - */ - $view = $this->createMock('\OC\Files\View'); - $root = $this->createMock('\OC\Files\Node\Root'); + /** @var View | \PHPUnit_Framework_MockObject_MockObject $view */ + $view = $this->createMock(View::class); + /** @var Root | \PHPUnit_Framework_MockObject_MockObject $root */ + $root = $this->createMock(Root::class); $view->expects($this->once()) ->method('getFileInfo') ->with('/bar/foo') ->will($this->returnValue($this->getFileInfo(['mimetype' => 'text/plain']))); - $node = new \OC\Files\Node\File($root, $view, '/bar/foo'); + $node = new File($root, $view, '/bar/foo'); $this->assertEquals('text/plain', $node->getMimeType()); } @@ -146,14 +147,14 @@ public function testFOpenRead() { rewind($stream); /** - * @var \OC\Files\Mount\Manager $manager + * @var Manager $manager */ - $manager = $this->createMock('\OC\Files\Mount\Manager'); + $manager = $this->createMock(Manager::class); /** - * @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject $view + * @var View | \PHPUnit_Framework_MockObject_MockObject $view */ - $view = $this->createMock('\OC\Files\View'); - $root = new \OC\Files\Node\Root($manager, $view, $this->user); + $view = $this->createMock(View::class); + $root = new Root($manager, $view, $this->user); $hook = function ($file) { throw new \Exception('Hooks are not supposed to be called'); @@ -170,9 +171,9 @@ public function testFOpenRead() { $view->expects($this->once()) ->method('getFileInfo') ->with('/bar/foo') - ->will($this->returnValue($this->getFileInfo(['permissions' => \OCP\Constants::PERMISSION_ALL]))); + ->will($this->returnValue($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL]))); - $node = new \OC\Files\Node\File($root, $view, '/bar/foo'); + $node = new File($root, $view, '/bar/foo'); $fh = $node->fopen('r'); $this->assertEquals($stream, $fh); $this->assertEquals('bar', fread($fh, 3)); @@ -182,14 +183,14 @@ public function testFOpenWrite() { $stream = fopen('php://memory', 'w+'); /** - * @var \OC\Files\Mount\Manager $manager + * @var Manager $manager */ - $manager = $this->createMock('\OC\Files\Mount\Manager'); + $manager = $this->createMock(Manager::class); /** - * @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject $view + * @var View | \PHPUnit_Framework_MockObject_MockObject $view */ - $view = $this->createMock('\OC\Files\View'); - $root = new \OC\Files\Node\Root($manager, new $view, $this->user); + $view = $this->createMock(View::class); + $root = new Root($manager, new $view, $this->user); $hooksCalled = 0; $hook = function ($file) use (&$hooksCalled) { @@ -207,9 +208,9 @@ public function testFOpenWrite() { $view->expects($this->once()) ->method('getFileInfo') ->with('/bar/foo') - ->will($this->returnValue($this->getFileInfo(['permissions' => \OCP\Constants::PERMISSION_ALL]))); + ->will($this->returnValue($this->getFileInfo(['permissions' => Constants::PERMISSION_ALL]))); - $node = new \OC\Files\Node\File($root, $view, '/bar/foo'); + $node = new File($root, $view, '/bar/foo'); $fh = $node->fopen('w'); $this->assertEquals($stream, $fh); fwrite($fh, 'bar'); @@ -223,14 +224,14 @@ public function testFOpenWrite() { */ public function testFOpenReadNotPermitted() { /** - * @var \OC\Files\Mount\Manager $manager + * @var Manager $manager */ - $manager = $this->createMock('\OC\Files\Mount\Manager'); + $manager = $this->createMock(Manager::class); /** - * @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject $view + * @var View | \PHPUnit_Framework_MockObject_MockObject $view */ - $view = $this->createMock('\OC\Files\View'); - $root = new \OC\Files\Node\Root($manager, $view, $this->user); + $view = $this->createMock(View::class); + $root = new Root($manager, $view, $this->user); $hook = function ($file) { throw new \Exception('Hooks are not supposed to be called'); @@ -241,7 +242,7 @@ public function testFOpenReadNotPermitted() { ->with('/bar/foo') ->will($this->returnValue($this->getFileInfo(['permissions' => 0]))); - $node = new \OC\Files\Node\File($root, $view, '/bar/foo'); + $node = new File($root, $view, '/bar/foo'); $node->fopen('r'); } @@ -250,14 +251,14 @@ public function testFOpenReadNotPermitted() { */ public function testFOpenReadWriteNoReadPermissions() { /** - * @var \OC\Files\Mount\Manager $manager + * @var Manager $manager */ - $manager = $this->createMock('\OC\Files\Mount\Manager'); + $manager = $this->createMock(Manager::class); /** - * @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject $view + * @var View | \PHPUnit_Framework_MockObject_MockObject $view */ - $view = $this->createMock('\OC\Files\View'); - $root = new \OC\Files\Node\Root($manager, $view, $this->user); + $view = $this->createMock(View::class); + $root = new Root($manager, $view, $this->user); $hook = function () { throw new \Exception('Hooks are not supposed to be called'); @@ -266,9 +267,9 @@ public function testFOpenReadWriteNoReadPermissions() { $view->expects($this->once()) ->method('getFileInfo') ->with('/bar/foo') - ->will($this->returnValue($this->getFileInfo(['permissions' => \OCP\Constants::PERMISSION_UPDATE]))); + ->will($this->returnValue($this->getFileInfo(['permissions' => Constants::PERMISSION_UPDATE]))); - $node = new \OC\Files\Node\File($root, $view, '/bar/foo'); + $node = new File($root, $view, '/bar/foo'); $node->fopen('w'); } @@ -277,14 +278,14 @@ public function testFOpenReadWriteNoReadPermissions() { */ public function testFOpenReadWriteNoWritePermissions() { /** - * @var \OC\Files\Mount\Manager $manager + * @var Manager $manager */ - $manager = $this->createMock('\OC\Files\Mount\Manager'); + $manager = $this->createMock(Manager::class); /** - * @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject $view + * @var View | \PHPUnit_Framework_MockObject_MockObject $view */ - $view = $this->createMock('\OC\Files\View'); - $root = new \OC\Files\Node\Root($manager, new $view, $this->user); + $view = $this->createMock(View::class); + $root = new Root($manager, new $view, $this->user); $hook = function () { throw new \Exception('Hooks are not supposed to be called'); @@ -293,11 +294,48 @@ public function testFOpenReadWriteNoWritePermissions() { $view->expects($this->once()) ->method('getFileInfo') ->with('/bar/foo') - ->will($this->returnValue($this->getFileInfo(['permissions' => \OCP\Constants::PERMISSION_READ]))); + ->will($this->returnValue($this->getFileInfo(['permissions' => Constants::PERMISSION_READ]))); - $node = new \OC\Files\Node\File($root, $view, '/bar/foo'); + $node = new File($root, $view, '/bar/foo'); $node->fopen('w'); } + public function testThumbnail() { + /** @var Manager $manager */ + $manager = $this->createMock(Manager::class); + /** + * @var View | \PHPUnit_Framework_MockObject_MockObject $view + */ + $view = $this->createMock(View::class); + $root = new Root($manager, $view, $this->user); + + $hook = function ($file) { + throw new \Exception('Hooks are not supposed to be called'); + }; + + $root->listen('\OC\Files', 'preWrite', $hook); + $root->listen('\OC\Files', 'postWrite', $hook); + + $content = $stream = fopen('data://text/plain,hello world!','r');; + $view->expects($this->once()) + ->method('fopen') + ->with('/bar/foo') + ->will($this->returnValue($content)); + + $view->expects($this->once()) + ->method('getFileInfo') + ->with('/bar/foo') + ->will($this->returnValue($this->getFileInfo([ + 'permissions' => Constants::PERMISSION_READ, + 'mimetype' => 'text/plain', + 'fileid' => 666 + ]))); + + $node = new File($root, $view, '/bar/foo'); + $image = $node->getThumbnail([]); + $this->assertInstanceOf(IImage::class, $image); + $this->assertEquals(32, $image->height()); + $this->assertEquals(32, $image->width()); + } }