summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLouis Chemineau <louis@chmn.me>2023-02-22 17:43:52 +0100
committerLouis Chemineau <louis@chmn.me>2023-02-23 18:31:09 +0100
commit5bd766cb929ee2d9016048aa553f2ae4fdb6c0e2 (patch)
tree510fd0867f4c4460f0dc3d16ffa628ff531dd6cf
parent4ed1fc11a67c293d6172d3e87fbc45196808a004 (diff)
Add DAV endpoint for location grouping
Signed-off-by: Louis Chemineau <louis@chmn.me>
-rw-r--r--appinfo/info.xml2
-rw-r--r--lib/DB/Location/LocationMapper.php90
-rw-r--r--lib/Sabre/Album/AlbumPhoto.php130
-rw-r--r--lib/Sabre/Album/AlbumRoot.php4
-rw-r--r--lib/Sabre/CollectionPhoto.php119
-rw-r--r--lib/Sabre/Location/LocationPhoto.php83
-rw-r--r--lib/Sabre/Location/LocationRoot.php152
-rw-r--r--lib/Sabre/Location/LocationsHome.php114
-rw-r--r--lib/Sabre/PhotosHome.php39
-rw-r--r--lib/Sabre/PropFindPlugin.php (renamed from lib/Sabre/Album/PropFindPlugin.php)26
-rw-r--r--lib/Sabre/RootCollection.php32
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[]
+