diff options
author | Louis Chemineau <louis@chmn.me> | 2023-02-22 17:43:52 +0100 |
---|---|---|
committer | Louis Chemineau <louis@chmn.me> | 2023-02-23 18:31:09 +0100 |
commit | 5bd766cb929ee2d9016048aa553f2ae4fdb6c0e2 (patch) | |
tree | 510fd0867f4c4460f0dc3d16ffa628ff531dd6cf | |
parent | 4ed1fc11a67c293d6172d3e87fbc45196808a004 (diff) |
Add DAV endpoint for location grouping
Signed-off-by: Louis Chemineau <louis@chmn.me>
-rw-r--r-- | appinfo/info.xml | 2 | ||||
-rw-r--r-- | lib/DB/Location/LocationMapper.php | 90 | ||||
-rw-r--r-- | lib/Sabre/Album/AlbumPhoto.php | 130 | ||||
-rw-r--r-- | lib/Sabre/Album/AlbumRoot.php | 4 | ||||
-rw-r--r-- | lib/Sabre/CollectionPhoto.php | 119 | ||||
-rw-r--r-- | lib/Sabre/Location/LocationPhoto.php | 83 | ||||
-rw-r--r-- | lib/Sabre/Location/LocationRoot.php | 152 | ||||
-rw-r--r-- | lib/Sabre/Location/LocationsHome.php | 114 | ||||
-rw-r--r-- | lib/Sabre/PhotosHome.php | 39 | ||||
-rw-r--r-- | lib/Sabre/PropFindPlugin.php (renamed from lib/Sabre/Album/PropFindPlugin.php) | 26 | ||||
-rw-r--r-- | lib/Sabre/RootCollection.php | 32 |
11 files changed, 620 insertions, 171 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml index 8ea43379..67c70b74 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -41,7 +41,7 @@ <collection>OCA\Photos\Sabre\PublicRootCollection</collection> </collections> <plugins> - <plugin>OCA\Photos\Sabre\Album\PropFindPlugin</plugin> + <plugin>OCA\Photos\Sabre\PropFindPlugin</plugin> </plugins> </sabre> diff --git a/lib/DB/Location/LocationMapper.php b/lib/DB/Location/LocationMapper.php index e3b9cac3..0326c69f 100644 --- a/lib/DB/Location/LocationMapper.php +++ b/lib/DB/Location/LocationMapper.php @@ -29,6 +29,7 @@ use Doctrine\DBAL\Exception\UniqueConstraintViolationException; 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 @@ class LocationMapper { /** @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 @@ class LocationMapper { 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 @@ class LocationMapper { ); } + 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 832bcb97..ec3e1f35 100644 --- a/lib/Sabre/Album/AlbumPhoto.php +++ b/lib/Sabre/Album/AlbumPhoto.php @@ -26,90 +26,36 @@ namespace OCA\Photos\Sabre\Album; 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 = '_$!<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 @@ class AlbumPhoto implements IFile { } } - 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 0a10157b..fe4f0341 100644 --- a/lib/Sabre/Album/AlbumRoot.php +++ b/lib/Sabre/Album/AlbumRoot.php @@ -129,14 +129,14 @@ class AlbumRoot implements ICollection, ICopyTarget { 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 00000000..b280b804 --- /dev/null +++ b/lib/Sabre/CollectionPhoto.php @@ -0,0 +1,119 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +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 00000000..6d479951 --- /dev/null +++ b/lib/Sabre/Location/LocationPhoto.php @@ -0,0 +1,83 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me> + * + * @author Louis Chemineau <louis@chmn.me> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +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 00000000..fc3fc681 --- /dev/null +++ b/lib/Sabre/Location/LocationRoot.php @@ -0,0 +1,152 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me> + * + * @author Louis Chemineau <louis@chmn.me> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +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[] + |