diff options
-rw-r--r-- | appinfo/application.php | 75 | ||||
-rw-r--r-- | appinfo/routes.php | 24 | ||||
-rw-r--r-- | controller/filescontroller.php | 129 | ||||
-rw-r--r-- | controller/previewcontroller.php | 269 | ||||
-rw-r--r-- | controller/publicfilescontroller.php | 36 | ||||
-rw-r--r-- | controller/publicpreviewcontroller.php | 89 | ||||
-rw-r--r-- | controller/publicservicecontroller.php | 6 | ||||
-rw-r--r-- | controller/servicecontroller.php | 67 | ||||
-rw-r--r-- | environment/environment.php | 29 | ||||
-rw-r--r-- | js/album.js | 2 | ||||
-rw-r--r-- | js/gallery.js | 37 | ||||
-rw-r--r-- | js/slideshow.js | 28 | ||||
-rw-r--r-- | service/configservice.php | 183 | ||||
-rw-r--r-- | service/downloadservice.php | 50 | ||||
-rw-r--r-- | service/filesservice.php | 294 | ||||
-rw-r--r-- | service/infoservice.php | 459 | ||||
-rw-r--r-- | service/previewservice.php | 91 | ||||
-rw-r--r-- | service/service.php | 83 | ||||
-rw-r--r-- | service/thumbnailservice.php | 9 |
19 files changed, 1404 insertions, 556 deletions
diff --git a/appinfo/application.php b/appinfo/application.php index 16235903..8313bec4 100644 --- a/appinfo/application.php +++ b/appinfo/application.php @@ -20,11 +20,14 @@ use OCP\AppFramework\App; use OCP\AppFramework\IAppContainer; use OCA\GalleryPlus\Controller\PageController; -use OCA\GalleryPlus\Controller\ServiceController; -use OCA\GalleryPlus\Controller\PublicServiceController; +use OCA\GalleryPlus\Controller\FilesController; +use OCA\GalleryPlus\Controller\PreviewController; +use OCA\GalleryPlus\Controller\PublicFilesController; +use OCA\GalleryPlus\Controller\PublicPreviewController; use OCA\GalleryPlus\Environment\Environment; use OCA\GalleryPlus\Preview\Preview; -use OCA\GalleryPlus\Service\InfoService; +use OCA\GalleryPlus\Service\FilesService; +use OCA\GalleryPlus\Service\ConfigService; use OCA\GalleryPlus\Service\ThumbnailService; use OCA\GalleryPlus\Service\PreviewService; use OCA\GalleryPlus\Service\DownloadService; @@ -64,32 +67,52 @@ class Application extends App { } ); $container->registerService( - 'ServiceController', function (IContainer $c) { - return new ServiceController( + 'FilesController', function (IContainer $c) { + return new FilesController( $c->query('AppName'), $c->query('Request'), - $c->query('Environment'), - $c->query('InfoService'), + $c->query('FilesService'), + $c->query('ConfigService'), + $c->query('SmarterLogger') + ); + } + ); + $container->registerService( + 'PublicFilesController', function (IContainer $c) { + return new PublicFilesController( + $c->query('AppName'), + $c->query('Request'), + $c->query('FilesService'), + $c->query('ConfigService'), + $c->query('SmarterLogger') + ); + } + ); + $container->registerService( + 'PreviewController', function (IContainer $c) { + return new PreviewController( + $c->query('AppName'), + $c->query('Request'), + $c->query('OCP\IURLGenerator'), $c->query('ThumbnailService'), $c->query('PreviewService'), $c->query('DownloadService'), - $c->query('OCP\IURLGenerator'), - $c->query('OCP\IEventSource') + $c->query('OCP\IEventSource'), + $c->query('SmarterLogger') ); } ); $container->registerService( - 'PublicServiceController', function (IContainer $c) { - return new PublicServiceController( + 'PublicPreviewController', function (IContainer $c) { + return new PublicPreviewController( $c->query('AppName'), $c->query('Request'), - $c->query('Environment'), - $c->query('InfoService'), + $c->query('OCP\IURLGenerator'), $c->query('ThumbnailService'), $c->query('PreviewService'), $c->query('DownloadService'), - $c->query('OCP\IURLGenerator'), - $c->query('OCP\IEventSource') + $c->query('OCP\IEventSource'), + $c->query('SmarterLogger') ); } ); @@ -98,12 +121,12 @@ class Application extends App { * Core */ $container->registerService( - 'OCP\IServerContainer', function ($c) { + 'OCP\IServerContainer', function (IAppContainer $c) { return $c->getServer(); } ); $container->registerService( - 'OCP\IEventSource', function ($c) { + 'OCP\IEventSource', function (IAppContainer $c) { return $c->getServer() ->createEventSource(); } @@ -186,27 +209,31 @@ class Application extends App { * Services */ $container->registerService( - 'InfoService', function (IContainer $c) { - return new InfoService( + 'FilesService', function (IContainer $c) { + return new FilesService( $c->query('AppName'), - $c->query('PreviewService'), + $c->query('Environment'), $c->query('SmarterLogger') ); } ); $container->registerService( - 'ThumbnailService', function (IAppContainer $c) { - return new ThumbnailService( + 'ConfigService', function (IContainer $c) { + return new ConfigService( $c->query('AppName'), $c->query('Environment'), - $c->query('CustomPreviewManager'), $c->query('SmarterLogger') ); } ); $container->registerService( + 'ThumbnailService', function () { + return new ThumbnailService(); + } + ); + $container->registerService( 'PreviewService', function (IContainer $c) { return new PreviewService( $c->query('AppName'), @@ -259,7 +286,7 @@ class Application extends App { } ); - // executed in the order that it is registered + // Executed in the order that it is registered $container->registerMiddleware('SharingCheckMiddleware'); $container->registerMiddleware('EnvCheckMiddleware'); diff --git a/appinfo/routes.php b/appinfo/routes.php index 00edbfd0..579d61bf 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -61,31 +61,31 @@ return [ */ // Supported media types. Only called by the slideshow [ - 'name' => 'service#get_types', - 'url' => '/mimetypes', + 'name' => 'preview#get_media_types', + 'url' => '/mediatypes', 'verb' => 'GET' ], // All the images of which a preview can be generated [ - 'name' => 'service#get_files', + 'name' => 'files#get_files', 'url' => '/files', 'verb' => 'GET' ], // Batch creation of thumbnails [ - 'name' => 'service#get_thumbnails', + 'name' => 'preview#get_thumbnails', 'url' => '/thumbnails', 'verb' => 'GET' ], // Large preview of a file [ - 'name' => 'service#show_preview', + 'name' => 'preview#show_preview', 'url' => '/preview', 'verb' => 'GET' ], // Download the file [ - 'name' => 'service#download_preview', + 'name' => 'preview#download_preview', 'url' => '/download', 'verb' => 'GET' ], @@ -93,27 +93,27 @@ return [ * Public services */ [ - 'name' => 'public_service#get_types', - 'url' => '/mimetypes.public', + 'name' => 'public_preview#get_media_types', + 'url' => '/mediatypes.public', 'verb' => 'GET' ], [ - 'name' => 'public_service#get_files', + 'name' => 'public_files#get_files', 'url' => '/files.public', 'verb' => 'GET' ], [ - 'name' => 'public_service#get_thumbnails', + 'name' => 'public_preview#get_thumbnails', 'url' => '/thumbnails.public', 'verb' => 'GET' ], [ - 'name' => 'public_service#show_preview', + 'name' => 'public_preview#show_preview', 'url' => '/preview.public', 'verb' => 'GET' ], [ - 'name' => 'public_service#download_preview', + 'name' => 'public_preview#download_preview', 'url' => '/download.public', 'verb' => 'GET' ], diff --git a/controller/filescontroller.php b/controller/filescontroller.php new file mode 100644 index 00000000..7e6c032b --- /dev/null +++ b/controller/filescontroller.php @@ -0,0 +1,129 @@ +<?php +/** + * ownCloud - galleryplus + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Olivier Paroz <owncloud@interfasys.ch> + * @author Robin Appelman <icewind@owncloud.com> + * + * @copyright Olivier Paroz 2014-2015 + * @copyright Robin Appelman 2012-2014 + */ + +namespace OCA\GalleryPlus\Controller; + +use OCP\IRequest; +use OCP\Files\Folder; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; + +use OCA\GalleryPlus\Service\FilesService; +use OCA\GalleryPlus\Service\ConfigService; +use OCA\GalleryPlus\Utility\SmarterLogger; + +/** + * Class FilesController + * + * @package OCA\GalleryPlus\Controller + */ +class FilesController extends Controller { + + use JsonHttpError; + + /** + * @type FilesService + */ + private $filesService; + /** + * @type ConfigService + */ + private $configService; + + /** + * Constructor + * + * @param string $appName + * @param IRequest $request + * @param FilesService $filesService + * @param ConfigService $configService + * @param SmarterLogger $logger + */ + public function __construct( + $appName, + IRequest $request, + FilesService $filesService, + ConfigService $configService, + SmarterLogger $logger + ) { + parent::__construct($appName, $request); + + $this->filesService = $filesService; + $this->configService = $configService; + //$this->logger = $logger; + } + + /** + * @NoAdminRequired + * + * Returns a list of all media files available to the authenticated user + * + * Authentication can be via a login/password or a token/(password) + * + * For private galleries, it returns all media files, with the full path + * from the root folder + * For public galleries, the path starts from the folder the link + * gives access to (virtual root) + * + * An exception is only caught in case something really wrong happens. As we don't test files + * before including them in the list, we may return some bad apples + * + * @param string $location a path representing the current album in the app + * + * @return array<string,array<string,string|int>>|Http\JSONResponse + */ + public function getFiles($location) { + $mediaTypesArray = explode(';', $this->request->getParam('mediatypes')); + try { + /** @type Folder $folderNode */ + list($folderPathFromRoot, $folderNode, $locationHasChanged) = + $this->filesService->getCurrentFolder(rawurldecode($location)); + if (is_null($folderNode)) { + // Something very wrong has just happened + return new JSONResponse(['message' => 'Oh Nooooes!', 'success' => false], 500); + } + list($albumInfo, $privateAlbum) = + $this->configService->getAlbumInfo($folderNode, $folderPathFromRoot); + if ($privateAlbum) { + return new JSONResponse(['message' => 'Album is private', 'success' => false], 403); + } + $files = $this->filesService->getMediaFiles($folderNode, $mediaTypesArray); + + return $this->formatResults($files, $albumInfo, $locationHasChanged); + } catch (\Exception $exception) { + return $this->error($exception); + } + } + + /** + * Simply builds and returns an array containing the list of files, the album information and + * whether the location has changed or not + * + * @param $files + * @param $albumInfo + * @param $locationHasChanged + * + * @return array + */ + private function formatResults($files, $albumInfo, $locationHasChanged) { + return [ + 'files' => $files, + 'albuminfo' => $albumInfo, + 'locationhaschanged' => $locationHasChanged + ]; + } + +} diff --git a/controller/previewcontroller.php b/controller/previewcontroller.php new file mode 100644 index 00000000..57f2afc7 --- /dev/null +++ b/controller/previewcontroller.php @@ -0,0 +1,269 @@ +<?php +/** + * ownCloud - galleryplus + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Olivier Paroz <owncloud@interfasys.ch> + * @author Robin Appelman <icewind@owncloud.com> + * + * @copyright Olivier Paroz 2014-2015 + * @copyright Robin Appelman 2012-2014 + */ + +namespace OCA\GalleryPlus\Controller; + +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\IEventSource; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; + +use OCA\GalleryPlus\Http\ImageResponse; +use OCA\GalleryPlus\Service\ServiceException; +use OCA\GalleryPlus\Service\ThumbnailService; +use OCA\GalleryPlus\Service\PreviewService; +use OCA\GalleryPlus\Service\DownloadService; +use OCA\GalleryPlus\Utility\SmarterLogger; + +/** + * Class PreviewController + * + * @package OCA\GalleryPlus\Controller + */ +class PreviewController extends Controller { + + use JsonHttpError; + + /** + * @type IURLGenerator + */ + private $urlGenerator; + /** + * @type ThumbnailService + */ + private $thumbnailService; + /** + * @type PreviewService + */ + private $previewService; + /** + * @type DownloadService + */ + private $downloadService; + /** + * @type IEventSource + */ + private $eventSource; + /** + * @type SmarterLogger + */ + private $logger; + + /** + * Constructor + * + * @param string $appName + * @param IRequest $request + * @param IURLGenerator $urlGenerator + * @param ThumbnailService $thumbnailService + * @param PreviewService $previewService + * @param DownloadService $downloadService + * @param IEventSource $eventSource + * @param SmarterLogger $logger + */ + public function __construct( + $appName, + IRequest $request, + IURLGenerator $urlGenerator, + ThumbnailService $thumbnailService, + PreviewService $previewService, + DownloadService $downloadService, + IEventSource $eventSource, + SmarterLogger $logger + ) { + parent::__construct($appName, $request); + + $this->urlGenerator = $urlGenerator; + $this->thumbnailService = $thumbnailService; + $this->previewService = $previewService; + $this->downloadService = $downloadService; + $this->eventSource = $eventSource; + $this->logger = $logger; + } + + /** + * @NoAdminRequired + * + * Sends back a list of all media types supported by the system, as well as the name of their + * icon + * + * @param bool $slideshow + * + * @return array <string,string>|null + */ + public function getMediaTypes($slideshow = false) { + return $this->previewService->getSupportedMediaTypes($slideshow); + } + + /** + * @NoAdminRequired + * + * Generates thumbnails + * + * Uses EventSource to send thumbnails back as soon as they're created + * + * FIXME: @LukasReschke says: The exit is required here because + * otherwise the AppFramework is trying to add headers as well after + * dispatching the request which results in a "Cannot modify header + * information" notice. + * + * WARNING: Returning a JSON response does not get rid of the problem + * + * @param string $images + * @param bool $square + * @param bool $scale + * + * @return array<string,array|string> + */ + public function getThumbnails($images, $square, $scale) { + $imagesArray = explode(';', $images); + + foreach ($imagesArray as $image) { + $thumbnail = $this->getThumbnail($image, $square, $scale); + $this->eventSource->send('preview', $thumbnail); + } + $this->eventSource->close(); + + exit(); + } + + /** + * @NoAdminRequired + * + * Sends either a large preview of the requested file or the + * original file itself + * + * If the browser can use the file as-is then we simply let + * the browser download the file, straight from the filesystem + * + * @param string $file + * @param int $x + * @param int $y + * + * @return ImageResponse|Http\JSONResponse + */ + public function showPreview($file, $x, $y) { + try { + $preview = $this->getPreview($file, $x, $y); + + return new ImageResponse($preview['data'], $preview['status']); + } catch (ServiceException $exception) { + return $this->error($exception); + } + } + + /** + * @NoAdminRequired + * + * Downloads the file + * + * @param string $file + * + * @return \OCA\GalleryPlus\Http\ImageResponse|Http\JSONResponse + */ + public function downloadPreview($file) { + try { + $download = $this->downloadService->downloadFile($file); + + return new ImageResponse($download); + } catch (ServiceException $exception) { + return $this->error($exception); + } + } + + /** + * Retrieves the thumbnail to send back to the browser + * + * The thumbnail is either a resized preview of the file or the original file + * Thumbnails are base64encoded before getting sent back + * + * @param string $image + * @param bool $square + * @param bool $scale + * + * @return array<string,array|string> + */ + private function getThumbnail($image, $square, $scale) { + list($width, $height, $aspect, $animatedPreview, $base64Encode) = + $this->thumbnailService->getThumbnailSpecs($square, $scale); + + try { + $preview = $this->getPreview( + $image, $width, $height, $aspect, $animatedPreview, $base64Encode + ); + } catch (ServiceException $exception) { + $preview = ['data' => null, 'status' => 500, 'type' => 'error']; + } + $thumbnail = $preview['data']; + if ($preview['status'] === 200 && $preview['type'] === 'preview') { + $thumbnail['preview'] = $this->previewService->previewValidator($square, $base64Encode); + } + $thumbnail['status'] = $preview['status']; + + return $thumbnail; + } + + /** + * Returns either a generated preview (or the mime-icon when the preview generation fails) + * or the file as-is + * + * Sample logger + * We can't just send the preview array as it can contain quite a large data stream + * $this->logger->debug("[Batch] THUMBNAIL NAME : {image} / PATH : {path} / + * MIME : {mimetype} / DATA : {preview}", [ + * 'image' => $preview['data']['image'], + * 'path' => $preview['data']['path'], + * 'mimetype' => $preview['data']['mimetype'], + * 'preview' => substr($preview['data']['preview'], 0, 20), + * ] + * ); + * + * @param string $image + * @param int $width + * @param int $height + * @param bool $keepAspect + * @param bool $animatedPreview + * @param bool $base64Encode + * + * @return array<string,\OC_Image|string> + */ + private function getPreview( + $image, $width, $height, $keepAspect = true, $animatedPreview = true, $base64Encode = false + ) { + $status = Http::STATUS_OK; + $previewRequired = $this->previewService->isPreviewRequired($image, $animatedPreview); + if ($previewRequired) { + $type = 'preview'; + $preview = $this->previewService->createPreview( + $image, $width, $height, $keepAspect, $base64Encode + ); + if (!$this->previewService->isPreviewValid()) { + $type = 'error'; + $status = Http::STATUS_NOT_FOUND; + } + } else { + $type = 'download'; + $preview = $this->downloadService->downloadFile($image, $base64Encode); + } + + return [ + 'data' => $preview, + 'status' => $status, + 'type' => $type + ]; + } + +} diff --git a/controller/publicfilescontroller.php b/controller/publicfilescontroller.php new file mode 100644 index 00000000..ab48b2ee --- /dev/null +++ b/controller/publicfilescontroller.php @@ -0,0 +1,36 @@ +<?php +/** + * ownCloud - galleryplus + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Olivier Paroz <owncloud@interfasys.ch> + * + * @copyright Olivier Paroz 2014-2015 + */ + +namespace OCA\GalleryPlus\Controller; + +/** + * Class PublicFilesController + * + * Note: Type casting only works if the "@param" parameters are also included in this class as + * their not yet inherited + * + * @package OCA\GalleryPlus\Controller + */ +class PublicFilesController extends FilesController { + + /** + * @PublicPage + * + * Returns a list of all images from the folder the link gives access to + * + * @inheritDoc + */ + public function getFiles($location) { + return parent::getFiles($location); + } + +} diff --git a/controller/publicpreviewcontroller.php b/controller/publicpreviewcontroller.php new file mode 100644 index 00000000..2a6d57f3 --- /dev/null +++ b/controller/publicpreviewcontroller.php @@ -0,0 +1,89 @@ +<?php +/** + * ownCloud - galleryplus + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Olivier Paroz <owncloud@interfasys.ch> + * + * @copyright Olivier Paroz 2014-2015 + */ + +namespace OCA\GalleryPlus\Controller; + +/** + * Class PublicPreviewController + * + * Note: Type casting only works if the "@param" parameters are also included in this class as + * their not yet inherited + * + * @package OCA\GalleryPlus\Controller + */ +class PublicPreviewController extends PreviewController { + + /** + * @PublicPage + * + * @inheritDoc + * + * @param bool $slideshow + */ + public function getMediaTypes($slideshow = false) { + return parent::getMediaTypes($slideshow); + } + + /** + * @PublicPage + * @UseSession + * + * Generates thumbnails for public galleries + * + * The session needs to be maintained open or previews can't be generated + * for files located on encrypted storage + * + * @inheritDoc + * + * @param string $images + * @param bool $square + * @param bool $scale + */ + public function getThumbnails($images, $square, $scale) { + return parent::getThumbnails($images, $square, $scale); + } + + /** + * @PublicPage + * @UseSession + * + * Shows a large preview of a file + * + * The session needs to be maintained open or previews can't be generated + * for files located on encrypted storage + * + * @inheritDoc + * + * @param string $file + * @param int $x + * @param int $y + */ + public function showPreview($file, $x, $y) { + return parent::showPreview($file, $x, $y); + } + + /** + * @PublicPage + * @UseSession + * + * Downloads the file + * + * The session needs to be maintained open or previews can't be generated + * for files located on encrypted storage + * + * @inheritDoc + */ + public function downloadPreview($file) { + return parent::downloadPreview($file); + } + +} diff --git a/controller/publicservicecontroller.php b/controller/publicservicecontroller.php index d5587fb3..c5142567 100644 --- a/controller/publicservicecontroller.php +++ b/controller/publicservicecontroller.php @@ -35,8 +35,8 @@ class PublicServiceController extends ServiceController { * * @inheritDoc */ - public function getFiles($location) { - return parent::getFiles($location); + public function getImages() { + return parent::getImages(); } /** @@ -84,4 +84,4 @@ class PublicServiceController extends ServiceController { return parent::downloadPreview($file); } -} +}
\ No newline at end of file diff --git a/controller/servicecontroller.php b/controller/servicecontroller.php index 6243cfbd..ebab195b 100644 --- a/controller/servicecontroller.php +++ b/controller/servicecontroller.php @@ -20,7 +20,6 @@ use OCP\IRequest; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; -use OCP\AppFramework\Http\JSONResponse; use OCA\GalleryPlus\Environment\Environment; use OCA\GalleryPlus\Environment\EnvironmentException; @@ -107,6 +106,28 @@ class ServiceController extends Controller { /** * @NoAdminRequired * + * Returns information about an album, based on its path + * + * Used to see if album thumbnails should be generated for a specific folder + * + * @param string $albumpath + * + * @return false|array<string,int>|Http\JSONResponse + */ + public function getAlbumInfo($albumpath) { + try { + $nodeInfo = $this->environment->getNodeInfo($albumpath); + + // Thanks to the AppFramework, Arrays are automatically JSON encoded + return $nodeInfo; + } catch (EnvironmentException $exception) { + return $this->error($exception); + } + } + + /** + * @NoAdminRequired + * * Sends back a list of all media types supported by the system * * @return string[] @@ -125,27 +146,19 @@ class ServiceController extends Controller { * For private galleries, it returns all images, with the full path from the root folder * For public galleries, the path starts from the folder the link gives access to * - * @param string $location a path representing the current album in the app - * - * @return array<string,array<string,string|int>>|Http\JSONResponse + * @return array|Http\JSONResponse */ - public function getFiles($location) { + public function getImages() { try { - $imagesFolder = $this->environment->getResourceFromPath($location); + $imagesFolder = $this->environment->getResourceFromPath(''); $fromRootToFolder = $this->environment->getFromRootToFolder(); - list($albumInfo, $privateAlbum) = - $this->infoService->getAlbumInfo($imagesFolder, $fromRootToFolder); - if ($privateAlbum) { - return new JSONResponse(['message' => 'Album is private', 'success' => false], 403); - } $folderData = [ 'imagesFolder' => $imagesFolder, 'fromRootToFolder' => $fromRootToFolder, ]; - $files = $this->infoService->getImages($folderData); - return ['files' => $files, 'albuminfo' => $albumInfo]; + return $this->infoService->getImages($folderData); } catch (EnvironmentException $exception) { return $this->error($exception); } @@ -221,7 +234,7 @@ class ServiceController extends Controller { $download = $this->downloadService->downloadFile($file); return new ImageResponse($download); - } catch (ServiceException $exception) { + } catch (EnvironmentException $exception) { return $this->error($exception); } } @@ -236,23 +249,19 @@ class ServiceController extends Controller { * @param bool $square * @param bool $scale * - * @return array<string,array|string> + * @return array|Http\JSONResponse */ private function getThumbnail($image, $square, $scale) { - list($width, $height, $aspect, $animatedPreview, $base64Encode) = - $this->thumbnailService->getThumbnailSpecs($square, $scale); - + $thumbSpecs = $this->thumbnailService->getThumbnailSpecs($square, $scale); try { $preview = $this->getPreview( - $image, $width, $height, $aspect, $animatedPreview, $base64Encode + $image, $thumbSpecs['width'], $thumbSpecs['height'], + $thumbSpecs['aspect'], $thumbSpecs['animatedPreview'], $thumbSpecs['base64Encode'] ); } catch (ServiceException $exception) { - $preview = ['data' => null, 'status' => 500, 'type' => 'error']; + return $this->error($exception); } $thumbnail = $preview['data']; - if ($preview['status'] === 200 && $preview['type'] === 'preview') { - $thumbnail['preview'] = $this->previewService->previewValidator($square, $base64Encode); - } $thumbnail['status'] = $preview['status']; return $thumbnail; @@ -280,7 +289,7 @@ class ServiceController extends Controller { * @param bool $animatedPreview * @param bool $base64Encode * - * @return array<string,\OC_Image|string> + * @return mixed */ private function getPreview( $image, $width, $height, $keepAspect = true, $animatedPreview = true, $base64Encode = false @@ -288,24 +297,20 @@ class ServiceController extends Controller { $status = Http::STATUS_OK; $previewRequired = $this->previewService->isPreviewRequired($image, $animatedPreview); if ($previewRequired) { - $type = 'preview'; $preview = $this->previewService->createPreview( $image, $width, $height, $keepAspect, $base64Encode ); if (!$this->previewService->isPreviewValid()) { - $type = 'error'; - $status = Http::STATUS_NOT_FOUND; + $status = Http::STATUS_UNSUPPORTED_MEDIA_TYPE; } } else { - $type = 'download'; $preview = $this->downloadService->downloadFile($image, $base64Encode); } return [ 'data' => $preview, - 'status' => $status, - 'type' => $type + 'status' => $status ]; } -} +}
\ No newline at end of file diff --git a/environment/environment.php b/environment/environment.php index 776114da..bc2147eb 100644 --- a/environment/environment.php +++ b/environment/environment.php @@ -129,7 +129,7 @@ class Environment { * Creates the environment for a logged-in user * * userId and userFolder are already known, we define fromRootToFolder - * so that the services can use one method to have access resources + * so that the services can use one method to have access to resources * without having to know whether they're private or public */ public function setStandardEnv() { @@ -139,8 +139,8 @@ class Environment { /** * Returns the resource located at the given path * - * The path starts from the user's files folder - * The resource is either a File or a Folder + * The path starts from the user's files folder because we'll query that folder to get the + * information we need. The resource is either a File or a Folder * * @param string $subPath * @@ -254,14 +254,27 @@ class Environment { } /** - * Returns fromRootToFolder + * Returns the path which goes from the file, up to the root folder of the Gallery: + * current_folder/my_file * - * @see buildFromRootToFolder + * That root folder changes when folders are shared publicly + * + * @param Node $file * * @return string */ - public function getFromRootToFolder() { - return $this->fromRootToFolder; + public function getPathFromVirtualRoot($file) { + $path = $file->getPath(); + + if ($file->getType() === 'dir') { + // Needed because fromRootToFolder always ends with a slash + $path .= '/'; + } + + $path = str_replace($this->fromRootToFolder, '', $path); + $path = rtrim($path, '/'); + + return $path; } /** @@ -291,6 +304,8 @@ class Environment { * Returns the path from the shared folder to the root folder in the original * owner's filesystem: /userId/files/parent_folder/shared_folder * + * This cannot be calculated with paths and IDs, the linkitem's file source is required + * * @param string $fileSource * * @return string diff --git a/js/album.js b/js/album.js index 38a31c23..4c89f030 100644 --- a/js/album.js +++ b/js/album.js @@ -212,6 +212,7 @@ Album.prototype = { * @returns {$.Deferred<Row>} */ getNextRow: function (width) { + var numberOfThumbnailsToPreload = 6; /** * Add images to the row until it's full * @@ -224,7 +225,6 @@ Album.prototype = { * @returns {$.Deferred<Row>} */ var addRowElements = function (album, row, images) { - var numberOfThumbnailsToPreload = 6; if ((album.viewedItems + 5) > album.preloadOffset) { album._preload(numberOfThumbnailsToPreload); } diff --git a/js/gallery.js b/js/gallery.js index 76a108f8..6422d39e 100644 --- a/js/gallery.js +++ b/js/gallery.js @@ -1,5 +1,6 @@ /* global OC, $, _, t, Album, GalleryImage, SlideShow, oc_requesttoken, marked */ var Gallery = {}; +Gallery.mediaTypes = {}; Gallery.images = []; Gallery.currentAlbum = null; Gallery.users = []; @@ -11,6 +12,20 @@ Gallery.token = undefined; Gallery.currentSort = {}; /** + * Returns a list of supported media types + * + * @returns {string} + */ +Gallery.getMediaTypes = function () { + var types = ''; + for (var i = 0, keys = Object.keys(Gallery.mediaTypes); i < keys.length; i++) { + types += keys[i] + ';'; + } + + return types.slice(0, -1); +}; + +/** * Builds a map of the albums located in the current folder * * @param {string} path @@ -63,8 +78,13 @@ Gallery.fillAlbums = function () { Gallery.images = []; Gallery.albumMap = {}; Gallery.imageMap = {}; - var currentFolder = decodeURI(window.location.href.split('#')[1] || ''); - var url = Gallery.buildUrl('files', '', {location: currentFolder}); + var currentLocation = decodeURI(window.location.href.split('#')[1] || ''); + var params = { + location: currentLocation, + mediatypes: Gallery.getMediaTypes() + }; + // Only use the folder as a GET parameter and not as part of the URL + var url = Gallery.buildUrl('files', '', params); return $.getJSON(url).then(function (data) { var path = null; var fileId = null; @@ -738,9 +758,16 @@ $(document).ready(function () { oc_requesttoken = Gallery.view.element.data('requesttoken'); } - Gallery.fillAlbums().then(function () { - window.onhashchange(); - }); + $.getJSON(Gallery.buildUrl('mediatypes', '', {})) + .then(function (mediaTypes) { + //console.log('mediaTypes', mediaTypes); + Gallery.mediaTypes = mediaTypes; + }) + .then(function () { + Gallery.fillAlbums().then(function () { + window.onhashchange(); + }); + }); $('#openAsFileListButton').click(function () { var subUrl = ''; diff --git a/js/slideshow.js b/js/slideshow.js index 2cd68225..9d9fda9c 100644 --- a/js/slideshow.js +++ b/js/slideshow.js @@ -29,6 +29,8 @@ var SlideShow = function (container, images, interval, maxScale) { this.smallImageScale = 2; }; +SlideShow.mediaTypes = {}; + SlideShow.prototype = { /** * @@ -338,7 +340,7 @@ SlideShow.prototype = { } }.bind(this); if (mimeType === 'image/svg+xml') { - image.src = this.getSVG(url); + image.src = this._getSVG(url); } else { image.src = url; } @@ -352,7 +354,7 @@ SlideShow.prototype = { * * @returns {*} */ - getSVG: function (source) { + _getSVG: function (source) { var xmlHttp = new XMLHttpRequest(); xmlHttp.open("GET", source, false); xmlHttp.send(null); @@ -678,19 +680,19 @@ $(document).ready(function () { }); }; - var url = SlideShow.buildUrl('mimetypes', {}); - // We're asking for a list of supported mimes. Images are given through the context - $.getJSON(url).then(function (supportedMimes) { - - //console.log("enabledPreviewProviders: ", supportedMimes); + var url = SlideShow.buildUrl('mediatypes', '', {slideshow: 1}); + // We're asking for a list of supported media types. Media files are retrieved through the + // context + $.getJSON(url).then(function (mediaTypes) { + //console.log("enabledPreviewProviders: ", mediaTypes); + SlideShow.mediaTypes = mediaTypes; // We only want to create slideshows for supported media types - for (var m = 0; m < supportedMimes.length; ++m) { - var mime = supportedMimes[m]; - // Each click handler gets the same function and images array and is responsible to - // load the slideshow - prepareFileActions(mime); - OCA.Files.fileActions.setDefault(mime, 'View'); + for (var i = 0, keys = Object.keys(mediaTypes); i < keys.length; i++) { + // Each click handler gets the same function and images array and + // is responsible to load the slideshow + prepareFileActions(keys[i]); + OCA.Files.fileActions.setDefault(keys[i], 'View'); } }); } diff --git a/service/configservice.php b/service/configservice.php new file mode 100644 index 00000000..34c167e5 --- /dev/null +++ b/service/configservice.php @@ -0,0 +1,183 @@ +<?php +/** + * ownCloud - galleryplus + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Olivier Paroz <owncloud@interfasys.ch> + * + * @copyright Olivier Paroz 2015 + */ + +namespace OCA\GalleryPlus\Service; + +use Symfony\Component\Yaml\Yaml; + +use OCP\Files\Folder; +use OCP\Files\File; + +/** + * Finds configurations files, parses them and returns a configuration array + * + * Checks the current and parent folders for configuration files + * + * @package OCA\GalleryPlus\Service + */ +class ConfigService extends Service { + + /** + * Returns information about the currently selected folder + * + * * privacy setting + * * special configuration + * * permissions + * * ID + * + * @param Folder $folderNode + * @param string $folderPathFromRoot + * + * @return array<string,string|int> + */ + public function getAlbumInfo($folderNode, $folderPathFromRoot) { + $configName = 'gallery.cnf'; + $privacyChecker = '.nomedia'; + $albumInfo = []; + list ($albumConfig, $privateAlbum) = + $this->getAlbumConfig($folderNode, $privacyChecker, $configName); + + if (!$privateAlbum) { + $albumInfo = [ + 'path' => $folderPathFromRoot, + 'fileid' => $folderNode->getID(), + 'permissions' => $folderNode->getPermissions() + ]; + $albumInfo = array_merge($albumInfo, $albumConfig); + } + + return [$albumInfo, $privateAlbum]; + } + + /** + * Returns an album configuration array + * + * @param Folder $folder + * @param string $privacyChecker + * @param string $configName + * @param int $level + * @param array $configArray + * @param bool $configComplete + * + * @return array <null|string,string> + */ + private function getAlbumConfig( + $folder, $privacyChecker, $configName, $level = 0, $configArray = [], + $configComplete = false + ) { + if ($folder->nodeExists($privacyChecker)) { + // Cancel as soon as we find out that the folder is private + return [null, true]; + } + list($configArray, $configComplete) = + $this->parseConfig($folder, $configName, $configArray, $configComplete, $level); + $parentFolder = $folder->getParent(); + $path = $parentFolder->getPath(); + if ($path !== '' && $path !== '/') { + $level++; + + return $this->getAlbumConfig( + $parentFolder, $privacyChecker, $configName, $level, $configArray, $configComplete + ); + } + + // We have reached the root folder + return [$configArray, false]; + } + + /** + * Returns a parsed configuration if one was found in the current folder + * + * @param Folder $folder + * @param string $configName + * @param array $currentConfigArray + * @param bool $configComplete + * @param int $level + * + * @return array<array,bool> + */ + private function parseConfig( + $folder, $configName, $currentConfigArray, $configComplete, $level + ) { + $configArray = $currentConfigArray; + // Let's try to find the missing information in the configuration located in this folder + if (!$configComplete && $folder->nodeExists($configName)) { + /** @type File $configFile */ + $configFile = $folder->get($configName); + try { + $rawConfig = $configFile->getContent(); + $saneConfig = $this->bomFixer($rawConfig); + $parsedConfigArray = Yaml::parse($saneConfig); + list($configArray, $configComplete) = + $this->validateConfig($currentConfigArray, $parsedConfigArray, $level); + } catch (\Exception $exception) { + $this->logger->error( + "Problem while parsing the configuration file : {path}", + ['path' => $folder->getPath() . '/' . $configFile->getPath()] + ); + } + } + + return [$configArray, $configComplete]; + } + + /** + * Removes the BOM from a file + * + * http://us.php.net/manual/en/function.pack.php#104151 + * + * @param string $file + * + * @return string + */ + private function bomFixer($file) { + $bom = pack("CCC", 0xef, 0xbb, 0xbf); + if (strncmp($file, $bom, 3) === 0) { + $file = substr($file, 3); + } + + return $file; + } + + /** + * Returns either the local config or one merged with a config containing sorting information + * + * @param array $currentConfigArray + * @param array $parsedConfigArray + * @param int $level + * + * @return array<array,bool> + */ + private function validateConfig($currentConfigArray, $parsedConfigArray, $level) { + $configComplete = false; + $sorting = $parsedConfigArray['sorting']; + $sortOrder = $parsedConfigArray['sort_order']; + $configArray = $parsedConfigArray; + if ($sorting) { + $configComplete = true; + if ($level > 0) { + // We only need the sorting information + $currentConfigArray['sorting'] = $sorting; + $currentConfigArray['sort_order'] = $sortOrder; + $configArray = $currentConfigArray; + } + } else { + if ($level > 0) { + // Reset the array to what we had earlier since we didn't find any sorting information + $configArray = $currentConfigArray; + } + } + + return [$configArray, $configComplete]; + } + +} diff --git a/service/downloadservice.php b/service/downloadservice.php index 669fb061..0e0074ad 100644 --- a/service/downloadservice.php +++ b/service/downloadservice.php @@ -14,9 +14,6 @@ namespace OCA\GalleryPlus\Service; use OCP\Files\File; -use OCA\GalleryPlus\Environment\Environment; -use OCA\GalleryPlus\Environment\NotFoundEnvException; -use OCA\GalleryPlus\Utility\SmarterLogger; /** * Prepares the file to download @@ -28,53 +25,34 @@ class DownloadService extends Service { use Base64Encode; /** - * @type Environment - */ - private $environment; - - /** - * Constructor - * - * @param string $appName - * @param Environment $environment - * @param SmarterLogger $logger - */ - public function __construct( - $appName, - Environment $environment, - SmarterLogger $logger - ) { - parent::__construct($appName, $logger); - - $this->environment = $environment; - } - - /** * Downloads the requested file * * @param string $image * @param bool $base64Encode * * @return array + * + * @throws NotFoundServiceException */ public function downloadFile($image, $base64Encode = false) { + $this->logger->debug("[DownloadService] File to Download: $image"); $file = null; + $download = false; try { /** @type File $file */ $file = $this->environment->getResourceFromPath($image); - } catch (NotFoundEnvException $exception) { + $download = [ + 'path' => $image, + 'preview' => $file->getContent(), + 'mimetype' => $file->getMimeType() + ]; + + if ($base64Encode) { + $download['preview'] = $this->encode($download['preview']); + } + } catch (\Exception $exception) { $this->logAndThrowNotFound($exception->getMessage()); } - $this->logger->debug("[DownloadService] File to Download: $image"); - $download = [ - 'path' => $image, - 'preview' => $file->getContent(), - 'mimetype' => $file->getMimeType() - ]; - - if ($base64Encode) { - $download['preview'] = $this->encode($download['preview']); - } return $download; } diff --git a/service/filesservice.php b/service/filesservice.php new file mode 100644 index 00000000..56bfe327 --- /dev/null +++ b/service/filesservice.php @@ -0,0 +1,294 @@ +<?php +/** + * ownCloud - galleryplus + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Olivier Paroz <owncloud@interfasys.ch> + * + * @copyright Olivier Paroz 2014-2015 + */ + +namespace OCA\GalleryPlus\Service; + +use OCP\Files\Folder; +use OCP\Files\File; +use OCP\Files\Node; + +/** + * Contains various methods which provide initial information about the + * supported media types, the folder permissions and the images contained in + * the system + * + * @package OCA\GalleryPlus\Service + */ +class FilesService extends Service { + + /** + * @type null|array<string,string|int> + */ + private $images = []; + /** + * @type string[] + */ + private $supportedMediaTypes; + + /** + * This returns the list of all media files which can be shown starting from the given folder + * + * @param Folder $folder + * @param string[] $supportedMediaTypes + * + * @return array<string,string|int> all the images we could find + */ + public function getMediaFiles($folder, $supportedMediaTypes) { + $this->supportedMediaTypes = $supportedMediaTypes; + + $this->searchFolder($folder); + + return $this->images; + } + + /** + * Look for media files and folders in the given folder + * + * @param Folder $folder + * @param int $subDepth + * + * @return int + */ + private function searchFolder($folder, $subDepth = 0) { + $albumImageCounter = 0; + $subFolders = []; + + $nodes = $this->getNodes($folder, $subDepth); + foreach ($nodes as $node) { + //$this->logger->debug("Sub-Node path : {path}", ['path' => $node->getPath()]); + $nodeType = $this->getNodeType($node); + $subFolders = array_merge($subFolders, $this->isFolder($node, $nodeType)); + + if ($nodeType === 'file') { + $albumImageCounter = $albumImageCounter + (int)$this->isPreviewAvailable($node); + if ($this->haveEnoughPictures($albumImageCounter, $subDepth)) { + break; + } + } + } + $this->searchSubFolders($subFolders, $subDepth, $albumImageCounter); + + return $albumImageCounter; + } + + /** + * Retrieves all files and sub-folders contained in a folder + * + * If we can't find anything in the current folder, we throw an exception as there is no point + * in doing any more work, but if we're looking at a sub-folder, we return an empty array so + * that it can be simply ignored + * + * @param Folder $folder + * @param int $subDepth + * + * @return array + * + * @throws NotFoundServiceException + */ + private function getNodes($folder, $subDepth) { + $nodes = []; + try { + if ($folder->isReadable() + && $folder->getStorage() + ->isLocal() + ) { + $nodes = $folder->getDirectoryListing(); + } + } catch (\Exception $exception) { + $nodes = $this->recoverFromGetNodesError($subDepth, $exception); + } + + return $nodes; + } + + /** + * Throws an exception if this problem occurs in the current folder, otherwise just ignores the + * sub-folder + * + * @param int $subDepth + * @param \Exception $exception + * + * @return array + * @throws NotFoundServiceException + */ + private function recoverFromGetNodesError($subDepth, $exception) { + if ($subDepth === 0) { + $this->logAndThrowNotFound($exception->getMessage()); + } + + return []; + } + + /** + * Returns the node type, either 'dir' or 'file' + * + * If there is a problem, we return an empty string so that the node can be ignored + * + * @param Node $node + * + * @return string + */ + private function getNodeType($node) { + try { + $nodeType = $node->getType(); + } catch (\Exception $exception) { + return ''; + } + + return $nodeType; + } + + /** + * Returns the node if it's a folder we have access to + * + * @param Folder $node + * @param string $nodeType + * + * @return array|Folder + */ + private function isFolder($node, $nodeType) { + if ($nodeType === 'dir') { + return [$node]; + } + + return []; + } + + /** + * Checks if we've collected enough pictures to be able to build the view + * + * An album is full when we find max 4 pictures at the same level + * + * @param int $albumImageCounter + * @param int $subDepth + * + * @return bool + */ + private function haveEnoughPictures($albumImageCounter, $subDepth) { + if ($subDepth === 0) { + return false; + } + if ($albumImageCounter === 4) { + return true; + } + + return false; + } + + /** + * Looks for pictures in sub-folders + * + * If we're at level 0, we need to look for pictures in sub-folders no matter what + * If we're at deeper levels, we only need to go further if we haven't managed to find one + * picture in the current folder + * + * @param array<Folder> $subFolders + * @param int $subDepth + * @param int $albumImageCounter + */ + private function searchSubFolders($subFolders, $subDepth, $albumImageCounter) { + if ($this->folderNeedsToBeSearched($subFolders, $subDepth, $albumImageCounter)) { + $subDepth++; + foreach ($subFolders as $subFolder) { + $count = $this->searchFolder($subFolder, $subDepth); + if ($this->abortSearch($subDepth, $count)) { + break; + } + } + } + } + + /** + * Checks if we need to look for media files in the specified folder + * + * @param array<Folder> $subFolders + * @param int $subDepth + * @param int $albumImageCounter + * + * @return bool + */ + private function folderNeedsToBeSearched($subFolders, $subDepth, $albumImageCounter) { + if (!empty($subFolders) && ($subDepth === 0 || $albumImageCounter === 0)) { + return true; + } + + return false; + } + + /** + * Returns true if there is no need to check any other sub-folder at the same depth level + * + * @param int $subDepth + * @param int $count + * + * @return bool + */ + private function abortSearch($subDepth, $count) { + if ($subDepth > 1 && $count > 0) { + return true; + } + + return false; + } + + /** + * Returns true if the file is of a supported media type and adds it to the array of items to + * return + * + * @todo We could potentially check if the file is readable ($file->stat() maybe) in order to + * only return valid files, but this may slow down operations + * + * @param File $file the file to test + * + * @return bool + */ + private function isPreviewAvailable($file) { + try { + $mimeType = $file->getMimetype(); + $isLocal = $file->getStorage() + ->isLocal(); + if ($isLocal && in_array($mimeType, $this->supportedMediaTypes)) { + $this->addFileToResults($file); + + return true; + } + } catch (\Exception $exception) { + return false; + } + + return false; + } + + /** + * Adds various information about a file to the list of results + * + * @param File $file + */ + private function addFileToResults($file) { + $imagePath = $this->environment->getPathFromVirtualRoot($file); + $imageId = $file->getId(); + $mimeType = $file->getMimetype(); + $mTime = $file->getMTime(); + + $imageData = [ + 'path' => $imagePath, + 'fileid' => $imageId, + 'mimetype' => $mimeType, + 'mtime' => $mTime + ]; + + $this->images[] = $imageData; + + //$this->logger->debug("Image path : {path}", ['path' => $imagePath]); + } + +} diff --git a/service/infoservice.php b/service/infoservice.php index f89545bb..0e8d82ee 100644 --- a/service/infoservice.php +++ b/service/infoservice.php @@ -12,11 +12,8 @@ namespace OCA\GalleryPlus\Service; -use Symfony\Component\Yaml\Yaml; - use OCP\Files\Folder; use OCP\Files\File; -use OCP\Files\Node; use OCA\GalleryPlus\Utility\SmarterLogger; @@ -60,18 +57,6 @@ class InfoService extends Service { 'application/font-sfnt', 'application/x-font', ]; - /** - * @type array<string, string|int> - */ - private $images = []; - /** - * @type string[] - */ - private $supportedMimes; - /** - * @type string - */ - private $fromRootToFolder; /** * Constructor @@ -122,423 +107,93 @@ class InfoService extends Service { } /** - * Returns information about the currently selected folder - * - * * privacy setting - * * special configuration - * * permissions - * * ID - * - * @param Folder $folderNode - * @param string $folderPathFromRoot - * - * @return array<string,string|int> - */ - public function getAlbumInfo($folderNode, $folderPathFromRoot) { - $configName = 'gallery.cnf'; - $privacyChecker = '.nomedia'; - $albumInfo = []; - list ($albumConfig, $privateAlbum) = - $this->getAlbumConfig($folderNode, $privacyChecker, $configName); - - if (!$privateAlbum) { - $path = str_replace($folderPathFromRoot, '', $folderNode->getPath()); - if (rtrim($folderPathFromRoot, '/') === $folderNode->getPath()) { - $path = ''; - } - $albumInfo = [ - 'path' => $path, - 'fileid' => $folderNode->getID(), - 'permissions' => $folderNode->getPermissions() - ]; - $albumInfo = array_merge($albumInfo, $albumConfig); - } - - return [$albumInfo, $privateAlbum]; - } - - /** * This returns the list of all images which can be shown starting from the given folder * - * If the starting URL is one of a fullscreen preview, we'll return the images of the - * containing folder - * - * @param array <Node, string> $folderData + * @param array $folderData * - * @return array<string,string|int> all the images we could find + * @return array all the images we could find */ public function getImages($folderData) { - $this->supportedMimes = $this->getSupportedMimes(false); - $this->fromRootToFolder = $folderData['fromRootToFolder']; - - /** @type Node $node */ - $node = $folderData['imagesFolder']; - if ($node->getType() === 'dir') { - $this->searchFolder($node); - } else { - $this->searchFolder($node->getParent()); - } - - return $this->images; - } - - /** - * Returns an album configuration array - * - * @param Folder $folder - * @param string $privacyChecker - * @param string $configName - * @param int $level - * @param array $configArray - * @param bool $configComplete - * - * @return array <null|string,string> - */ - private function getAlbumConfig( - $folder, $privacyChecker, $configName, $level = 0, $configArray = [], - $configComplete = false - ) { - if ($folder->nodeExists($privacyChecker)) { - // Cancel as soon as we find out that the folder is private - return [null, true]; - } - list($configArray, $configComplete) = - $this->parseConfig($folder, $configName, $configArray, $configComplete, $level); - $parentFolder = $folder->getParent(); - $path = $parentFolder->getPath(); - if ($path !== '' && $path !== '/') { - $level++; - - return $this->getAlbumConfig( - $parentFolder, $privacyChecker, $configName, $level, $configArray, $configComplete - ); - } - - // We have reached the root folder - return [$configArray, false]; - } - - /** - * Returns a parsed configuration if one was found in the current folder - * - * @param Folder $folder - * @param string $configName - * @param array $currentConfigArray - * @param bool $configComplete - * @param int $level - * - * @return array<array,bool> - */ - private function parseConfig( - $folder, $configName, $currentConfigArray, $configComplete, $level - ) { - $configArray = $currentConfigArray; - // Let's try to find the missing information in the configuration located in this folder - if (!$configComplete && $folder->nodeExists($configName)) { - /** @type File $configFile */ - $configFile = $folder->get($configName); - try { - $rawConfig = $configFile->getContent(); - $saneConfig = $this->bomFixer($rawConfig); - $parsedConfigArray = Yaml::parse($saneConfig); - list($configArray, $configComplete) = - $this->validateConfig($currentConfigArray, $parsedConfigArray, $level); - } catch (\Exception $exception) { - $this->logger->error( - "Problem while parsing the configuration file : {path}", - ['path' => $folder->getPath() . '/' . $configFile->getPath()] - ); - } - } - - return [$configArray, $configComplete]; - } - - /** - * Removes the BOM from a file - * - * http://us.php.net/manual/en/function.pack.php#104151 - * - * @param string $file - * - * @return string - */ - private function bomFixer($file) { - $bom = pack("CCC", 0xef, 0xbb, 0xbf); - if (strncmp($file, $bom, 3) === 0) { - $file = substr($file, 3); - } - - return $file; - } - - /** - * Returns either the local config or one merged with a config containing sorting information - * - * @param array $currentConfigArray - * @param array $parsedConfigArray - * @param int $level - * - * @return array<array,bool> - */ - private function validateConfig($currentConfigArray, $parsedConfigArray, $level) { - $configComplete = false; - $sorting = $parsedConfigArray['sorting']; - $sortOrder = $parsedConfigArray['sort_order']; - $configArray = $parsedConfigArray; - if ($sorting) { - $configComplete = true; - if ($level > 0) { - // We only need the sorting information - $currentConfigArray['sorting'] = $sorting; - $currentConfigArray['sort_order'] = $sortOrder; - $configArray = $currentConfigArray; - } - } else { - if ($level > 0) { - // Reset the array to what we had earlier since we didn't find any sorting information - $configArray = $currentConfigArray; - } - } - - return [$configArray, $configComplete]; - } - - /** - * Look for media files and folders in the given folder - * - * @param Folder $folder - * @param int $subDepth - * - * @return int - */ - private function searchFolder($folder, $subDepth = 0) { - $albumImageCounter = 0; - $subFolders = []; + $images = $this->searchByMime($folderData['imagesFolder']); + $fromRootToFolder = $folderData['fromRootToFolder']; - $nodes = $this->getNodes($folder, $subDepth); - foreach ($nodes as $node) { - //$this->logger->debug("Sub-Node path : {path}", ['path' => $node->getPath()]); - $nodeType = $this->getNodeType($node); - $subFolders = array_merge($subFolders, $this->allowedSubFolder($node, $nodeType)); + $result = $this->prepareImagesArray($images, $fromRootToFolder); - if ($nodeType === 'file') { - $albumImageCounter = $albumImageCounter + (int)$this->isPreviewAvailable($node); - if ($this->haveEnoughPictures($albumImageCounter, $subDepth)) { - break; - } - } - } - $this->searchSubFolders($subFolders, $subDepth, $albumImageCounter); - - return $albumImageCounter; - } - - /** - * Retrieves all files and sub-folders contained in a folder - * - * If we can't find anything in the current folder, we throw an exception as there is no point - * in doing any more work, but if we're looking at a sub-folder, we return an empty array so - * that it can be simply ignored - * - * @param Folder $folder - * @param int $subDepth - * - * @return array - * - * @throws NotFoundServiceException - */ - private function getNodes($folder, $subDepth) { - $nodes = []; - try { - if ($folder->isReadable() - && $folder->getStorage() - ->isLocal() - ) { - $nodes = $folder->getDirectoryListing(); - } - } catch (\Exception $exception) { - $nodes = $this->recoverFromGetNodesError($subDepth, $exception); - } + //$this->logger->debug("Images array: {images}", ['images' => $result]); - return $nodes; + return $result; } /** - * Throws an exception if this problem occurs in the current folder, otherwise just ignores the - * sub-folder + * Returns all the images of which we can generate a preview * - * @param int $subDepth - * @param \Exception $exception + * @param Folder $imagesFolder * * @return array - * @throws NotFoundServiceException - */ - private function recoverFromGetNodesError($subDepth, $exception) { - if ($subDepth === 0) { - $this->logAndThrowNotFound($exception->getMessage()); - } - - return []; - } - - /** - * Returns the node type, either 'dir' or 'file' - * - * If there is a problem, we return an empty string so that the node can be ignored - * - * @param Node $node - * - * @return string - */ - private function getNodeType($node) { - try { - $nodeType = $node->getType(); - } catch (\Exception $exception) { - return ''; - } - - return $nodeType; - } - - /** - * Returns the node if it's a folder we have access to - * - * @param Folder $node - * @param string $nodeType - * - * @return array|Folder - */ - private function allowedSubFolder($node, $nodeType) { - if ($nodeType === 'dir') { - /** @type Folder $node */ - if (!$node->nodeExists('.nomedia')) { - return [$node]; - } - } - - return []; - } - - /** - * Checks if we've collected enough pictures to be able to build the view - * - * An album is full when we find max 4 pictures at the same level - * - * @param int $albumImageCounter - * @param int $subDepth - * - * @return bool */ - private function haveEnoughPictures($albumImageCounter, $subDepth) { - if ($subDepth === 0) { - return false; - } - if ($albumImageCounter === 4) { - return true; - } + private function searchByMime($imagesFolder) { + $images = []; + $mimes = $this->getSupportedMimes(false); - return false; - } + foreach ($mimes as $mime) { + /** + * We look for images of this media type in the whole system. + * This can lead to performance issues + * + * @todo Use an internal Class to solve the performance issue + */ + $mimeImages = $imagesFolder->searchByMime($mime); - /** - * Looks for pictures in sub-folders - * - * If we're at level 0, we need to look for pictures in sub-folders no matter what - * If we're at deeper levels, we only need to go further if we haven't managed to find one - * picture in the current folder - * - * @param array <Folder> $subFolders - * @param int $subDepth - * @param int $albumImageCounter - */ - private function searchSubFolders($subFolders, $subDepth, $albumImageCounter) { - if ($this->folderNeedsToBeSearched($subFolders, $subDepth, $albumImageCounter)) { - $subDepth++; - foreach ($subFolders as $subFolder) { - $count = $this->searchFolder($subFolder, $subDepth); - if ($this->abortSearch($subDepth, $count)) { - break; - } - } + $images = array_merge($images, $mimeImages); } - } - /** - * Checks if we need to look for media files in the specified folder - * - * @param array <Folder> $subFolders - * @param int $subDepth - * @param int $albumImageCounter - * - * @return bool - */ - private function folderNeedsToBeSearched($subFolders, $subDepth, $albumImageCounter) { - if (!empty($subFolders) && ($subDepth === 0 || $albumImageCounter === 0)) { - return true; - } - - return false; + return $images; } /** - * Returns true if there is no need to check any other sub-folder at the same depth level - * - * @param int $subDepth - * @param int $count - * - * @return bool - */ - private function abortSearch($subDepth, $count) { - if ($subDepth > 1 && $count > 0) { - return true; - } - - return false; - } - - /** - * Returns true if the file is of a supported media type and adds it to the array of items to - * return + * Fixes the path of each image we've found * * We remove the part which goes from the user's root to the current * folder and we also remove the current folder for public galleries * - * @todo We could potentially check if the file is readable ($file->stat() maybe) in order to - * only return valid files, but this may slow down operations + * @todo Test this on OC8 + * On OC7, we fix searchByMime which returns images from the rubbish bin... + * https://github.com/owncloud/core/issues/4903 * - * @param File $file the file to test + * Example logger + * $this->logger->debug( + * "folderPath: {folderPath} pathRelativeToFolder: {pathRelativeToFolder} + * imagePath: {imagePath} mime: {mime}", [ + * 'folderPath' => $folderPath, + * 'pathRelativeToFolder' => $pathRelativeToFolder, + * 'imagePath' => $imagePath, + * 'mime' => $mimeType + * ] + * ); * - * @return bool + * @param array $images + * @param string $fromRootToFolder + * + * @return array */ - private function isPreviewAvailable($file) { - try { - $mimeType = $file->getMimetype(); - $isLocal = $file->getStorage() - ->isLocal(); - if ($isLocal && in_array($mimeType, $this->supportedMimes)) { - $imagePath = $file->getPath(); - $fixedPath = str_replace($this->fromRootToFolder, '', $imagePath); - $imageId = $file->getId(); - $mTime = $file->getMTime(); - $imageData = [ - 'path' => $fixedPath, - 'fileid' => $imageId, - 'mimetype' => $mimeType, - 'mtime' => $mTime - ]; - $this->images[] = $imageData; - - /*$this->logger->debug( - "Image path : {path}", ['path' => $imagePath] - );*/ - - return true; + private function prepareImagesArray($images, $fromRootToFolder) { + $result = []; + /** @type File $image */ + foreach ($images as $image) { + $imagePath = $image->getPath(); + $mimeType = $image->getMimetype(); + $fixedPath = str_replace($fromRootToFolder, '', $imagePath); + if (substr($fixedPath, 0, 9) === "_trashbin") { + continue; } - } catch (\Exception $exception) { - return false; + $imageData = [ + 'path' => $fixedPath, + 'mimetype' => $mimeType + ]; + $result[] = $imageData; } - return false; + return $result; } -} + +}
\ No newline at end of file diff --git a/service/previewservice.php b/service/previewservice.php index f9608827..0484e20d 100644 --- a/service/previewservice.php +++ b/service/previewservice.php @@ -13,6 +13,7 @@ namespace OCA\GalleryPlus\Service; use OCP\Files\File; +use OCP\Template; use OCA\GalleryPlus\Environment\Environment; use OCA\GalleryPlus\Environment\NotFoundEnvException; @@ -29,13 +30,36 @@ class PreviewService extends Service { use Base64Encode; /** - * @type Environment - */ - private $environment; - /** * @type Preview */ private $previewManager; + /** + * @todo This hard-coded array could be replaced by admin settings + * + * @type string[] + */ + private $baseMimeTypes = [ + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/x-xbitmap', + 'image/bmp', + 'image/tiff', + 'image/x-dcraw', + 'application/x-photoshop', + 'application/illustrator', + 'application/postscript', + ]; + /** + * These types are useful for files preview in the files app, but + * not for the gallery side + * + * @type string[] + */ + private $slideshowMimeTypes = [ + 'application/font-sfnt', + 'application/x-font', + ]; /** * Constructor @@ -51,21 +75,45 @@ class PreviewService extends Service { Preview $previewManager, SmarterLogger $logger ) { - parent::__construct($appName, $logger); + parent::__construct($appName, $environment, $logger); - $this->environment = $environment; $this->previewManager = $previewManager; } /** - * Returns true if the passed mime type is supported + * This builds and returns a list of all supported media types * - * @param string $mimeType + * @todo Native SVG could be disabled via admin settings * - * @return boolean + * @param bool $slideshow + * + * @return string[] all supported media types */ - public function isMimeSupported($mimeType = '*') { - return $this->previewManager->isMimeSupported($mimeType); + public function getSupportedMediaTypes($slideshow) { + $supportedMimes = []; + $wantedMimes = $this->baseMimeTypes; + + if ($slideshow) { + $wantedMimes = array_merge($wantedMimes, $this->slideshowMimeTypes); + } + + foreach ($wantedMimes as $wantedMime) { + // Let's see if a preview of files of that media type can be generated + if ($this->isMimeSupported($wantedMime)) { + $pathToIcon = Template::mimetype_icon($wantedMime); + $supportedMimes[$wantedMime] = + $pathToIcon; // We add it to the list of supported media types + } + } + // If it's enabled, but doesn't work, an exception will be raised. + // If it's disabled, we support it via the browser's native support + if (!$supportedMimes['image/svg+xml']) { + $supportedMimes['image/svg+xml'] = Template::mimetype_icon('image/svg+xml'); + } + + $this->logger->debug("Supported Mimes: {mimes}", ['mimes' => $supportedMimes]); + + return $supportedMimes; } /** @@ -75,6 +123,8 @@ class PreviewService extends Service { * @param bool $animatedPreview * * @return bool + * + * @throws NotFoundServiceException */ public function isPreviewRequired($image, $animatedPreview) { $file = null; @@ -176,6 +226,25 @@ class PreviewService extends Service { } /** + * Returns true if the passed mime type is supported + * + * In case of a failure, we just return that the media type is not supported + * + * @param string $mimeType + * + * @return boolean + */ + private function isMimeSupported($mimeType = '*') { + try { + return $this->previewManager->isMimeSupported($mimeType); + } catch (\Exception $exception) { + unset($exception); + + return false; + } + } + + /** * Decides if we should download the SVG or generate a preview * * SVGs are downloaded if the SVG converter is disabled diff --git a/service/service.php b/service/service.php index bb9b229f..11d970c5 100644 --- a/service/service.php +++ b/service/service.php @@ -12,6 +12,10 @@ namespace OCA\GalleryPlus\Service; +use OCP\Files\Folder; + +use OCA\GalleryPlus\Environment\Environment; +use OCA\GalleryPlus\Environment\NotFoundEnvException; use OCA\GalleryPlus\Utility\SmarterLogger; /** @@ -24,7 +28,11 @@ abstract class Service { /** * @type string */ - private $appName; + protected $appName; + /** + * @type Environment + */ + protected $environment; /** * @type SmarterLogger */ @@ -34,26 +42,95 @@ abstract class Service { * Constructor * * @param string $appName + * @param Environment $environment * @param SmarterLogger $logger */ public function __construct( $appName, + Environment $environment, SmarterLogger $logger ) { $this->appName = $appName; + $this->environment = $environment; $this->logger = $logger; } /** - * Logs the error and raises an exception + * This returns the current folder node based on a path + * + * If the path leads to a file, we'll return the node of the containing folder + * + * If we can't find anything, we try with the parent folder, up to the root or until we reach + * our recursive limit + * + * @param string $location + * @param int $depth + * + * @return array <Folder,string,bool> + */ + public function getCurrentFolder($location, $depth = 0) { + $node = null; + $location = $this->validateLocation($location, $depth); + try { + $node = $this->environment->getResourceFromPath($location); + if ($node->getType() === 'file') { + $node = $node->getParent(); + } + } catch (NotFoundEnvException $exception) { + // There might be a typo in the file or folder name + $folder = pathinfo($location, PATHINFO_DIRNAME); + $depth++; + + return $this->getCurrentFolder($folder, $depth); + } + $path = $this->environment->getPathFromVirtualRoot($node); + $locationHasChanged = $this->hasLocationChanged($depth); + + return [$path, $node, $locationHasChanged]; + } + + /** + * Logs the error and raises a "Not found" type exception * * @param string $message * - * @throws ServiceException + * @throws NotFoundServiceException */ protected function logAndThrowNotFound($message) { $this->logger->error($message . ' (404)'); throw new NotFoundServiceException($message); } + + /** + * Makes sure we don't go too far up before giving up + * + * @param string $location + * @param int $depth + * + * @return string + */ + private function validateLocation($location, $depth) { + if ($depth === 4) { + // We can't find anything, so we decide to return data for the root folder + $location = ''; + } + + return $location; + } + + /** + * @param $depth + * + * @return bool + */ + private function hasLocationChanged($depth) { + $locationHasChanged = false; + if ($depth > 0) { + $locationHasChanged = true; + } + + return $locationHasChanged; + } + } diff --git a/service/thumbnailservice.php b/service/thumbnailservice.php index e2f4c4d1..b0103fe2 100644 --- a/service/thumbnailservice.php +++ b/service/thumbnailservice.php @@ -12,15 +12,8 @@ namespace OCA\GalleryPlus\Service; -use OCA\GalleryPlus\Environment\Environment; -use OCA\GalleryPlus\Preview\Preview; -use OCA\GalleryPlus\Utility\SmarterLogger; - /** - * Creates thumbnails for the list of images which is submitted to - * the service - * - * Uses EventSource to send back thumbnails as soon as they're ready + * Deals with any thumbnail specific requests * * @package OCA\GalleryPlus\Service */ |