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 {