From 5bd766cb929ee2d9016048aa553f2ae4fdb6c0e2 Mon Sep 17 00:00:00 2001 From: Louis Chemineau Date: Wed, 22 Feb 2023 17:43:52 +0100 Subject: [PATCH] Add DAV endpoint for location grouping Signed-off-by: Louis Chemineau --- appinfo/info.xml | 2 +- lib/DB/Location/LocationMapper.php | 90 ++++++++++++-- lib/Sabre/Album/AlbumPhoto.php | 130 ++++--------------- lib/Sabre/Album/AlbumRoot.php | 4 +- lib/Sabre/CollectionPhoto.php | 119 ++++++++++++++++++ lib/Sabre/Location/LocationPhoto.php | 83 +++++++++++++ lib/Sabre/Location/LocationRoot.php | 152 +++++++++++++++++++++++ lib/Sabre/Location/LocationsHome.php | 114 +++++++++++++++++ lib/Sabre/PhotosHome.php | 39 +++--- lib/Sabre/{Album => }/PropFindPlugin.php | 26 +++- lib/Sabre/RootCollection.php | 32 ++--- 11 files changed, 620 insertions(+), 171 deletions(-) create mode 100644 lib/Sabre/CollectionPhoto.php create mode 100644 lib/Sabre/Location/LocationPhoto.php create mode 100644 lib/Sabre/Location/LocationRoot.php create mode 100644 lib/Sabre/Location/LocationsHome.php rename lib/Sabre/{Album => }/PropFindPlugin.php (86%) diff --git a/appinfo/info.xml b/appinfo/info.xml index 8ea43379b..67c70b743 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -41,7 +41,7 @@ OCA\Photos\Sabre\PublicRootCollection - OCA\Photos\Sabre\Album\PropFindPlugin + OCA\Photos\Sabre\PropFindPlugin diff --git a/lib/DB/Location/LocationMapper.php b/lib/DB/Location/LocationMapper.php index e3b9cac39..0326c69f7 100644 --- a/lib/DB/Location/LocationMapper.php +++ b/lib/DB/Location/LocationMapper.php @@ -29,6 +29,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\IMimeTypeLoader; use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; use OCP\IDBConnection; class LocationMapper { @@ -43,19 +44,19 @@ public function __construct( /** @return LocationInfo[] */ public function findLocationsForUser(string $userId): array { - $mountId = $this->rootFolder + $storageId = $this->rootFolder ->getUserFolder($userId) ->getMountPoint() - ->getMountId(); + ->getNumericStorageId(); + $mimepart = $this->mimeTypeLoader->getId('image'); $qb = $this->connection->getQueryBuilder(); $rows = $qb->selectDistinct('meta.metadata') - ->from('mounts', 'mount') - ->join('mount', 'filecache', 'file', $qb->expr()->eq('file.storage', 'mount.storage_id', IQueryBuilder::PARAM_INT)) - ->join('file', 'file_metadata', 'meta', $qb->expr()->eq('file.fileid', 'meta.id', IQueryBuilder::PARAM_INT)) - ->where($qb->expr()->eq('mount.id', $qb->createNamedParameter($mountId), IQueryBuilder::PARAM_INT)) + ->from('file_metadata', 'meta') + ->join('meta', 'filecache', 'file', $qb->expr()->eq('file.fileid', 'meta.id', IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('file.storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('file.mimepart', $qb->createNamedParameter($mimepart, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('meta.group_name', $qb->createNamedParameter(self::METADATA_TYPE))) ->executeQuery() @@ -64,21 +65,49 @@ public function findLocationsForUser(string $userId): array { return array_map(fn ($row) => new LocationInfo($userId, $row['metadata']), $rows); } + /** @return LocationInfo */ + public function findLocationForUser(string $userId, string $location): LocationInfo { + $storageId = $this->rootFolder + ->getUserFolder($userId) + ->getMountPoint() + ->getNumericStorageId(); + + $mimepart = $this->mimeTypeLoader->getId('image'); + + $qb = $this->connection->getQueryBuilder(); + + $rows = $qb->selectDistinct('meta.metadata') + ->from('file_metadata', 'meta') + ->join('meta', 'filecache', 'file', $qb->expr()->eq('file.fileid', 'meta.id', IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('file.storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('file.mimepart', $qb->createNamedParameter($mimepart, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('meta.group_name', $qb->createNamedParameter(self::METADATA_TYPE))) + ->andWhere($qb->expr()->eq('meta.metadata', $qb->createNamedParameter($location))) + ->executeQuery() + ->fetchAll(); + + if (count($rows) !== 1) { + throw new NotFoundException(); + } + + return new LocationInfo($userId, $rows[0]['metadata']); + } + /** @return LocationFile[] */ public function findFilesForUserAndLocation(string $userId, string $location) { - $mountId = $this->rootFolder + $storageId = $this->rootFolder ->getUserFolder($userId) ->getMountPoint() - ->getMountId(); + ->getNumericStorageId(); + $mimepart = $this->mimeTypeLoader->getId('image'); $qb = $this->connection->getQueryBuilder(); $rows = $qb->select('file.fileid', 'file.name', 'file.mimetype', 'file.size', 'file.mtime', 'file.etag', 'meta.metadata') - ->from('mounts', 'mount') - ->join('mount', 'filecache', 'file', $qb->expr()->eq('file.storage', 'mount.storage_id', IQueryBuilder::PARAM_INT)) - ->join('file', 'file_metadata', 'meta', $qb->expr()->eq('file.fileid', 'meta.id', IQueryBuilder::PARAM_INT)) - ->where($qb->expr()->eq('mount.id', $qb->createNamedParameter($mountId), IQueryBuilder::PARAM_INT)) + ->from('file_metadata', 'meta') + ->join('meta', 'filecache', 'file', $qb->expr()->eq('file.fileid', 'meta.id', IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('file.storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('file.mimepart', $qb->createNamedParameter($mimepart, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('meta.group_name', $qb->createNamedParameter(self::METADATA_TYPE))) ->andWhere($qb->expr()->eq('meta.metadata', $qb->createNamedParameter($location))) @@ -99,6 +128,43 @@ public function findFilesForUserAndLocation(string $userId, string $location) { ); } + public function findFileForUserAndLocation(string $userId, string $location, string $fileId, string $fileName): LocationFile { + $storageId = $this->rootFolder + ->getUserFolder($userId) + ->getMountPoint() + ->getNumericStorageId(); + + $mimepart = $this->mimeTypeLoader->getId('image'); + + $qb = $this->connection->getQueryBuilder(); + + $rows = $qb->select('file.fileid', 'file.name', 'file.mimetype', 'file.size', 'file.mtime', 'file.etag', 'meta.metadata') + ->from('file_metadata', 'meta') + ->join('meta', 'filecache', 'file', $qb->expr()->eq('file.fileid', 'meta.id', IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('file.storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('file.mimepart', $qb->createNamedParameter($mimepart, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('file.fileid', $qb->createNamedParameter($fileId))) + ->andWhere($qb->expr()->eq('file.name', $qb->createNamedParameter($fileName))) + ->andWhere($qb->expr()->eq('meta.group_name', $qb->createNamedParameter(self::METADATA_TYPE))) + ->andWhere($qb->expr()->eq('meta.metadata', $qb->createNamedParameter($location))) + ->executeQuery() + ->fetchAll(); + + if (count($rows) !== 1) { + throw new NotFoundException(); + } + + return new LocationFile( + (int)$rows[0]['fileid'], + $rows[0]['name'], + $this->mimeTypeLoader->getMimetypeById($rows[0]['mimetype']), + (int)$rows[0]['size'], + (int)$rows[0]['mtime'], + $rows[0]['etag'], + $rows[0]['metadata'] + ); + } + public function setLocationForFile(string $location, int $fileId): void { try { $query = $this->connection->getQueryBuilder(); diff --git a/lib/Sabre/Album/AlbumPhoto.php b/lib/Sabre/Album/AlbumPhoto.php index 832bcb979..ec3e1f35b 100644 --- a/lib/Sabre/Album/AlbumPhoto.php +++ b/lib/Sabre/Album/AlbumPhoto.php @@ -26,90 +26,36 @@ use OCA\Photos\Album\AlbumFile; use OCA\Photos\Album\AlbumInfo; use OCA\Photos\Album\AlbumMapper; +use OCA\Photos\Sabre\CollectionPhoto; use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\Files\File; +use OCP\Files\Folder; use OCP\Files\NotFoundException; -use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\IFile; -class AlbumPhoto implements IFile { - private AlbumMapper $albumMapper; - private AlbumInfo $album; - private AlbumFile $albumFile; - private IRootFolder $rootFolder; - - public const TAG_FAVORITE = '_$!!$_'; - - public function __construct(AlbumMapper $albumMapper, AlbumInfo $album, AlbumFile $albumFile, IRootFolder $rootFolder) { - $this->albumMapper = $albumMapper; - $this->album = $album; - $this->albumFile = $albumFile; - $this->rootFolder = $rootFolder; +class AlbumPhoto extends CollectionPhoto implements IFile { + public function __construct( + private AlbumMapper $albumMapper, + private AlbumInfo $album, + private AlbumFile $albumFile, + private IRootFolder $rootFolder, + Folder $userFolder, + ) { + parent::__construct($albumFile, $userFolder); } /** * @return void */ public function delete() { - $this->albumMapper->removeFile($this->album->getId(), $this->albumFile->getFileId()); - } - - public function getName() { - return $this->albumFile->getFileId() . "-" . $this->albumFile->getName(); - } - - /** - * @return never - */ - public function setName($name) { - throw new Forbidden('Can\'t rename photos trough the album api'); - } - - public function getLastModified() { - return $this->albumFile->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->albumFile->getOwner() ?: $this->album->getUserId()) - ->getById($this->albumFile->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->albumFile->getFileId(); + $this->albumMapper->removeFile($this->album->getId(), $this->file->getFileId()); } - public function getFileInfo(): Node { + private function getNode(): Node { $nodes = $this->rootFolder ->getUserFolder($this->albumFile->getOwner() ?: $this->album->getUserId()) - ->getById($this->albumFile->getFileId()); + ->getById($this->file->getFileId()); $node = current($nodes); if ($node) { return $node; @@ -118,48 +64,16 @@ public function getFileInfo(): Node { } } - public function getContentType() { - return $this->albumFile->getMimeType(); - } - - public function getETag() { - return $this->albumFile->getEtag(); - } - - public function getSize() { - return $this->albumFile->getSize(); - } - - public function getFile(): AlbumFile { - return $this->albumFile; - } - - 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; + public function get() { + $node = $this->getNode(); + if ($node instanceof File) { + return $node->fopen('r'); + } else { + throw new NotFoundException("Photo is a folder"); } - - 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->albumFile->getFileId()); - case "1": - return $tagger->addToFavorites($this->albumFile->getFileId()); - default: - new \Exception('Favorite state is invalide, should be 0 or 1.'); - } + public function getFileInfo(): Node { + return $this->getNode(); } } diff --git a/lib/Sabre/Album/AlbumRoot.php b/lib/Sabre/Album/AlbumRoot.php index 0a10157b4..fe4f03414 100644 --- a/lib/Sabre/Album/AlbumRoot.php +++ b/lib/Sabre/Album/AlbumRoot.php @@ -129,14 +129,14 @@ public function createDirectory($name) { public function getChildren(): array { return array_map(function (AlbumFile $file) { - return new AlbumPhoto($this->albumMapper, $this->album->getAlbum(), $file, $this->rootFolder); + return new AlbumPhoto($this->albumMapper, $this->album->getAlbum(), $file, $this->rootFolder, $this->rootFolder->getUserFolder($this->userId)); }, $this->album->getFiles()); } public function getChild($name): AlbumPhoto { foreach ($this->album->getFiles() as $file) { if ($file->getFileId() . "-" . $file->getName() === $name) { - return new AlbumPhoto($this->albumMapper, $this->album->getAlbum(), $file, $this->rootFolder); + return new AlbumPhoto($this->albumMapper, $this->album->getAlbum(), $file, $this->rootFolder, $this->rootFolder->getUserFolder($this->userId)); } } throw new NotFound("$name not found"); diff --git a/lib/Sabre/CollectionPhoto.php b/lib/Sabre/CollectionPhoto.php new file mode 100644 index 000000000..b280b804f --- /dev/null +++ b/lib/Sabre/CollectionPhoto.php @@ -0,0 +1,119 @@ + + * + * @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; + +use OCA\Photos\DB\PhotosFile; +use OCP\Files\Node; +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\NotFoundException; +use OCP\ITags; +use Sabre\DAV\Exception\Forbidden; + +class CollectionPhoto { + public function __construct( + protected PhotosFile $file, + protected Folder $userFolder, + ) { + } + + public function getName() { + return $this->file->getFileId() . "-" . $this->file->getName(); + } + + /** + * @return never + */ + public function setName($name) { + throw new Forbidden('Can\'t rename photos trough this api'); + } + + public function getLastModified() { + return $this->file->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 getFileId(): int { + return $this->file->getFileId(); + } + + public function getContentType() { + return $this->file->getMimeType(); + } + + public function getETag() { + return $this->file->getEtag(); + } + + public function getSize() { + return $this->file->getSize(); + } + + public function getFile(): PhotosFile { + return $this->file; + } + + 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(ITags::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->file->getFileId()); + case "1": + return $tagger->addToFavorites($this->file->getFileId()); + default: + new \Exception('Favorite state is invalide, should be 0 or 1.'); + } + } +} diff --git a/lib/Sabre/Location/LocationPhoto.php b/lib/Sabre/Location/LocationPhoto.php new file mode 100644 index 000000000..6d4799519 --- /dev/null +++ b/lib/Sabre/Location/LocationPhoto.php @@ -0,0 +1,83 @@ + + * + * @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\Sabre\CollectionPhoto; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\NotFoundException; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\IFile; + +class LocationPhoto extends CollectionPhoto implements IFile { + public function __construct( + private LocationInfo $locationInfo, + LocationFile $file, + private IRootFolder $rootFolder, + Folder $userFolder + ) { + parent::__construct($file, $userFolder); + } + + /** + * @return void + */ + public function delete() { + throw new Forbidden('Cannot remove from a location'); + } + + private function getNode(): Node { + $nodes = $this->rootFolder + ->getUserFolder($this->locationInfo->getUserId()) + ->getById($this->file->getFileId()); + + $node = current($nodes); + + if ($node) { + return $node; + } else { + throw new NotFoundException("Photo not found for user"); + } + } + + public function get() { + $node = $this->getNode(); + + if ($node instanceof File) { + return $node->fopen('r'); + } else { + throw new NotFoundException("Photo is a folder"); + } + } + + public function getFileInfo(): Node { + return $this->getNode(); + } +} diff --git a/lib/Sabre/Location/LocationRoot.php b/lib/Sabre/Location/LocationRoot.php new file mode 100644 index 000000000..fc3fc681c --- /dev/null +++ b/lib/Sabre/Location/LocationRoot.php @@ -0,0 +1,152 @@ + + * + * @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\ReverseGeoCoderService; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; + +class LocationRoot implements ICollection { + /** @var LocationFile[]|null */ + protected ?array $children = null; + + public function __construct( + protected LocationMapper $locationMapper, + protected ReverseGeoCoderService $reverseGeoCoderService, + protected LocationInfo $locationInfo, + protected string $userId, + protected IRootFolder $rootFolder, + ) { + } + + /** + * @return never + */ + public function delete() { + throw new Forbidden('Not allowed to delete a location collection'); + } + + public function getName(): string { + return $this->locationInfo->getLocation(); + } + + /** + * @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'); + } + + /** + * @return LocationPhoto[] + */ + public function getChildren(): array { + if ($this->children === null) { + $this->children = array_map( + fn (LocationFile $file) => new LocationPhoto($this->locationInfo, $file, $this->rootFolder, $this->rootFolder->getUserFolder($this->userId)), + $this->locationMapper->findFilesForUserAndLocation($this->locationInfo->getUserId(), $this->locationInfo->getLocation()) + ); + } + + return $this->children; + } + + public function getChild($name): LocationPhoto { + try { + [$fileId, $fileName] = explode('-', $name, 2); + $locationFile = $this->locationMapper->findFileForUserAndLocation($this->locationInfo->getUserId(), $this->locationInfo->getLocation(), $fileId, $fileName); + return new LocationPhoto($this->locationInfo, $locationFile, $this->rootFolder, $this->rootFolder->getUserFolder($this->userId)); + } catch (NotFoundException $ex) { + throw new NotFound("File $name not found", 0, $ex); + } + } + + public function childExists($name): bool { + try { + $this->getChild($name); + return true; + } catch (NotFound $e) { + return false; + } + } + + public function getLastModified(): int { + return 0; + } + + public function getFirstPhoto(): int { + $children = $this->getChildren(); + if (count($children) === 0) { + throw new \Exception('No children found for location'); + } + + return $children[0]->getFileId(); + } + + /** + * @return int[] + */ + public function getFileIds(): array { + return array_map(function (LocationPhoto $file) { + return $file->getFileId(); + }, $this->getChildren()); + } + + /** + * @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/Location/LocationsHome.php b/lib/Sabre/Location/LocationsHome.php new file mode 100644 index 000000000..89547b3cb --- /dev/null +++ b/lib/Sabre/Location/LocationsHome.php @@ -0,0 +1,114 @@ + + * + * @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\ReverseGeoCoderService; +use OCP\Files\NotFoundException; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; + +class LocationsHome implements ICollection { + public const NAME = 'locations'; + + /** + * @var LocationRoot[] + */ + protected ?array $children = null; + + public function __construct( + protected string $userId, + protected IRootFolder $rootFolder, + protected ReverseGeoCoderService $reverseGeoCoderService, + protected 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): LocationRoot { + try { + $locationInfo = $this->locationMapper->findLocationForUser($this->userId, $name); + return new LocationRoot($this->locationMapper, $this->reverseGeoCoderService, $locationInfo, $this->userId, $this->rootFolder); + } catch (NotFoundException $ex) { + throw new NotFound("Location $name does not exist", 0, $ex); + } + } + + /** + * @return LocationRoot[] + */ + public function getChildren(): array { + if ($this->children === null) { + $this->children = array_map( + fn (LocationInfo $locationInfo) => new LocationRoot($this->locationMapper, $this->reverseGeoCoderService, $locationInfo, $this->userId, $this->rootFolder), + $this->locationMapper->findLocationsForUser($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/PhotosHome.php b/lib/Sabre/PhotosHome.php index fc2e6b4cf..9e7f0b854 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\LocationsHome; +use OCA\Photos\Service\ReverseGeoCoderService; use OCA\Photos\Service\UserConfigService; use OCP\Files\IRootFolder; use OCP\IUserManager; @@ -35,30 +38,17 @@ use Sabre\DAV\ICollection; class PhotosHome implements ICollection { - private AlbumMapper $albumMapper; - private array $principalInfo; - private string $userId; - private IRootFolder $rootFolder; - private IUserManager $userManager; - private IGroupManager $groupManager; - private UserConfigService $userConfigService; - public function __construct( - array $principalInfo, - AlbumMapper $albumMapper, - string $userId, - IRootFolder $rootFolder, - IUserManager $userManager, - IGroupManager $groupManager, - UserConfigService $userConfigService + private array $principalInfo, + private AlbumMapper $albumMapper, + private LocationMapper $locationMapper, + private ReverseGeoCoderService $reverseGeoCoderService, + private string $userId, + private IRootFolder $rootFolder, + private IUserManager $userManager, + private IGroupManager $groupManager, + private UserConfigService $userConfigService, ) { - $this->principalInfo = $principalInfo; - $this->albumMapper = $albumMapper; - $this->userId = $userId; - $this->rootFolder = $rootFolder; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->userConfigService = $userConfigService; } /** @@ -97,6 +87,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 LocationsHome::NAME: + return new LocationsHome($this->userId, $this->rootFolder, $this->reverseGeoCoderService, $this->locationMapper); } throw new NotFound(); @@ -109,11 +101,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 LocationsHome($this->userId, $this->rootFolder, $this->reverseGeoCoderService, $this->locationMapper), ]; } public function childExists($name): bool { - return $name === AlbumsHome::NAME || $name === SharedAlbumsHome::NAME; + return $name === AlbumsHome::NAME || $name === SharedAlbumsHome::NAME || $name === LocationsHome::NAME; } public function getLastModified(): int { diff --git a/lib/Sabre/Album/PropFindPlugin.php b/lib/Sabre/PropFindPlugin.php similarity index 86% rename from lib/Sabre/Album/PropFindPlugin.php rename to lib/Sabre/PropFindPlugin.php index 9f3572cef..832942315 100644 --- a/lib/Sabre/Album/PropFindPlugin.php +++ b/lib/Sabre/PropFindPlugin.php @@ -21,11 +21,15 @@ * */ -namespace OCA\Photos\Sabre\Album; +namespace OCA\Photos\Sabre; use OC\Metadata\IMetadataManager; use OCA\DAV\Connector\Sabre\FilesPlugin; use OCA\Photos\Album\AlbumMapper; +use OCA\Photos\Sabre\Album\AlbumPhoto; +use OCA\Photos\Sabre\Album\AlbumRoot; +use OCA\Photos\Sabre\Location\LocationPhoto; +use OCA\Photos\Sabre\Location\LocationRoot; use OCP\IConfig; use OCP\IPreview; use OCP\Files\NotFoundException; @@ -46,8 +50,6 @@ class PropFindPlugin extends ServerPlugin { public const NBITEMS_PROPERTYNAME = '{http://nextcloud.org/ns}nbItems'; public const COLLABORATORS_PROPERTYNAME = '{http://nextcloud.org/ns}collaborators'; - public const TAG_FAVORITE = '_$!!$_'; - private IConfig $config; private IMetadataManager $metadataManager; private IPreview $previewManager; @@ -91,7 +93,7 @@ public function initialize(Server $server) { } public function propFind(PropFind $propFind, INode $node): void { - if ($node instanceof AlbumPhoto) { + if ($node instanceof AlbumPhoto || $node instanceof LocationPhoto) { // Checking if the node is truly available and ignoring if not // Should be pre-emptively handled by the NodeDeletedEvent try { @@ -144,6 +146,22 @@ public function propFind(PropFind $propFind, INode $node): void { } } } + + if ($node instanceof LocationRoot) { + $propFind->handle(self::LAST_PHOTO_PROPERTYNAME, fn () => $node->getFirstPhoto()); + $propFind->handle(self::NBITEMS_PROPERTYNAME, fn () => count($node->getChildren())); + + // TODO detect dynamically which metadata groups are requested and + // preload all of them and not just size + if ($this->metadataEnabled && in_array(FilesPlugin::FILE_METADATA_SIZE, $propFind->getRequestedProperties(), true)) { + $fileIds = $node->getFileIds(); + $preloadedMetadata = $this->metadataManager->fetchMetadataFor('size', $fileIds); + + foreach ($node->getChildren() as $file) { + $file->getFile()->setMetadata('size', $preloadedMetadata[$file->getFileId()]); + } + } + } } public function handleUpdateProperties($path, PropPatch $propPatch): void { diff --git a/lib/Sabre/RootCollection.php b/lib/Sabre/RootCollection.php index 8bcb42c7e..c6c86ad8b 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\ReverseGeoCoderService; use OCA\Photos\Service\UserConfigService; use OCP\Files\IRootFolder; use OCP\IUserSession; @@ -33,30 +35,18 @@ use OCP\IGroupManager; class RootCollection extends AbstractPrincipalCollection { - private AlbumMapper $folderMapper; - private IUserSession $userSession; - private IRootFolder $rootFolder; - private IUserManager $userManager; - private IGroupManager $groupManager; - private UserConfigService $userConfigService; - public function __construct( - AlbumMapper $folderMapper, - IUserSession $userSession, - IRootFolder $rootFolder, + private AlbumMapper $albumMapper, + private LocationMapper $locationMapper, + private ReverseGeoCoderService $reverseGeoCoderService, + private IUserSession $userSession, + private IRootFolder $rootFolder, PrincipalBackend\BackendInterface $principalBackend, - IUserManager $userManager, - IGroupManager $groupManager, - UserConfigService $userConfigService + private IUserManager $userManager, + private IGroupManager $groupManager, + private UserConfigService $userConfigService, ) { parent::__construct($principalBackend, 'principals/users'); - - $this->folderMapper = $folderMapper; - $this->userSession = $userSession; - $this->rootFolder = $rootFolder; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->userConfigService = $userConfigService; } /** @@ -74,7 +64,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->reverseGeoCoderService, $name, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService); } public function getName(): string {