From 218b33d137a5f6cfa121bb6a9546847174c2167e Mon Sep 17 00:00:00 2001 From: Louis Chemineau Date: Thu, 13 Oct 2022 10:58:59 +0200 Subject: [PATCH] Add DAV endpoint for location grouping Signed-off-by: Louis Chemineau --- lib/Sabre/Location/LocationHome.php | 126 ++++++++++++++++++++ lib/Sabre/Location/LocationPhoto.php | 168 +++++++++++++++++++++++++++ lib/Sabre/Location/LocationRoot.php | 138 ++++++++++++++++++++++ lib/Sabre/PhotosHome.php | 14 ++- lib/Sabre/RootCollection.php | 16 ++- 5 files changed, 457 insertions(+), 5 deletions(-) create mode 100644 lib/Sabre/Location/LocationHome.php create mode 100644 lib/Sabre/Location/LocationPhoto.php create mode 100644 lib/Sabre/Location/LocationRoot.php diff --git a/lib/Sabre/Location/LocationHome.php b/lib/Sabre/Location/LocationHome.php new file mode 100644 index 000000000..01a463ed5 --- /dev/null +++ b/lib/Sabre/Location/LocationHome.php @@ -0,0 +1,126 @@ + + * + * @author Louis Chemineau + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OCA\Photos\Sabre\Location; + +use OCP\Files\IRootFolder; +use OCA\Photos\DB\Location\LocationInfo; +use OCA\Photos\DB\Location\LocationMapper; +use OCA\Photos\Service\MediaLocationManager; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; + +class LocationHome implements ICollection { + protected array $principalInfo; + protected string $userId; + protected IRootFolder $rootFolder; + protected MediaLocationManager $mediaLocationManager; + protected LocationMapper $locationMapper; + + public const NAME = 'locations'; + + /** + * @var LocationRoot[] + */ + protected ?array $children = null; + + public function __construct( + array $principalInfo, + string $userId, + IRootFolder $rootFolder, + MediaLocationManager $mediaLocationManager, + LocationMapper $locationMapper + ) { + $this->principalInfo = $principalInfo; + $this->userId = $userId; + $this->rootFolder = $rootFolder; + $this->mediaLocationManager = $mediaLocationManager; + $this->locationMapper = $locationMapper; + } + + /** + * @return never + */ + public function delete() { + throw new Forbidden(); + } + + public function getName(): string { + return self::NAME; + } + + /** + * @return never + */ + public function setName($name) { + throw new Forbidden('Permission denied to rename this folder'); + } + + public function createFile($name, $data = null) { + throw new Forbidden('Not allowed to create files in this folder'); + } + + public function createDirectory($name) { + throw new Forbidden('Not allowed to create folder in this folder'); + } + + public function getChild($name) { + foreach ($this->getChildren() as $child) { + if ($child->getName() === $name) { + return $child; + } + } + + throw new NotFound(); + } + + /** + * @return AlbumRoot[] + */ + public function getChildren(): array { + if ($this->children === null) { + $this->children = array_map( + fn (LocationInfo $locationInfo) => new LocationRoot($this->locationMapper, $this->mediaLocationManager, $locationInfo, $this->rootFolder), + $this->locationMapper->findLocationForUser($this->userId) + ); + } + + return $this->children; + } + + public function childExists($name): bool { + try { + $this->getChild($name); + return true; + } catch (NotFound $e) { + return false; + } + } + + public function getLastModified(): int { + return 0; + } +} diff --git a/lib/Sabre/Location/LocationPhoto.php b/lib/Sabre/Location/LocationPhoto.php new file mode 100644 index 000000000..09570e640 --- /dev/null +++ b/lib/Sabre/Location/LocationPhoto.php @@ -0,0 +1,168 @@ + + * + * @author Louis Chemineau + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OCA\Photos\Sabre\Location; + +use OCA\Photos\DB\Location\LocationFile; +use OCA\Photos\DB\Location\LocationInfo; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\Files\File; +use OCP\Files\NotFoundException; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\IFile; + +class LocationPhoto implements IFile { + private LocationInfo $locationInfo; + private LocationFile $locationFile; + private IRootFolder $rootFolder; + + public const TAG_FAVORITE = '_$!!$_'; + + public function __construct( + LocationInfo $locationInfo, + LocationFile $locationFile, + IRootFolder $rootFolder + ) { + $this->locationInfo = $locationInfo; + $this->locationFile = $locationFile; + $this->rootFolder = $rootFolder; + } + + /** + * @return void + */ + public function delete() { + throw new Forbidden('Cannot remove from a location'); + } + + public function getName() { + return $this->locationFile->getFileId() . "-" . $this->locationFile->getName(); + } + + /** + * @return never + */ + public function setName($name) { + throw new Forbidden('Cannot rename from a location'); + } + + public function getLastModified() { + return $this->locationFile->getMTime(); + } + + public function put($data) { + $nodes = $this->userFolder->getById($this->file->getFileId()); + $node = current($nodes); + if ($node) { + /** @var Node $node */ + if ($node instanceof File) { + return $node->putContent($data); + } else { + throw new NotFoundException("Photo is a folder"); + } + } else { + throw new NotFoundException("Photo not found for user"); + } + } + + public function get() { + $nodes = $this->rootFolder + ->getUserFolder($this->locationInfo->getUserId()) + ->getById($this->locationFile->getFileId()); + $node = current($nodes); + if ($node) { + /** @var Node $node */ + if ($node instanceof File) { + return $node->fopen('r'); + } else { + throw new NotFoundException("Photo is a folder"); + } + } else { + throw new NotFoundException("Photo not found for user"); + } + } + + public function getFileId(): int { + return $this->locationFile->getFileId(); + } + + public function getFileInfo(): Node { + $nodes = $this->rootFolder + ->getUserFolder($this->locationInfo->getUserId()) + ->getById($this->locationFile->getFileId()); + $node = current($nodes); + if ($node) { + return $node->get; + } else { + throw new NotFoundException("Photo not found for user"); + } + } + + public function getContentType() { + return $this->locationFile->getMimeType(); + } + + public function getETag() { + return $this->locationFile->getEtag(); + } + + public function getSize() { + return $this->locationFile->getSize(); + } + + public function getFile(): LocationFile { + return $this->locationFile; + } + + public function isFavorite(): bool { + $tagManager = \OCP\Server::get(\OCP\ITagManager::class); + $tagger = $tagManager->load('files'); + if ($tagger === null) { + return false; + } + $tags = $tagger->getTagsForObjects([$this->getFileId()]); + + if ($tags === false || empty($tags)) { + return false; + } + + return array_search(self::TAG_FAVORITE, current($tags)) !== false; + } + + public function setFavoriteState($favoriteState): bool { + $tagManager = \OCP\Server::get(\OCP\ITagManager::class); + $tagger = $tagManager->load('files'); + + switch ($favoriteState) { + case "0": + return $tagger->removeFromFavorites($this->locationFile->getFileId()); + case "1": + return $tagger->addToFavorites($this->locationFile->getFileId()); + default: + new \Exception('Favorite state is invalide, should be 0 or 1.'); + } + } +} diff --git a/lib/Sabre/Location/LocationRoot.php b/lib/Sabre/Location/LocationRoot.php new file mode 100644 index 000000000..9682002b0 --- /dev/null +++ b/lib/Sabre/Location/LocationRoot.php @@ -0,0 +1,138 @@ + + * + * @author Louis Chemineau + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OCA\Photos\Sabre\Location; + +use OCA\Photos\DB\Location\LocationFile; +use OCA\Photos\DB\Location\LocationInfo; +use OCA\Photos\DB\Location\LocationMapper; +use OCA\Photos\Service\MediaLocationManager; +use OCP\Files\IRootFolder; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; + +class LocationRoot implements ICollection { + protected LocationMapper $locationMapper; + protected MediaLocationManager $mediaLocationManager; + protected LocationInfo $locationInfo; + protected IRootFolder $rootFolder; + /** @var array */ + protected ?array $children = null; + + public function __construct( + LocationMapper $locationMapper, + MediaLocationManager $mediaLocationManager, + LocationInfo $locationInfo, + IRootFolder $rootFolder, + ) { + $this->locationMapper = $locationMapper; + $this->mediaLocationManager = $mediaLocationManager; + $this->locationInfo = $locationInfo; + $this->rootFolder = $rootFolder; + } + + /** + * @return never + */ + public function delete() { + throw new Forbidden('Not allowed to delete a location collection'); + } + + public function getName(): string { + // TODO: Get the real name with the MediaLocationManager + return (string)$this->locationInfo->getLocationId(); + } + + /** + * @return never + */ + public function setName($name) { + throw new Forbidden('Cannot change the location collection name'); + } + + /** + * @param string $name + * @param null|resource|string $data + * @return never + */ + public function createFile($name, $data = null) { + throw new Forbidden('Cannot create a file in a location collection'); + } + + /** + * @return never + */ + public function createDirectory($name) { + throw new Forbidden('Not allowed to create directories in this folder'); + } + + public function getChildren(): array { + if ($this->children === null) { + $this->children = array_map( + fn (LocationFile $file) => new LocationPhoto($this->locationInfo, $file, $this->rootFolder), + $this->locationMapper->findFilesForUserAndLocation($this->locationInfo->getUserId(), $this->locationInfo->getLocationId()) + ); + } + + return $this->children; + } + + public function getChild($name): LocationPhoto { + foreach ($this->getChildren() as $child) { + if ($child->getName() === $name) { + return $child; + } + } + + throw new NotFound("$name not found"); + } + + public function childExists($name): bool { + try { + $this->getChild($name); + return true; + } catch (NotFound $e) { + return false; + } + } + + public function getLastModified(): int { + return 0; + } + + /** + * @return int|null + */ + public function getCover() { + $children = $this->getChildren(); + + if (count($children) > 0) { + return $children[0]->getFileId(); + } else { + return null; + } + } +} diff --git a/lib/Sabre/PhotosHome.php b/lib/Sabre/PhotosHome.php index fc2e6b4cf..741a477b0 100644 --- a/lib/Sabre/PhotosHome.php +++ b/lib/Sabre/PhotosHome.php @@ -24,8 +24,11 @@ namespace OCA\Photos\Sabre; use OCA\Photos\Album\AlbumMapper; +use OCA\Photos\DB\Location\LocationMapper; use OCA\Photos\Sabre\Album\AlbumsHome; use OCA\Photos\Sabre\Album\SharedAlbumsHome; +use OCA\Photos\Sabre\Location\LocationHome; +use OCA\Photos\Service\MediaLocationManager; use OCA\Photos\Service\UserConfigService; use OCP\Files\IRootFolder; use OCP\IUserManager; @@ -36,6 +39,8 @@ class PhotosHome implements ICollection { private AlbumMapper $albumMapper; + private LocationMapper $locationMapper; + private MediaLocationManager $mediaLocationManager; private array $principalInfo; private string $userId; private IRootFolder $rootFolder; @@ -46,6 +51,8 @@ class PhotosHome implements ICollection { public function __construct( array $principalInfo, AlbumMapper $albumMapper, + LocationMapper $locationMapper, + MediaLocationManager $mediaLocationManager, string $userId, IRootFolder $rootFolder, IUserManager $userManager, @@ -54,6 +61,8 @@ public function __construct( ) { $this->principalInfo = $principalInfo; $this->albumMapper = $albumMapper; + $this->locationMapper = $locationMapper; + $this->mediaLocationManager = $mediaLocationManager; $this->userId = $userId; $this->rootFolder = $rootFolder; $this->userManager = $userManager; @@ -97,6 +106,8 @@ public function getChild($name) { return new AlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userConfigService); case SharedAlbumsHome::NAME: return new SharedAlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService); + case LocationHome::NAME: + return new LocationHome($this->principalInfo, $this->userId, $this->rootFolder, $this->mediaLocationManager, $this->locationMapper); } throw new NotFound(); @@ -109,11 +120,12 @@ public function getChildren(): array { return [ new AlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userConfigService), new SharedAlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService), + new LocationHome($this->principalInfo, $this->userId, $this->rootFolder, $this->mediaLocationManager, $this->locationMapper), ]; } public function childExists($name): bool { - return $name === AlbumsHome::NAME || $name === SharedAlbumsHome::NAME; + return $name === AlbumsHome::NAME || $name === SharedAlbumsHome::NAME || $name === LocationHome::NAME; } public function getLastModified(): int { diff --git a/lib/Sabre/RootCollection.php b/lib/Sabre/RootCollection.php index 8bcb42c7e..23611e330 100644 --- a/lib/Sabre/RootCollection.php +++ b/lib/Sabre/RootCollection.php @@ -24,6 +24,8 @@ namespace OCA\Photos\Sabre; use OCA\Photos\Album\AlbumMapper; +use OCA\Photos\DB\Location\LocationMapper; +use OCA\Photos\Service\MediaLocationManager; use OCA\Photos\Service\UserConfigService; use OCP\Files\IRootFolder; use OCP\IUserSession; @@ -33,7 +35,9 @@ use OCP\IGroupManager; class RootCollection extends AbstractPrincipalCollection { - private AlbumMapper $folderMapper; + private AlbumMapper $albumMapper; + private LocationMapper $locationMapper; + private MediaLocationManager $mediaLocationManager; private IUserSession $userSession; private IRootFolder $rootFolder; private IUserManager $userManager; @@ -41,7 +45,9 @@ class RootCollection extends AbstractPrincipalCollection { private UserConfigService $userConfigService; public function __construct( - AlbumMapper $folderMapper, + AlbumMapper $albumMapper, + LocationMapper $locationMapper, + MediaLocationManager $mediaLocationManager, IUserSession $userSession, IRootFolder $rootFolder, PrincipalBackend\BackendInterface $principalBackend, @@ -51,7 +57,9 @@ public function __construct( ) { parent::__construct($principalBackend, 'principals/users'); - $this->folderMapper = $folderMapper; + $this->albumMapper = $albumMapper; + $this->locationMapper = $locationMapper; + $this->mediaLocationManager = $mediaLocationManager; $this->userSession = $userSession; $this->rootFolder = $rootFolder; $this->userManager = $userManager; @@ -74,7 +82,7 @@ public function getChildForPrincipal(array $principalInfo): PhotosHome { if (is_null($user) || $name !== $user->getUID()) { throw new \Sabre\DAV\Exception\Forbidden(); } - return new PhotosHome($principalInfo, $this->folderMapper, $name, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService); + return new PhotosHome($principalInfo, $this->albumMapper, $this->locationMapper, $this->mediaLocationManager, $name, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService); } public function getName(): string {