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