diff --git a/appinfo/info.xml b/appinfo/info.xml index 80fcd66b6..9ae3c4cbc 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -4,7 +4,7 @@ Photos Your memories under your control Your memories under your control - 1.8.0 + 1.9.0 agpl John Molakvoæ Photos diff --git a/lib/Album/AlbumFile.php b/lib/Album/AlbumFile.php index d6f09caa2..5e8f776d4 100644 --- a/lib/Album/AlbumFile.php +++ b/lib/Album/AlbumFile.php @@ -24,8 +24,9 @@ namespace OCA\Photos\Album; use OC\Metadata\FileMetadata; +use OCA\Photos\DB\PhotosFile; -class AlbumFile { +class AlbumFile extends PhotosFile{ private int $fileId; private string $name; private string $mimeType; diff --git a/lib/Command/DownloadReverseGeocodingFiles.php b/lib/Command/DownloadReverseGeocodingFiles.php index 19d0dec40..edb3ff1fd 100644 --- a/lib/Command/DownloadReverseGeocodingFiles.php +++ b/lib/Command/DownloadReverseGeocodingFiles.php @@ -59,7 +59,7 @@ protected function configure() { */ protected function execute(InputInterface $input, OutputInterface $output): int { try { - $this->rgcService->initCities1000KdTree(true); + $this->rgcService->createCities1000KdTree(true); } catch (\Exception $ex) { $output->writeln('Failed to update reverse geocoding files'); $output->writeln($ex->getMessage()); diff --git a/lib/Command/ReverseGeoCodeMedia.php b/lib/Command/ReverseGeoCodeMedia.php index 201b3fd6f..2c426cf2b 100644 --- a/lib/Command/ReverseGeoCodeMedia.php +++ b/lib/Command/ReverseGeoCodeMedia.php @@ -29,34 +29,30 @@ use OCP\Files\IRootFolder; use OCP\Files\Folder; use OCP\BackgroundJob\IJobList; -use OCA\Photos\Service\ReverseGeoCoderService; -use OCA\Photos\Service\LocationTagService; +use OCA\Photos\Service\MediaLocationManager; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class ReverseGeoCodeMedia extends Command { - private ReverseGeoCoderService $rgcService; private IRootFolder $rootFolder; - private LocationTagService $locationTagService; + private MediaLocationManager $mediaLocationManager; private IConfig $config; private IUserManager $userManager; public function __construct( - ReverseGeoCoderService $rgcService, IJobList $jobList, IRootFolder $rootFolder, - LocationTagService $locationTagService, + MediaLocationManager $mediaLocationManager, IConfig $config, IUserManager $userManager ) { parent::__construct(); - $this->rgcService = $rgcService; $this->config = $config; $this->jobList = $jobList; $this->rootFolder = $rootFolder; - $this->locationTagService = $locationTagService; + $this->mediaLocationManager = $mediaLocationManager; $this->userManager = $userManager; } @@ -120,7 +116,7 @@ private function scanFolder(Folder $folder) { continue; } - $this->locationTagService->tag($node->getId()); + $this->mediaLocationManager->addLocationForFileAndUser($node->getOwner()->getUID(), $node->getId()); } } } diff --git a/lib/DB/Location/LocationFile.php b/lib/DB/Location/LocationFile.php new file mode 100644 index 000000000..c6f6bfcea --- /dev/null +++ b/lib/DB/Location/LocationFile.php @@ -0,0 +1,57 @@ + + * + * @author Louis Chemineau + * + * @license AGPL-3.0-or-later + * + * 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\DB\Location; + +use OCA\Photos\DB\PhotosFile; + +class LocationFile extends PhotosFile { + private int $locationId; + + public function __construct( + int $fileId, + string $name, + string $mimeType, + int $size, + int $mtime, + string $etag, + int $locationId, + ) { + parent::__construct( + $fileId, + $name, + $mimeType, + $size, + $mtime, + $etag + ); + + $this->locationId = $locationId; + } + + public function getLocationId(): int { + return $this->locationId; + } +} diff --git a/lib/DB/Location/LocationInfo.php b/lib/DB/Location/LocationInfo.php new file mode 100644 index 000000000..ae292e43e --- /dev/null +++ b/lib/DB/Location/LocationInfo.php @@ -0,0 +1,47 @@ + + * + * @author Louis Chemineau + * + * @license AGPL-3.0-or-later + * + * 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\DB\Location; + +class LocationInfo { + private string $userId; + private int $locationId; + + public function __construct( + string $userId, + int $locationId, + ) { + $this->userId = $userId; + $this->locationId = $locationId; + } + + public function getUserId(): string { + return $this->userId; + } + + public function getLocationId(): int { + return $this->locationId; + } +} diff --git a/lib/DB/Location/LocationMapper.php b/lib/DB/Location/LocationMapper.php new file mode 100644 index 000000000..39ecb2a5f --- /dev/null +++ b/lib/DB/Location/LocationMapper.php @@ -0,0 +1,109 @@ + + * + * @author Louis Chemineau + * + * @license AGPL-3.0-or-later + * + * 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\DB\Location; + +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class LocationMapper { + const PHOTOS_LOCATION = 'photos_locations'; + + private IDBConnection $connection; + + public function __construct( + IDBConnection $connection, + ) { + $this->connection = $connection; + } + + /** @return array */ + public function findLocationForUser(string $userId): array { + $qb = $this->connection->getQueryBuilder(); + + $rows = $qb->selectDistinct('location_id') + ->from(self::PHOTOS_LOCATION) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))) + ->executeQuery() + ->fetchAll(); + + return array_map(fn ($row) => new LocationInfo($userId, $row['location_id']), $rows); + } + + /** @return array */ + public function findFilesForUserAndLocation(string $userId, int $locationId) { + $qb = $this->db->getQueryBuilder(); + + $rows = $qb->select("fileid", "name", "mimetype", "size", "mtime", "etag", "location_id") + ->from(self::PHOTOS_LOCATION, 'l') + ->leftJoin("p", "filecache", "f", $qb->expr()->eq("l.file_id", "f.fileid")) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))) + ->andWhere($qb->expr()->eq('location_id', $qb->createNamedParameter($locationId, IQueryBuilder::PARAM_INT))); + + return array_map( + fn ($row) => new LocationFile( + (int)$row['fileid'], + $row['name'], + $this->mimeTypeLoader->getMimetypeById($row['mimetype']), + (int)$row['size'], + (int)$row['mtime'], + $row['etag'], + (int)$row['location_id'] + ), + $rows + ); + } + + public function addFile(string $userId, int $locationId, int $fileId): void { + try { + $query = $this->connection->getQueryBuilder(); + $query->insert(self::PHOTOS_LOCATION) + ->values([ + "user_id" => $query->createNamedParameter($userId), + "location_id" => $query->createNamedParameter($locationId, IQueryBuilder::PARAM_INT), + "file_id" => $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT), + ]) + ->executeStatement(); + } catch (UniqueConstraintViolationException $ex) { + $this->updateFileLocationForUser($userId, $locationId, $fileId); + } + } + + public function updateFileLocation(int $locationId, int $fileId): void { + $query = $this->connection->getQueryBuilder(); + $query->update(self::PHOTOS_LOCATION) + ->set("location_id", $query->createNamedParameter($locationId, IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('file_id', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->executeStatement(); + } + + public function removeFileLocation(int $fileId): void { + $query = $this->connection->getQueryBuilder(); + $query->delete(self::PHOTOS_LOCATION) + ->where($query->expr()->eq('file_id', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->executeStatement(); + } +} \ No newline at end of file diff --git a/lib/DB/PhotosFile.php b/lib/DB/PhotosFile.php new file mode 100644 index 000000000..d78ba8443 --- /dev/null +++ b/lib/DB/PhotosFile.php @@ -0,0 +1,91 @@ + + * + * @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\DB; + +use OC\Metadata\FileMetadata; + +class PhotosFile { + private int $fileId; + private string $name; + private string $mimeType; + private int $size; + private int $mtime; + private string $etag; + /** @var array */ + private array $metaData = []; + + public function __construct( + int $fileId, + string $name, + string $mimeType, + int $size, + int $mtime, + string $etag, + ) { + $this->fileId = $fileId; + $this->name = $name; + $this->mimeType = $mimeType; + $this->size = $size; + $this->mtime = $mtime; + $this->etag = $etag; + } + + public function getFileId(): int { + return $this->fileId; + } + + public function getName(): string { + return $this->name; + } + + public function getMimeType(): string { + return $this->mimeType; + } + + public function getSize(): int { + return $this->size; + } + + public function getMTime(): int { + return $this->mtime; + } + + public function getEtag(): string { + return $this->etag; + } + + public function setMetadata(string $key, FileMetadata $value): void { + $this->metaData[$key] = $value; + } + + public function hasMetadata(string $key): bool { + return isset($this->metaData[$key]); + } + + public function getMetadata(string $key): FileMetadata { + return $this->metaData[$key]; + } +} diff --git a/lib/Listener/LocationTagNodeEventListener.php b/lib/Listener/LocationTagNodeEventListener.php index c6fc2fb3e..854470144 100644 --- a/lib/Listener/LocationTagNodeEventListener.php +++ b/lib/Listener/LocationTagNodeEventListener.php @@ -2,7 +2,7 @@ namespace OCA\Photos\Listener; -use OCA\Photos\Service\LocationTagService; +use OCA\Photos\Service\PhotosLocationManager; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\Files\Cache\CacheEntryRemovedEvent; @@ -10,25 +10,32 @@ use OCP\Files\Events\Node\NodeCreatedEvent; class LocationTagNodeEventListener implements IEventListener { - private LocationTagService $locationTagService; + private PhotosLocationManager $photosLocationManager; public function __construct( - LocationTagService $locationTagService, + PhotosLocationManager $photosLocationManager, ) { - $this->locationTagService = $locationTagService; + $this->photosLocationManager = $photosLocationManager; } public function handle(Event $event): void { + if (!str_starts_with($event->getNode()->getMimeType(), 'image')) { + return; + } + if ($event instanceof CacheEntryRemovedEvent) { - $this->locationTagService->unTag($event->getFileId()); + $this->photosLocationManager->clearLocationForFile($event->getFileId()); } - if ($event instanceof NodeWrittenEvent || $event instanceof NodeCreatedEvent) { - if (!str_starts_with($event->getNode()->getMimeType(), 'image')) { - return; - } + // TODO: this will load the KDTree, so put that in a job. + if ($event instanceof NodeWrittenEvent) { + $userId = $event->getNode()->getOwner()->getUID(); + $this->locationMapper->addLocationForFileAndUser($userId, $event->getNode()->getId()); + } - $this->locationTagService->tag($event->getNode()->getId()); + // TODO: this will load the KDTree, so put that in a job. + if ($event instanceof NodeCreatedEvent) { + $this->locationMapper->updateLocationForFile($event->getNode()->getId()); } } } diff --git a/lib/Migration/Version20002Date20221012131022.php b/lib/Migration/Version20002Date20221012131022.php new file mode 100644 index 000000000..8f13b5c74 --- /dev/null +++ b/lib/Migration/Version20002Date20221012131022.php @@ -0,0 +1,73 @@ + + * + * @author Your name + * + * @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\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Auto-generated migration step: Please modify to your needs! + */ +class Version20002Date20221012131022 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + $modified = false; + + if (!$schema->hasTable("photos_locations")) { + $modified = true; + $table = $schema->createTable("photos_locations"); + $table->addColumn('user_id', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('location_id', Types::BIGINT, [ + 'notnull' => true, + ]); + $table->addColumn('file_id', Types::BIGINT, [ + 'notnull' => true, + ]); + + $table->addUniqueConstraint(['user_id', 'location_id', 'file_id'], 'locations_unique_idx'); + } + + if ($modified) { + return $schema; + } else { + return null; + } + } +} diff --git a/lib/Sabre/Location/LocationHome.php b/lib/Sabre/Location/LocationHome.php index a853395f3..01a463ed5 100644 --- a/lib/Sabre/Location/LocationHome.php +++ b/lib/Sabre/Location/LocationHome.php @@ -2,7 +2,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Robin Appelman + * @copyright Copyright (c) 2022 Louis Chemineau + * + * @author Louis Chemineau * * @license GNU AGPL version 3 or any later version * @@ -24,7 +26,9 @@ namespace OCA\Photos\Sabre\Location; use OCP\Files\IRootFolder; -use OCA\Photos\Service\LocationTagService; +use OCA\Photos\DB\Location\LocationInfo; +use OCA\Photos\DB\Location\LocationMapper; +use OCA\Photos\Service\MediaLocationManager; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\ICollection; @@ -33,7 +37,8 @@ class LocationHome implements ICollection { protected array $principalInfo; protected string $userId; protected IRootFolder $rootFolder; - protected LocationTagService $locationTagService; + protected MediaLocationManager $mediaLocationManager; + protected LocationMapper $locationMapper; public const NAME = 'locations'; @@ -46,12 +51,14 @@ public function __construct( array $principalInfo, string $userId, IRootFolder $rootFolder, - LocationTagService $locationTagService + MediaLocationManager $mediaLocationManager, + LocationMapper $locationMapper ) { $this->principalInfo = $principalInfo; $this->userId = $userId; $this->rootFolder = $rootFolder; - $this->locationTagService = $locationTagService; + $this->mediaLocationManager = $mediaLocationManager; + $this->locationMapper = $locationMapper; } /** @@ -95,9 +102,10 @@ public function getChild($name) { */ public function getChildren(): array { if ($this->children === null) { - $this->children = array_map(function (LocationInfo $locationInfo) { - return new LocationRoot($this->locationMapper, new LocationWithFile($locationInfo, $this->locationMapper), $this->rootFolder, $this->userId, $this->userConfigService); - }, $locationInfos); + $this->children = array_map( + fn (LocationInfo $locationInfo) => new LocationRoot($this->locationMapper, $this->mediaLocationManager, $locationInfo, $this->rootFolder), + $this->locationMapper->findLocationForUser($this->userId) + ); } return $this->children; diff --git a/lib/Sabre/Location/LocationPhoto.php b/lib/Sabre/Location/LocationPhoto.php new file mode 100644 index 000000000..09570e640 --- /dev/null +++ b/lib/Sabre/Location/LocationPhoto.php @@ -0,0 +1,168 @@ + + * + * @author Louis Chemineau + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Photos\Sabre\Location; + +use OCA\Photos\DB\Location\LocationFile; +use OCA\Photos\DB\Location\LocationInfo; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\Files\File; +use OCP\Files\NotFoundException; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\IFile; + +class LocationPhoto implements IFile { + private LocationInfo $locationInfo; + private LocationFile $locationFile; + private IRootFolder $rootFolder; + + public const TAG_FAVORITE = '_$!!$_'; + + public function __construct( + LocationInfo $locationInfo, + LocationFile $locationFile, + IRootFolder $rootFolder + ) { + $this->locationInfo = $locationInfo; + $this->locationFile = $locationFile; + $this->rootFolder = $rootFolder; + } + + /** + * @return void + */ + public function delete() { + throw new Forbidden('Cannot remove from a location'); + } + + public function getName() { + return $this->locationFile->getFileId() . "-" . $this->locationFile->getName(); + } + + /** + * @return never + */ + public function setName($name) { + throw new Forbidden('Cannot rename from a location'); + } + + public function getLastModified() { + return $this->locationFile->getMTime(); + } + + public function put($data) { + $nodes = $this->userFolder->getById($this->file->getFileId()); + $node = current($nodes); + if ($node) { + /** @var Node $node */ + if ($node instanceof File) { + return $node->putContent($data); + } else { + throw new NotFoundException("Photo is a folder"); + } + } else { + throw new NotFoundException("Photo not found for user"); + } + } + + public function get() { + $nodes = $this->rootFolder + ->getUserFolder($this->locationInfo->getUserId()) + ->getById($this->locationFile->getFileId()); + $node = current($nodes); + if ($node) { + /** @var Node $node */ + if ($node instanceof File) { + return $node->fopen('r'); + } else { + throw new NotFoundException("Photo is a folder"); + } + } else { + throw new NotFoundException("Photo not found for user"); + } + } + + public function getFileId(): int { + return $this->locationFile->getFileId(); + } + + public function getFileInfo(): Node { + $nodes = $this->rootFolder + ->getUserFolder($this->locationInfo->getUserId()) + ->getById($this->locationFile->getFileId()); + $node = current($nodes); + if ($node) { + return $node->get; + } else { + throw new NotFoundException("Photo not found for user"); + } + } + + public function getContentType() { + return $this->locationFile->getMimeType(); + } + + public function getETag() { + return $this->locationFile->getEtag(); + } + + public function getSize() { + return $this->locationFile->getSize(); + } + + public function getFile(): LocationFile { + return $this->locationFile; + } + + public function isFavorite(): bool { + $tagManager = \OCP\Server::get(\OCP\ITagManager::class); + $tagger = $tagManager->load('files'); + if ($tagger === null) { + return false; + } + $tags = $tagger->getTagsForObjects([$this->getFileId()]); + + if ($tags === false || empty($tags)) { + return false; + } + + return array_search(self::TAG_FAVORITE, current($tags)) !== false; + } + + public function setFavoriteState($favoriteState): bool { + $tagManager = \OCP\Server::get(\OCP\ITagManager::class); + $tagger = $tagManager->load('files'); + + switch ($favoriteState) { + case "0": + return $tagger->removeFromFavorites($this->locationFile->getFileId()); + case "1": + return $tagger->addToFavorites($this->locationFile->getFileId()); + default: + new \Exception('Favorite state is invalide, should be 0 or 1.'); + } + } +} diff --git a/lib/Sabre/Location/LocationRoot.php b/lib/Sabre/Location/LocationRoot.php new file mode 100644 index 000000000..9682002b0 --- /dev/null +++ b/lib/Sabre/Location/LocationRoot.php @@ -0,0 +1,138 @@ + + * + * @author Louis Chemineau + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Photos\Sabre\Location; + +use OCA\Photos\DB\Location\LocationFile; +use OCA\Photos\DB\Location\LocationInfo; +use OCA\Photos\DB\Location\LocationMapper; +use OCA\Photos\Service\MediaLocationManager; +use OCP\Files\IRootFolder; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; + +class LocationRoot implements ICollection { + protected LocationMapper $locationMapper; + protected MediaLocationManager $mediaLocationManager; + protected LocationInfo $locationInfo; + protected IRootFolder $rootFolder; + /** @var array */ + protected ?array $children = null; + + public function __construct( + LocationMapper $locationMapper, + MediaLocationManager $mediaLocationManager, + LocationInfo $locationInfo, + IRootFolder $rootFolder, + ) { + $this->locationMapper = $locationMapper; + $this->mediaLocationManager = $mediaLocationManager; + $this->locationInfo = $locationInfo; + $this->rootFolder = $rootFolder; + } + + /** + * @return never + */ + public function delete() { + throw new Forbidden('Not allowed to delete a location collection'); + } + + public function getName(): string { + // TODO: Get the real name with the MediaLocationManager + return (string)$this->locationInfo->getLocationId(); + } + + /** + * @return never + */ + public function setName($name) { + throw new Forbidden('Cannot change the location collection name'); + } + + /** + * @param string $name + * @param null|resource|string $data + * @return never + */ + public function createFile($name, $data = null) { + throw new Forbidden('Cannot create a file in a location collection'); + } + + /** + * @return never + */ + public function createDirectory($name) { + throw new Forbidden('Not allowed to create directories in this folder'); + } + + public function getChildren(): array { + if ($this->children === null) { + $this->children = array_map( + fn (LocationFile $file) => new LocationPhoto($this->locationInfo, $file, $this->rootFolder), + $this->locationMapper->findFilesForUserAndLocation($this->locationInfo->getUserId(), $this->locationInfo->getLocationId()) + ); + } + + return $this->children; + } + + public function getChild($name): LocationPhoto { + foreach ($this->getChildren() as $child) { + if ($child->getName() === $name) { + return $child; + } + } + + throw new NotFound("$name not found"); + } + + public function childExists($name): bool { + try { + $this->getChild($name); + return true; + } catch (NotFound $e) { + return false; + } + } + + public function getLastModified(): int { + return 0; + } + + /** + * @return int|null + */ + public function getCover() { + $children = $this->getChildren(); + + if (count($children) > 0) { + return $children[0]->getFileId(); + } else { + return null; + } + } +} diff --git a/lib/Sabre/PhotosHome.php b/lib/Sabre/PhotosHome.php index 52d83d2f7..741a477b0 100644 --- a/lib/Sabre/PhotosHome.php +++ b/lib/Sabre/PhotosHome.php @@ -24,8 +24,11 @@ namespace OCA\Photos\Sabre; use OCA\Photos\Album\AlbumMapper; +use OCA\Photos\DB\Location\LocationMapper; use OCA\Photos\Sabre\Album\AlbumsHome; use OCA\Photos\Sabre\Album\SharedAlbumsHome; +use OCA\Photos\Sabre\Location\LocationHome; +use OCA\Photos\Service\MediaLocationManager; use OCA\Photos\Service\UserConfigService; use OCP\Files\IRootFolder; use OCP\IUserManager; @@ -36,6 +39,8 @@ class PhotosHome implements ICollection { private AlbumMapper $albumMapper; + private LocationMapper $locationMapper; + private MediaLocationManager $mediaLocationManager; private array $principalInfo; private string $userId; private IRootFolder $rootFolder; @@ -46,6 +51,8 @@ class PhotosHome implements ICollection { public function __construct( array $principalInfo, AlbumMapper $albumMapper, + LocationMapper $locationMapper, + MediaLocationManager $mediaLocationManager, string $userId, IRootFolder $rootFolder, IUserManager $userManager, @@ -54,6 +61,8 @@ public function __construct( ) { $this->principalInfo = $principalInfo; $this->albumMapper = $albumMapper; + $this->locationMapper = $locationMapper; + $this->mediaLocationManager = $mediaLocationManager; $this->userId = $userId; $this->rootFolder = $rootFolder; $this->userManager = $userManager; @@ -98,7 +107,7 @@ public function getChild($name) { case SharedAlbumsHome::NAME: return new SharedAlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService); case LocationHome::NAME: - return new LocationHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService); + return new LocationHome($this->principalInfo, $this->userId, $this->rootFolder, $this->mediaLocationManager, $this->locationMapper); } throw new NotFound(); @@ -111,6 +120,7 @@ public function getChildren(): array { return [ new AlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userConfigService), new SharedAlbumsHome($this->principalInfo, $this->albumMapper, $this->userId, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService), + new LocationHome($this->principalInfo, $this->userId, $this->rootFolder, $this->mediaLocationManager, $this->locationMapper), ]; } diff --git a/lib/Sabre/RootCollection.php b/lib/Sabre/RootCollection.php index 8bcb42c7e..23611e330 100644 --- a/lib/Sabre/RootCollection.php +++ b/lib/Sabre/RootCollection.php @@ -24,6 +24,8 @@ namespace OCA\Photos\Sabre; use OCA\Photos\Album\AlbumMapper; +use OCA\Photos\DB\Location\LocationMapper; +use OCA\Photos\Service\MediaLocationManager; use OCA\Photos\Service\UserConfigService; use OCP\Files\IRootFolder; use OCP\IUserSession; @@ -33,7 +35,9 @@ use OCP\IGroupManager; class RootCollection extends AbstractPrincipalCollection { - private AlbumMapper $folderMapper; + private AlbumMapper $albumMapper; + private LocationMapper $locationMapper; + private MediaLocationManager $mediaLocationManager; private IUserSession $userSession; private IRootFolder $rootFolder; private IUserManager $userManager; @@ -41,7 +45,9 @@ class RootCollection extends AbstractPrincipalCollection { private UserConfigService $userConfigService; public function __construct( - AlbumMapper $folderMapper, + AlbumMapper $albumMapper, + LocationMapper $locationMapper, + MediaLocationManager $mediaLocationManager, IUserSession $userSession, IRootFolder $rootFolder, PrincipalBackend\BackendInterface $principalBackend, @@ -51,7 +57,9 @@ public function __construct( ) { parent::__construct($principalBackend, 'principals/users'); - $this->folderMapper = $folderMapper; + $this->albumMapper = $albumMapper; + $this->locationMapper = $locationMapper; + $this->mediaLocationManager = $mediaLocationManager; $this->userSession = $userSession; $this->rootFolder = $rootFolder; $this->userManager = $userManager; @@ -74,7 +82,7 @@ public function getChildForPrincipal(array $principalInfo): PhotosHome { if (is_null($user) || $name !== $user->getUID()) { throw new \Sabre\DAV\Exception\Forbidden(); } - return new PhotosHome($principalInfo, $this->folderMapper, $name, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService); + return new PhotosHome($principalInfo, $this->albumMapper, $this->locationMapper, $this->mediaLocationManager, $name, $this->rootFolder, $this->userManager, $this->groupManager, $this->userConfigService); } public function getName(): string { diff --git a/lib/Service/LocationTagService.php b/lib/Service/LocationTagService.php deleted file mode 100644 index f8b25894c..000000000 --- a/lib/Service/LocationTagService.php +++ /dev/null @@ -1,99 +0,0 @@ -systemTagManager = $systemTagManager; - $this->systemTagObjectMapper = $systemTagObjectMapper; - $this->metadataManager = $metadataManager; - $this->rgcService = $rgcService; - } - - public function tag(int $fileId) { - $locationId = $this->getLocationId($fileId); - if ($locationId === -1) { - return; - } - - $locationTagName = self::LOCATION_TAG_PREFIX.':'.$locationId; - - $existingLocationTag = $this->getTagForFileId($fileId); - if ($existingLocationTag !== null && $existingLocationTag->getName() === $locationTagName) { - return; - } - - $this->unTag($fileId); - $systemTag = $this->createTagIfNoExist($locationTagName); - $this->systemTagObjectMapper->assignTags($fileId, 'files', [$systemTag->getId()]); - } - - public function unTag(int $fileId): void { - $locationTag = $this->getTagForFileId($fileId); - - if ($locationTag === null) { - return; - } - - $this->systemTagObjectMapper->unassignTags($fileId, 'files', $locationTag->getId()); - - $otherFileIds = $this->systemTagObjectMapper->getObjectIdsForTags([$locationTag->getId()], 'files'); - if (count($otherFileIds) === 0) { - $this->systemTagManager->deleteTags([$locationTag->getId()]); - } - } - - public function getFileIdsForUser(string $userId): array { - // $this->systemTagManager - } - - private function getLocationId(int $fileId): int { - $gpsMetadata = $this->metadataManager->fetchMetadataFor('gps', [$fileId])[$fileId]; - $metadata = $gpsMetadata->getMetadata(); - $latitude = $metadata['latitude']; - $longitude = $metadata['longitude']; - - if ($latitude === null || $longitude === null) { - return -1; - } - - return $this->rgcService->getLocationIdForCoordinates($latitude, $longitude); - } - - private function createTagIfNoExist(string $tagName): ISystemTag { - try { - return $this->systemTagManager->getTag($tagName, false, false); - } catch (\Exception $ex) { - if ($ex instanceof TagNotFoundException) { - return $this->systemTagManager->createTag($tagName, false, false); - } - - throw $ex; - } - } - - private function getTagForFileId(int $fileId): ISystemTag|null { - $tagIds = $this->systemTagObjectMapper->getTagIdsForObjects([$fileId], 'files')[$fileId]; - $tags = $this->systemTagManager->getTagsByIds($tagIds); - $locationTags = array_filter($tags, fn (ISystemTag $tag) => str_starts_with($tag->getName(), self::LOCATION_TAG_PREFIX)); - return array_pop($locationTags); - } -} diff --git a/lib/Service/MediaLocationManager.php b/lib/Service/MediaLocationManager.php new file mode 100644 index 000000000..6a0acf936 --- /dev/null +++ b/lib/Service/MediaLocationManager.php @@ -0,0 +1,83 @@ + + * + * @author Louis Chemineau + * + * @license AGPL-3.0-or-later + * + * 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\Service; + +use OC\Metadata\MetadataManager; +use OCA\Photos\Service\ReverseGeoCoderService; +use OCA\Photos\DB\Location\LocationMapper; + +class MediaLocationManager { + private MetadataManager $metadataManager; + private ReverseGeoCoderService $rgcService; + private LocationMapper $locationMapper; + + public function __construct( + MetadataManager $metadataManager, + ReverseGeoCoderService $rgcService, + LocationMapper $locationMapper + ) { + $this->metadataManager = $metadataManager; + $this->rgcService = $rgcService; + $this->locationMapper = $locationMapper; + } + + public function addLocationForFileAndUser(string $userId, int $fileId) { + $locationId = $this->getLocationId($fileId); + + if ($locationId === -1) { + return; + } + + $this->locationMapper->addFile($userId, $locationId, $fileId); + } + + public function updateLocationForFile(int $fileId) { + $locationId = $this->getLocationId($fileId); + + if ($locationId === -1) { + return; + } + + $this->locationMapper->updateFileLocation($locationId, $fileId); + } + + public function clearLocationForFile(int $fileId): void { + $this->locationMapper->removeFileLocation($fileId); + } + + private function getLocationId(int $fileId): int { + $gpsMetadata = $this->metadataManager->fetchMetadataFor('gps', [$fileId])[$fileId]; + $metadata = $gpsMetadata->getMetadata(); + $latitude = $metadata['latitude']; + $longitude = $metadata['longitude']; + + if ($latitude === null || $longitude === null) { + return -1; + } + + return $this->rgcService->getLocationIdForCoordinates($latitude, $longitude); + } +} diff --git a/lib/Service/ReverseGeoCoderService.php b/lib/Service/ReverseGeoCoderService.php index b4496a3a0..f4e759c21 100644 --- a/lib/Service/ReverseGeoCoderService.php +++ b/lib/Service/ReverseGeoCoderService.php @@ -75,7 +75,7 @@ public function getLocationIdForCoordinates(float $latitude, float $longitude): } // All cities with a population > 1000 or seats of adm div down to PPLA3 (ca 130.000), see 'geoname' table for columns - public function initCities1000KdTree(bool $force = false) { + public function createCities1000KdTree(bool $force = false) { if ($this->geoNameFolder->fileExists('cities1000.bin') && !$force) { return; } @@ -122,6 +122,7 @@ private function buildKDTree(string $fileContent): KDTree { } private function loadKDTree($fileName): NearestSearch { + $this->createCities1000KdTree(); $kdTreeFileContent = $this->geoNameFolder->getFile($fileName)->getContent(); $kdTreeTmpFileName = tempnam(sys_get_temp_dir(), "nextcloud_photos_"); file_put_contents($kdTreeTmpFileName, $kdTreeFileContent); diff --git a/package.json b/package.json index 53cb2de7f..2479bbf56 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "photos", "description": "Your memories under your control", - "version": "1.8.0", + "version": "1.9.0", "author": "John Molakvoæ ", "contributors": [ "John Molakvoæ " @@ -95,4 +95,4 @@ "wait-on": "^6.0.1", "workbox-webpack-plugin": "^6.5.4" } -} +} \ No newline at end of file