Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/photos.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLouis Chemineau <louis@chmn.me>2022-11-08 14:02:45 +0300
committerLouis Chemineau <louis@chmn.me>2022-11-08 15:49:17 +0300
commitd88e010f055f892388bd6b1dbacbb7c1261b2012 (patch)
treea195b6e7124a52118355684e09b1a10d88877250
parent05c3c2f2271b5ee58c1a0b9a7b3f55573230d20b (diff)
Add commands and listeners to generate location data of filesartonge/feat/reverse_geocoding_cli
- `ReverseGeoCoderService` download the necessary files and build the `KDTree` - `UpdateReverseGeocodingFilesCommand` command to allow to manually create the needed reverse geocoding files - `MediaLocationManager` to manager the location mappings - `MapMediaToLocationCommand` command to manually trigger location data mapping. Useful for pre-existing pictures. - `LocationManagerNodeEventListener` to react to node, user and share events. - `MapMediaToLocationJob` to reduce the load in event listeners ┌─────────────────────┐ ┌────────────►│MapMediaToLocationJob│ │ └─────────┬───────────┘ │ │ ┌────────────────────────┴───────┐ │ │LocationManagerNodeEventListener├──┐ ▼ └────────────────────────────────┘ │ ┌────────────────────┐ ┌──────────────┐ ├─►│MediaLocationManager├────►│LocationMapper│ ┌─────────────────────────┐ │ └─────────┬──────────┘ └──────────────┘ │MapMediaToLocationCommand├─────────┘ │ └─────────────────────────┘ │ ▼ ┌──────────────────────────────────┐ ┌──────────────────────┐ │UpdateReverseGeocodingFilesCommand├──►│ReverseGeoCoderService│ └──────────────────────────────────┘ └──────────────────────┘ Signed-off-by: Louis Chemineau <louis@chmn.me>
-rw-r--r--.github/workflows/cypress.yml5
-rw-r--r--appinfo/info.xml18
-rw-r--r--composer.json3
-rw-r--r--composer.lock66
-rw-r--r--lib/Album/AlbumFile.php63
-rw-r--r--lib/AppInfo/Application.php16
-rw-r--r--lib/Command/MapMediaToLocationCommand.php117
-rw-r--r--lib/Command/UpdateReverseGeocodingFilesCommand.php71
-rw-r--r--lib/DB/Location/LocationFile.php57
-rw-r--r--lib/DB/Location/LocationInfo.php47
-rw-r--r--lib/DB/Location/LocationMapper.php128
-rw-r--r--lib/DB/PhotosFile.php91
-rw-r--r--lib/Jobs/MapMediaToLocationJob.php51
-rw-r--r--lib/Listener/LocationManagerNodeEventListener.php131
-rw-r--r--lib/Migration/Version20002Date20221012131022.php79
-rw-r--r--lib/Service/MediaLocationManager.php91
-rw-r--r--lib/Service/ReverseGeoCoderService.php167
-rw-r--r--package.json2
-rw-r--r--tests/stub.phpstub30
19 files changed, 1155 insertions, 78 deletions
diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml
index bae6e124..bf5fac24 100644
--- a/.github/workflows/cypress.yml
+++ b/.github/workflows/cypress.yml
@@ -21,6 +21,9 @@ jobs:
- name: Checkout app
uses: actions/checkout@v3
+ - name: Install server dependencies
+ run: composer install
+
- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@v1.1
id: versions
@@ -36,7 +39,7 @@ jobs:
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
- - name: Install dependencies & build app
+ - name: Install node dependencies & build app
run: |
npm ci
TESTING=true npm run build --if-present
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 140167a4..0f6774bd 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -1,13 +1,12 @@
<?xml version="1.0"?>
-<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
- xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
+<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>photos</id>
<name>Photos</name>
<summary>Your memories under your control</summary>
<description>Your memories under your control</description>
- <version>2.2.0</version>
+ <version>2.2.1</version>
<licence>agpl</licence>
- <author mail="skjnldsv@protonmail.com">John Molakvoæ</author>
+ <author mail="skjnldsv@protonmail.com">John Molakvoæ</author>
<namespace>Photos</namespace>
<category>multimedia</category>
<types>
@@ -15,8 +14,8 @@
<authentication />
</types>
- <website>https://github.com/nextcloud/photos</website>
- <bugs>https://github.com/nextcloud/photos/issues</bugs>
+ <website>https://github.com/nextcloud/photos</website>
+ <bugs>https://github.com/nextcloud/photos/issues</bugs>
<repository>https://github.com/nextcloud/photos.git</repository>
<default_enable />
<dependencies>
@@ -30,6 +29,11 @@
</navigation>
</navigations>
+ <commands>
+ <command>OCA\Photos\Command\UpdateReverseGeocodingFilesCommand</command>
+ <command>OCA\Photos\Command\MapMediaToLocationCommand</command>
+ </commands>
+
<sabre>
<collections>
<collection>OCA\Photos\Sabre\RootCollection</collection>
@@ -39,4 +43,4 @@
<plugin>OCA\Photos\Sabre\Album\PropFindPlugin</plugin>
</plugins>
</sabre>
-</info>
+</info> \ No newline at end of file
diff --git a/composer.json b/composer.json
index 595b6bc8..c03b850e 100644
--- a/composer.json
+++ b/composer.json
@@ -20,5 +20,8 @@
"vimeo/psalm": "^4.22",
"sabre/dav": "^4.2.1",
"nextcloud/ocp": "dev-master"
+ },
+ "require": {
+ "hexogen/kdtree": "^0.2.0"
}
}
diff --git a/composer.lock b/composer.lock
index dcb41dd3..25d0f052 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,70 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "35d14b088efbd1d2f344adcab31683b3",
- "packages": [],
+ "content-hash": "807a478aabd3b0507dfa0c5bfea979f7",
+ "packages": [
+ {
+ "name": "hexogen/kdtree",
+ "version": "0.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/hexogen/kdtree.git",
+ "reference": "5d75517670f7ecf149688757f8540c2648bd5230"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/hexogen/kdtree/zipball/5d75517670f7ecf149688757f8540c2648bd5230",
+ "reference": "5d75517670f7ecf149688757f8540c2648bd5230",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~7.1"
+ },
+ "require-dev": {
+ "league/csv": "^8.0",
+ "mockery/mockery": "dev-master",
+ "phpunit/phpunit": "^7.0",
+ "squizlabs/php_codesniffer": "^2.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Hexogen\\KDTree\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Volodymyr Basarab",
+ "email": "volodymyrbas@gmail.com",
+ "homepage": "https://github.com/hexogen",
+ "role": "Developer"
+ }
+ ],
+ "description": "file system KDTree index",
+ "homepage": "https://github.com/hexogen/kdtree",
+ "keywords": [
+ "algorithms",
+ "data structures",
+ "hexogen",
+ "kdtree",
+ "search"
+ ],
+ "support": {
+ "issues": "https://github.com/hexogen/kdtree/issues",
+ "source": "https://github.com/hexogen/kdtree/tree/master"
+ },
+ "time": "2018-12-23T19:57:09+00:00"
+ }
+ ],
"packages-dev": [
{
"name": "amphp/amp",
diff --git a/lib/Album/AlbumFile.php b/lib/Album/AlbumFile.php
index d6f09caa..7d084bfd 100644
--- a/lib/Album/AlbumFile.php
+++ b/lib/Album/AlbumFile.php
@@ -23,19 +23,11 @@ declare(strict_types=1);
namespace OCA\Photos\Album;
-use OC\Metadata\FileMetadata;
+use OCA\Photos\DB\PhotosFile;
-class AlbumFile {
- private int $fileId;
- private string $name;
- private string $mimeType;
- private int $size;
- private int $mtime;
- private string $etag;
+class AlbumFile extends PhotosFile {
private int $added;
private string $owner;
- /** @var array<string, FileMetadata> */
- private array $metaData = [];
public function __construct(
int $fileId,
@@ -47,52 +39,19 @@ class AlbumFile {
int $added,
string $owner
) {
- $this->fileId = $fileId;
- $this->name = $name;
- $this->mimeType = $mimeType;
- $this->size = $size;
- $this->mtime = $mtime;
- $this->etag = $etag;
+ parent::__construct(
+ $fileId,
+ $name,
+ $mimeType,
+ $size,
+ $mtime,
+ $etag
+ );
+
$this->added = $added;
$this->owner = $owner;
}
- 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];
- }
-
public function getAdded(): int {
return $this->added;
}
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 4a1dc328..909ea099 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -29,11 +29,18 @@ use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\Photos\Listener\SabrePluginAuthInitListener;
use OCA\DAV\Connector\Sabre\Principal;
use OCA\Photos\Listener\CacheEntryRemovedListener;
+use OCA\Photos\Listener\LocationManagerNodeEventListener;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Files\Cache\CacheEntryRemovedEvent;
+use OCP\Files\Events\Node\NodeWrittenEvent;
+use OCP\Share\Events\ShareCreatedEvent;
+use OCP\Share\Events\ShareDeletedEvent;
+use OCP\User\Events\UserDeletedEvent;
+
+require_once __DIR__ . '/../../vendor/autoload.php';
class Application extends App implements IBootstrap {
public const APP_ID = 'photos';
@@ -65,7 +72,16 @@ class Application extends App implements IBootstrap {
public function register(IRegistrationContext $context): void {
/** Register $principalBackend for the DAV collection */
$context->registerServiceAlias('principalBackend', Principal::class);
+
$context->registerEventListener(CacheEntryRemovedEvent::class, CacheEntryRemovedListener::class);
+
+ $context->registerEventListener(CacheEntryRemovedEvent::class, LocationManagerNodeEventListener::class);
+ // Priority of -1 to be triggered after event listeners populating metadata.
+ $context->registerEventListener(NodeWrittenEvent::class, LocationManagerNodeEventListener::class, -1);
+ $context->registerEventListener(UserDeletedEvent::class, LocationManagerNodeEventListener::class);
+ $context->registerEventListener(ShareCreatedEvent::class, LocationManagerNodeEventListener::class);
+ $context->registerEventListener(ShareDeletedEvent::class, LocationManagerNodeEventListener::class);
+
$context->registerEventListener(SabrePluginAuthInitEvent::class, SabrePluginAuthInitListener::class);
}
diff --git a/lib/Command/MapMediaToLocationCommand.php b/lib/Command/MapMediaToLocationCommand.php
new file mode 100644
index 00000000..d440aa3a
--- /dev/null
+++ b/lib/Command/MapMediaToLocationCommand.php
@@ -0,0 +1,117 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCA\Photos\Command;
+
+use OCP\IConfig;
+use OCP\IUserManager;
+use OCP\Files\IRootFolder;
+use OCP\Files\Folder;
+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 MapMediaToLocationCommand extends Command {
+ private IRootFolder $rootFolder;
+ private MediaLocationManager $mediaLocationManager;
+ private IConfig $config;
+ private IUserManager $userManager;
+
+ public function __construct(
+ IRootFolder $rootFolder,
+ MediaLocationManager $mediaLocationManager,
+ IConfig $config,
+ IUserManager $userManager
+ ) {
+ parent::__construct();
+ $this->config = $config;
+ $this->rootFolder = $rootFolder;
+ $this->mediaLocationManager = $mediaLocationManager;
+ $this->userManager = $userManager;
+ }
+
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure() {
+ $this->setName('photos:map-media-to-location')
+ ->setDescription('Reverse geocode media coordinates.')
+ ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'Limit the mapping to a user.', null);
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ *
+ * @return int
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ if (!$this->config->getSystemValueBool('enable_file_metadata', true)) {
+ throw new \Exception('File metadata is not enabled.');
+ }
+
+ $userId = $input->getOption('user');
+ if ($userId === null) {
+ $this->scanForAllUsers();
+ } else {
+ $this->scanFilesForUser($userId);
+ }
+
+ return 0;
+ }
+
+ private function scanForAllUsers() {
+ $users = $this->userManager->search('');
+
+ foreach ($users as $user) {
+ $this->scanFilesForUser($user->getUID());
+ }
+ }
+
+ private function scanFilesForUser(string $userId) {
+ $userFolder = $this->rootFolder->getUserFolder($userId);
+ $this->scanFolder($userFolder);
+ }
+
+ private function scanFolder(Folder $folder) {
+ foreach ($folder->getDirectoryListing() as $node) {
+ if ($node instanceof Folder) {
+ $this->scanFolder($node);
+ continue;
+ }
+
+ if (!str_starts_with($node->getMimeType(), 'image')) {
+ continue;
+ }
+
+ $this->mediaLocationManager->addLocationForFileAndUser($node->getId(), $node->getOwner()->getUID());
+ }
+ }
+}
diff --git a/lib/Command/UpdateReverseGeocodingFilesCommand.php b/lib/Command/UpdateReverseGeocodingFilesCommand.php
new file mode 100644
index 00000000..abfbcb9c
--- /dev/null
+++ b/lib/Command/UpdateReverseGeocodingFilesCommand.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCA\Photos\Command;
+
+use OCA\Photos\Service\ReverseGeoCoderService;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class UpdateReverseGeocodingFilesCommand extends Command {
+ private ReverseGeoCoderService $rgcService;
+
+ public function __construct(
+ ReverseGeoCoderService $rgcService
+ ) {
+ parent::__construct();
+ $this->rgcService = $rgcService;
+ }
+
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure() {
+ $this->setName('photos:update-reverse-geocoding-files')
+ ->setDescription('Update the necessary reverse geocoding files');
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ *
+ * @return int
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ try {
+ $this->rgcService->buildKDTree(true);
+ } catch (\Exception $ex) {
+ $output->writeln('<error>Failed to update reverse geocoding files</error>');
+ $output->writeln($ex->getMessage());
+ return 1;
+ }
+
+ return 0;
+ }
+}
diff --git a/lib/DB/Location/LocationFile.php b/lib/DB/Location/LocationFile.php
new file mode 100644
index 00000000..5a8fcda8
--- /dev/null
+++ b/lib/DB/Location/LocationFile.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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 00000000..f8b9b76d
--- /dev/null
+++ b/lib/DB/Location/LocationInfo.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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 00000000..ccefac25
--- /dev/null
+++ b/lib/DB/Location/LocationMapper.php
@@ -0,0 +1,128 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Photos\DB\Location;
+
+use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\Files\IMimeTypeLoader;
+use OCP\IDBConnection;
+
+class LocationMapper {
+ public const TABLE_NAME = 'photos_locations';
+
+ private IDBConnection $connection;
+ private IMimeTypeLoader $mimeTypeLoader;
+
+ public function __construct(
+ IDBConnection $connection,
+ IMimeTypeLoader $mimeTypeLoader
+ ) {
+ $this->connection = $connection;
+ $this->mimeTypeLoader = $mimeTypeLoader;
+ }
+
+ /** @return LocationInfo[] */
+ public function findLocationsForUser(string $userId): array {
+ $qb = $this->connection->getQueryBuilder();
+
+ $rows = $qb->selectDistinct('location_id')
+ ->from(self::TABLE_NAME)
+ ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
+ ->executeQuery()
+ ->fetchAll();
+
+ return array_map(fn ($row) => new LocationInfo($userId, (int)$row['location_id']), $rows);
+ }
+
+ /** @return LocationFile[] */
+ public function findFilesForUserAndLocation(string $userId, int $locationId) {
+ $qb = $this->connection->getQueryBuilder();
+
+ $rows = $qb->select("fileid", "name", "mimetype", "size", "mtime", "etag", "location_id")
+ ->from(self::TABLE_NAME, 'l')
+ ->leftJoin("l", "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)))
+ ->executeQuery();
+
+ 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->fetchAll(),
+ );
+ }
+
+ public function addLocationForFileAndUser(int $locationId, int $fileId, string $userId): void {
+ try {
+ $query = $this->connection->getQueryBuilder();
+ $query->insert(self::TABLE_NAME)
+ ->values([
+ "user_id" => $query->createNamedParameter($userId),
+ "location_id" => $query->createNamedParameter($locationId, IQueryBuilder::PARAM_INT),
+ "file_id" => $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT),
+ ])
+ ->executeStatement();
+ } catch (\Exception $ex) {
+ if ($ex->getPrevious() instanceof UniqueConstraintViolationException) {
+ $this->updateLocationForFile($locationId, $fileId);
+ }
+ }
+ }
+
+ public function updateLocationForFile(int $locationId, int $fileId): void {
+ $query = $this->connection->getQueryBuilder();
+ $query->update(self::TABLE_NAME)
+ ->set("location_id", $query->createNamedParameter($locationId, IQueryBuilder::PARAM_INT))
+ ->where($query->expr()->eq('file_id', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)))
+ ->executeStatement();
+ }
+
+ public function removeLocationForFile(int $fileId, ?string $userId = null): void {
+ $query = $this->connection->getQueryBuilder();
+ $query->delete(self::TABLE_NAME)
+ ->where($query->expr()->eq('file_id', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
+
+ if ($userId !== null) {
+ $query->where($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
+ }
+
+ $query->executeStatement();
+ }
+
+ public function removeLocationForUser(string $userId): void {
+ $query = $this->connection->getQueryBuilder();
+ $query->delete(self::TABLE_NAME)
+ ->where($query->expr()->eq('user_id', $query->createNamedParameter($userId)))
+ ->executeStatement();
+ }
+}
diff --git a/lib/DB/PhotosFile.php b/lib/DB/PhotosFile.php
new file mode 100644
index 00000000..9473a0f8
--- /dev/null
+++ b/lib/DB/PhotosFile.php
@@ -0,0 +1,91 @@
+<?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\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<string, FileMetadata> */
+ 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/Jobs/MapMediaToLocationJob.php b/lib/Jobs/MapMediaToLocationJob.php
new file mode 100644
index 00000000..046c4d95
--- /dev/null
+++ b/lib/Jobs/MapMediaToLocationJob.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Photos\Jobs;
+
+use OCA\Photos\Service\MediaLocationManager;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\QueuedJob;
+
+class MapMediaToLocationJob extends QueuedJob {
+ private MediaLocationManager $mediaLocationManager;
+
+ public function __construct(
+ ITimeFactory $time,
+ MediaLocationManager $mediaLocationManager
+ ) {
+ parent::__construct($time);
+ $this->mediaLocationManager = $mediaLocationManager;
+ }
+
+ protected function run($argument) {
+ [$fileId, $ownerId] = $argument;
+
+ $this->mediaLocationManager->addLocationForFileAndUser(
+ $fileId,
+ $ownerId,
+ );
+ }
+}
diff --git a/lib/Listener/LocationManagerNodeEventListener.php b/lib/Listener/LocationManagerNodeEventListener.php
new file mode 100644
index 00000000..0778834c
--- /dev/null
+++ b/lib/Listener/LocationManagerNodeEventListener.php
@@ -0,0 +1,131 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Photos\Listener;
+
+use OCA\Photos\Jobs\MapMediaToLocationJob;
+use OCA\Photos\Service\MediaLocationManager;
+use OCP\BackgroundJob\IJobList;
+use OCP\IConfig;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Files\Cache\CacheEntryRemovedEvent;
+use OCP\Files\Events\Node\NodeWrittenEvent;
+use OCP\Files\Folder;
+use OCP\Files\File;
+use OCP\Files\Node;
+use OCP\Share\Events\ShareCreatedEvent;
+use OCP\Share\Events\ShareDeletedEvent;
+use OCP\User\Events\UserDeletedEvent;
+
+/**
+ * Listener to create, update or remove location info from the database.
+ */
+class LocationManagerNodeEventListener implements IEventListener {
+ private MediaLocationManager $mediaLocationManager;
+ private IConfig $config;
+ private IJobList $jobList;
+
+ public function __construct(
+ MediaLocationManager $mediaLocationManager,
+ IConfig $config,
+ IJobList $jobList
+ ) {
+ $this->mediaLocationManager = $mediaLocationManager;
+ $this->config = $config;
+ $this->jobList = $jobList;
+ }
+
+ public function handle(Event $event): void {
+ if (!$this->config->getSystemValueBool('enable_file_metadata', true)) {
+ return;
+ }
+
+ if ($event instanceof CacheEntryRemovedEvent) {
+ if ($this->isCorrectPath($event->getPath())) {
+ $this->mediaLocationManager->clearLocationForFile($event->getFileId());
+ }
+ }
+
+ if ($event instanceof NodeWrittenEvent) {
+ if (!$this->isCorrectPath($event->getNode()->getPath())) {
+ return;
+ }
+
+ if (!str_starts_with($event->getNode()->getMimeType(), 'image')) {
+ return;
+ }
+
+ $fileId = $event->getNode()->getId();
+ $ownerId = $event->getNode()->getOwner()->getUID();
+
+ $this->jobList->add(MapMediaToLocationJob::class, [$fileId, $ownerId]);
+ }
+
+ if ($event instanceof UserDeletedEvent) {
+ $this->mediaLocationManager->clearLocationForUser($event->getUser()->getUID());
+ }
+
+ if ($event instanceof ShareCreatedEvent) {
+ $receiverId = $event->getShare()->getSharedWith();
+
+ $this->forEachSubNode(
+ $event->getShare()->getNode(),
+ fn ($fileId) => $this->jobList->add(MapMediaToLocationJob::class, [$fileId, $receiverId]),
+ );
+ }
+
+ if ($event instanceof ShareDeletedEvent) {
+ $receiverId = $event->getShare()->getSharedWith();
+
+ $this->forEachSubNode(
+ $event->getShare()->getNode(),
+ fn ($fileId) => $this->mediaLocationManager->clearLocationForFile($fileId, $receiverId),
+ );
+ }
+ }
+
+ private function isCorrectPath(string $path): bool {
+ // TODO make this more dynamic, we have the same issue in other places
+ return !str_starts_with($path, 'appdata_') && !str_starts_with($path, 'files_versions/');
+ }
+
+
+ private function forEachSubNode(Node $node, callable $callback) {
+ if ($node instanceof Folder) {
+ foreach ($node->getDirectoryListing() as $subNode) {
+ $this->forEachSubNode($subNode, $callback);
+ }
+ }
+
+ if ($node instanceof File) {
+ if (!str_starts_with($node->getMimeType(), 'image')) {
+ return;
+ }
+
+ $callback($node->getId());
+ }
+ }
+}
diff --git a/lib/Migration/Version20002Date20221012131022.php b/lib/Migration/Version20002Date20221012131022.php
new file mode 100644
index 00000000..6593017c
--- /dev/null
+++ b/lib/Migration/Version20002Date20221012131022.php
@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2022 Your name <your@email.com>
+ *
+ * @author Your name <your@email.com>
+ *
+ * @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\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 $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @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('id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $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->setPrimaryKey(['id']);
+ $table->addUniqueConstraint(['user_id', 'file_id'], 'photos_locations_unique_idx');
+ }
+
+ if ($modified) {
+ return $schema;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/lib/Service/MediaLocationManager.php b/lib/Service/MediaLocationManager.php
new file mode 100644
index 00000000..ee34cc6c
--- /dev/null
+++ b/lib/Service/MediaLocationManager.php
@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Photos\Service;
+
+use OC\Metadata\IMetadataManager;
+use OCA\Photos\DB\Location\LocationMapper;
+
+class MediaLocationManager {
+ private IMetadataManager $metadataManager;
+ private ReverseGeoCoderService $rgcService;
+ private LocationMapper $locationMapper;
+
+ public function __construct(
+ IMetadataManager $metadataManager,
+ ReverseGeoCoderService $rgcService,
+ LocationMapper $locationMapper
+ ) {
+ $this->metadataManager = $metadataManager;
+ $this->rgcService = $rgcService;
+ $this->locationMapper = $locationMapper;
+ }
+
+ public function addLocationForFileAndUser(int $fileId, string $userId) {
+ $locationId = $this->getLocationIdForFile($fileId);
+
+ if ($locationId === -1) {
+ return;
+ }
+
+ $this->locationMapper->addLocationForFileAndUser($locationId, $fileId, $userId);
+ }
+
+ public function updateLocationForFile(int $fileId) {
+ $locationId = $this->getLocationIdForFile($fileId);
+
+ if ($locationId === -1) {
+ return;
+ }
+
+ $this->locationMapper->updateLocationForFile($locationId, $fileId);
+ }
+
+ public function clearLocationForFile(int $fileId, ?string $userId = null): void {
+ $this->locationMapper->removeLocationForFile($fileId, $userId);
+ }
+
+ public function clearLocationForUser(string $userId): void {
+ $this->locationMapper->removeLocationForUser($userId);
+ }
+
+ private function getLocationIdForFile(int $fileId): int {
+ $gpsMetadata = $this->metadataManager->fetchMetadataFor('gps', [$fileId])[$fileId];
+ $metadata = $gpsMetadata->getMetadata();
+
+ if (count($metadata) === 0) {
+ return -1;
+ }
+
+ $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
new file mode 100644
index 00000000..a9338dff
--- /dev/null
+++ b/lib/Service/ReverseGeoCoderService.php
@@ -0,0 +1,167 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCA\Photos\Service;
+
+use OCP\Files\IAppData;
+use OCP\Files\SimpleFS\ISimpleFolder;
+use OCP\Files\NotFoundException;
+use OCP\Http\Client\IClientService;
+use Hexogen\KDTree\FSTreePersister;
+use Hexogen\KDTree\FSKDTree;
+use Hexogen\KDTree\KDTree;
+use Hexogen\KDTree\Item;
+use Hexogen\KDTree\ItemList;
+use Hexogen\KDTree\ItemFactory;
+use Hexogen\KDTree\NearestSearch;
+use Hexogen\KDTree\Point;
+
+class ReverseGeoCoderService {
+ private IClientService $clientService;
+ private ISimpleFolder $geoNameFolder;
+ private ?NearestSearch $fsSearcher = null;
+ /** @var array<int, string> */
+ private ?array $citiesMapping = null;
+
+ public function __construct(
+ IAppData $appData,
+ IClientService $clientService
+ ) {
+ $this->clientService = $clientService;
+
+ try {
+ $this->geoNameFolder = $appData->getFolder("geonames");
+ } catch (\Exception $ex) {
+ if ($ex instanceof NotFoundException) {
+ $this->geoNameFolder = $appData->newFolder("geonames");
+ }
+
+ throw $ex;
+ }
+ }
+
+ public function getLocationIdForCoordinates(float $latitude, float $longitude): int {
+ if ($this->fsSearcher === null) {
+ $this->buildKDTree();
+ $kdTreeFileContent = $this->geoNameFolder->getFile("cities1000.bin")->getContent();
+ $kdTreeTmpFileName = tempnam(sys_get_temp_dir(), "nextcloud_photos_");
+ file_put_contents($kdTreeTmpFileName, $kdTreeFileContent);
+ $fsTree = new FSKDTree($kdTreeTmpFileName, new ItemFactory());
+ $this->fsSearcher = new NearestSearch($fsTree);
+ }
+
+ $result = $this->fsSearcher->search(new Point([$latitude, $longitude]), 1);
+ return $result[0]->getId();
+ }
+
+ public function getLocationNameForLocationId(int $locationId): string {
+ if ($this->citiesMapping === null) {
+ $this->downloadCities1000();
+ $cities1000 = $this->loadCities1000();
+ foreach ($cities1000 as $city) {
+ $this->citiesMapping[$city['id']] = $city['name'];
+ }
+ }
+
+ return $this->citiesMapping[$locationId] ?? '';
+ }
+
+ private function downloadCities1000(bool $force = false) {
+ if ($this->geoNameFolder->fileExists('cities1000.csv') && !$force) {
+ return;
+ }
+
+ // Download zip file to a tmp file.
+ $response = $this->clientService->newClient()->get("http://download.geonames.org/export/dump/cities1000.zip");
+ $cities1000ZipTmpFileName = tempnam(sys_get_temp_dir(), "nextcloud_photos_");
+ file_put_contents($cities1000ZipTmpFileName, $response->getBody());
+
+ // Unzip the txt file into a stream.
+ $zip = new \ZipArchive;
+ $res = $zip->open($cities1000ZipTmpFileName);
+ if ($res !== true) {
+ throw new \Exception("Fail to unzip location file: $res", $res);
+ }
+ $cities1000TxtSteam = $zip->getStream('cities1000.txt');
+
+ // Dump the txt file info into a smaller csv file.
+ $destinationStream = $this->geoNameFolder->newFile('cities1000.csv')->write();
+
+ while (($fields = fgetcsv($cities1000TxtSteam, 0, " ")) !== false) {
+ $result = fputcsv(
+ $destinationStream,
+ [
+ 'id' => (int)$fields[0],
+ 'name' => $fields[1],
+ 'latitude' => (float)$fields[4],
+ 'longitude' => (float)$fields[5],
+ ]
+ );
+
+ if ($result === false) {
+ throw new \Exception('Failed to write csv line to tmp stream');
+ }
+ }
+
+ $zip->close();
+ }
+
+ private function loadCities1000(): array {
+ $csvStream = $this->geoNameFolder->getFile('cities1000.csv')->read();
+ $cities = [];
+
+ while (($fields = fgetcsv($csvStream)) !== false) {
+ $cities[] = [
+ 'id' => (int)$fields[0],
+ 'name' => $fields[1],
+ 'latitude' => (float)$fields[2],
+ 'longitude' => (float)$fields[3],
+ ];
+ }
+
+ return $cities;
+ }
+
+ public function buildKDTree($force = false) {
+ if ($this->geoNameFolder->fileExists('cities1000.bin') && !$force) {
+ return;
+ }
+
+ $this->downloadCities1000($force);
+ $cities1000 = $this->loadCities1000();
+
+ $itemList = new ItemList(2);
+ foreach ($cities1000 as $city) {
+ $itemList->addItem(new Item($city['id'], [$city['latitude'], $city['longitude']]));
+ }
+ $tree = new KDTree($itemList);
+
+ // Persiste KDTree in app data.
+ $persister = new FSTreePersister('/');
+ $kdTreeTmpFileName = tempnam(sys_get_temp_dir(), "nextcloud_photos_");
+ $persister->convert($tree, $kdTreeTmpFileName);
+ $kdTreeString = file_get_contents($kdTreeTmpFileName);
+ $this->geoNameFolder->newFile('cities1000.bin', $kdTreeString);
+ }
+}
diff --git a/package.json b/package.json
index 6a997b82..f0111fd0 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "photos",
"description": "Your memories under your control",
- "version": "2.2.0",
+ "version": "2.2.1",
"author": "John Molakvoæ <skjnldsv@protonmail.com>",
"contributors": [
"John Molakvoæ <skjnldsv@protonmail.com>"
diff --git a/tests/stub.phpstub b/tests/stub.phpstub
index 038e42e2..b0232e08 100644
--- a/tests/stub.phpstub
+++ b/tests/stub.phpstub
@@ -269,10 +269,10 @@ namespace OC\Files\Mount {
protected $class;
protected $storageId;
protected $rootId = null;
-
+
/** @var int|null */
protected $mountId;
-
+
/**
* @param string|\OCP\Files\Storage\IStorage $storage
* @param string $mountpoint
@@ -285,7 +285,7 @@ namespace OC\Files\Mount {
public function __construct($storage, $mountpoint, $arguments = null, $loader = null, $mountOptions = null, $mountId = null) {
throw new \Exception('stub');
}
-
+
/**
* get complete path to the mount point, relative to data/
*
@@ -294,7 +294,7 @@ namespace OC\Files\Mount {
public function getMountPoint() {
throw new \Exception('stub');
}
-
+
/**
* Sets the mount point path, relative to data/
*
@@ -303,28 +303,28 @@ namespace OC\Files\Mount {
public function setMountPoint($mountPoint) {
throw new \Exception('stub');
}
-
+
/**
* @return \OCP\Files\Storage\IStorage
*/
public function getStorage() {
throw new \Exception('stub');
}
-
+
/**
* @return string
*/
public function getStorageId() {
throw new \Exception('stub');
}
-
+
/**
* @return int
*/
public function getNumericStorageId() {
throw new \Exception('stub');
}
-
+
/**
* @param string $path
* @return string
@@ -332,14 +332,14 @@ namespace OC\Files\Mount {
public function getInternalPath($path) {
throw new \Exception('stub');
}
-
+
/**
* @param callable $wrapper
*/
public function wrapStorage($wrapper) {
throw new \Exception('stub');
}
-
+
/**
* Get a mount option
*
@@ -350,7 +350,7 @@ namespace OC\Files\Mount {
public function getOption($name, $default) {
throw new \Exception('stub');
}
-
+
/**
* Get all options for the mount
*
@@ -359,18 +359,18 @@ namespace OC\Files\Mount {
public function getOptions() {
throw new \Exception('stub');
}
-
+
/**
* @return int
*/
public function getStorageRootId() {
throw new \Exception('stub');
}
-
+
public function getMountId() {
throw new \Exception('stub');
}
-
+
public function getMountType() {
throw new \Exception('stub');
}
@@ -656,7 +656,7 @@ use OCP\DB\Types;
/**
* @method string getGroupName()
* @method void setGroupName(string $groupName)
- * @method string getMetadata()
+ * @method array getMetadata()
* @method void setMetadata(array $metadata)
* @see \OC\Core\Migrations\Version240000Date20220404230027
*/