From 564127e9fea465852eac6d99f85cb3837306c380 Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Thu, 24 Nov 2022 08:56:22 +0100 Subject: [PATCH 1/2] Make CalDAV resource sync a service and expose as occ command Signed-off-by: Christoph Wurst --- apps/dav/appinfo/info.xml | 1 + .../composer/composer/autoload_classmap.php | 2 + .../dav/composer/composer/autoload_static.php | 2 + apps/dav/lib/AppInfo/Application.php | 6 +- ...ateCalendarResourcesRoomsBackgroundJob.php | 414 +--------------- .../CalendarResourcesRoomsSyncService.php | 442 ++++++++++++++++++ apps/dav/lib/Command/SyncResourcesRooms.php | 53 +++ ...alendarResourcesRoomsBackgroundJobTest.php | 41 +- 8 files changed, 538 insertions(+), 423 deletions(-) create mode 100644 apps/dav/lib/CalDAV/CalendarResourcesRoomsSyncService.php create mode 100644 apps/dav/lib/Command/SyncResourcesRooms.php rename apps/dav/tests/unit/{BackgroundJob => CalDAV}/UpdateCalendarResourcesRoomsBackgroundJobTest.php (92%) diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index dd657564ea9ef..110134a4ae6cf 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -55,6 +55,7 @@ OCA\DAV\Command\RetentionCleanupCommand OCA\DAV\Command\SendEventReminders OCA\DAV\Command\SyncBirthdayCalendar + OCA\DAV\Command\SyncResourcesRooms OCA\DAV\Command\SyncSystemAddressBook OCA\DAV\Command\RemoveInvalidShares diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index ce98cece3a192..bafe7172d185b 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -50,6 +50,7 @@ 'OCA\\DAV\\CalDAV\\CalendarManager' => $baseDir . '/../lib/CalDAV/CalendarManager.php', 'OCA\\DAV\\CalDAV\\CalendarObject' => $baseDir . '/../lib/CalDAV/CalendarObject.php', 'OCA\\DAV\\CalDAV\\CalendarProvider' => $baseDir . '/../lib/CalDAV/CalendarProvider.php', + 'OCA\\DAV\\CalDAV\\CalendarResourcesRoomsSyncService' => $baseDir . '/../lib/CalDAV/CalendarResourcesRoomsSyncService.php', 'OCA\\DAV\\CalDAV\\CalendarRoot' => $baseDir . '/../lib/CalDAV/CalendarRoot.php', 'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', 'OCA\\DAV\\CalDAV\\IRestorable' => $baseDir . '/../lib/CalDAV/IRestorable.php', @@ -132,6 +133,7 @@ 'OCA\\DAV\\Command\\RetentionCleanupCommand' => $baseDir . '/../lib/Command/RetentionCleanupCommand.php', 'OCA\\DAV\\Command\\SendEventReminders' => $baseDir . '/../lib/Command/SendEventReminders.php', 'OCA\\DAV\\Command\\SyncBirthdayCalendar' => $baseDir . '/../lib/Command/SyncBirthdayCalendar.php', + 'OCA\\DAV\\Command\\SyncResourcesRooms' => $baseDir . '/../lib/Command/SyncResourcesRooms.php', 'OCA\\DAV\\Command\\SyncSystemAddressBook' => $baseDir . '/../lib/Command/SyncSystemAddressBook.php', 'OCA\\DAV\\Comments\\CommentNode' => $baseDir . '/../lib/Comments/CommentNode.php', 'OCA\\DAV\\Comments\\CommentsPlugin' => $baseDir . '/../lib/Comments/CommentsPlugin.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index a5a7d34d128d4..26c4aa268e81e 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -65,6 +65,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CalDAV\\CalendarManager' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarManager.php', 'OCA\\DAV\\CalDAV\\CalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarObject.php', 'OCA\\DAV\\CalDAV\\CalendarProvider' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarProvider.php', + 'OCA\\DAV\\CalDAV\\CalendarResourcesRoomsSyncService' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarResourcesRoomsSyncService.php', 'OCA\\DAV\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarRoot.php', 'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', 'OCA\\DAV\\CalDAV\\IRestorable' => __DIR__ . '/..' . '/../lib/CalDAV/IRestorable.php', @@ -147,6 +148,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Command\\RetentionCleanupCommand' => __DIR__ . '/..' . '/../lib/Command/RetentionCleanupCommand.php', 'OCA\\DAV\\Command\\SendEventReminders' => __DIR__ . '/..' . '/../lib/Command/SendEventReminders.php', 'OCA\\DAV\\Command\\SyncBirthdayCalendar' => __DIR__ . '/..' . '/../lib/Command/SyncBirthdayCalendar.php', + 'OCA\\DAV\\Command\\SyncResourcesRooms' => __DIR__ . '/..' . '/../lib/Command/SyncResourcesRooms.php', 'OCA\\DAV\\Command\\SyncSystemAddressBook' => __DIR__ . '/..' . '/../lib/Command/SyncSystemAddressBook.php', 'OCA\\DAV\\Comments\\CommentNode' => __DIR__ . '/..' . '/../lib/Comments/CommentNode.php', 'OCA\\DAV\\Comments\\CommentsPlugin' => __DIR__ . '/..' . '/../lib/Comments/CommentsPlugin.php', diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index 8674986262623..f25c076103a63 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -33,7 +33,7 @@ namespace OCA\DAV\AppInfo; use Exception; -use OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob; +use OCA\DAV\BackgroundJob\CalendarResourcesRoomsSyncService; use OCA\DAV\CalDAV\Activity\Backend; use OCA\DAV\CalDAV\CalendarManager; use OCA\DAV\CalDAV\CalendarProvider; @@ -246,8 +246,8 @@ public function registerHooks(HookManager $hm, $eventHandler = function () use ($container, $serverContainer): void { try { - /** @var UpdateCalendarResourcesRoomsBackgroundJob $job */ - $job = $container->query(UpdateCalendarResourcesRoomsBackgroundJob::class); + /** @var CalendarResourcesRoomsSyncService $job */ + $job = $container->query(CalendarResourcesRoomsSyncService::class); $job->run([]); $serverContainer->getJobList()->setLastRun($job); } catch (Exception $ex) { diff --git a/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php b/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php index f7addd58248c7..b71bd5794f270 100644 --- a/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php +++ b/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php @@ -28,427 +28,29 @@ namespace OCA\DAV\BackgroundJob; -use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\CalendarResourcesRoomsSyncService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; -use OCP\Calendar\BackendTemporarilyUnavailableException; -use OCP\Calendar\IMetadataProvider; -use OCP\Calendar\Resource\IBackend as IResourceBackend; -use OCP\Calendar\Resource\IManager as IResourceManager; -use OCP\Calendar\Resource\IResource; -use OCP\Calendar\Room\IManager as IRoomManager; -use OCP\Calendar\Room\IRoom; -use OCP\IDBConnection; class UpdateCalendarResourcesRoomsBackgroundJob extends TimedJob { - /** @var IResourceManager */ - private $resourceManager; + private CalendarResourcesRoomsSyncService $syncService; - /** @var IRoomManager */ - private $roomManager; - - /** @var IDBConnection */ - private $dbConnection; - - /** @var CalDavBackend */ - private $calDavBackend; - - public function __construct(ITimeFactory $time, - IResourceManager $resourceManager, - IRoomManager $roomManager, - IDBConnection $dbConnection, - CalDavBackend $calDavBackend) { + public function __construct(CalendarResourcesRoomsSyncService $syncService, + ITimeFactory $time) { parent::__construct($time); - $this->resourceManager = $resourceManager; - $this->roomManager = $roomManager; - $this->dbConnection = $dbConnection; - $this->calDavBackend = $calDavBackend; // Run once an hour $this->setInterval(60 * 60); $this->setTimeSensitivity(self::TIME_SENSITIVE); - } - - /** - * @param $argument - */ - public function run($argument): void { - $this->runForBackend( - $this->resourceManager, - 'calendar_resources', - 'calendar_resources_md', - 'resource_id', - 'principals/calendar-resources' - ); - $this->runForBackend( - $this->roomManager, - 'calendar_rooms', - 'calendar_rooms_md', - 'room_id', - 'principals/calendar-rooms' - ); - } - - /** - * Run background-job for one specific backendManager - * either ResourceManager or RoomManager - * - * @param IResourceManager|IRoomManager $backendManager - * @param string $dbTable - * @param string $dbTableMetadata - * @param string $foreignKey - * @param string $principalPrefix - */ - private function runForBackend($backendManager, - string $dbTable, - string $dbTableMetadata, - string $foreignKey, - string $principalPrefix): void { - $backends = $backendManager->getBackends(); - - foreach ($backends as $backend) { - $backendId = $backend->getBackendIdentifier(); - - try { - if ($backend instanceof IResourceBackend) { - $list = $backend->listAllResources(); - } else { - $list = $backend->listAllRooms(); - } - } catch (BackendTemporarilyUnavailableException $ex) { - continue; - } - - $cachedList = $this->getAllCachedByBackend($dbTable, $backendId); - $newIds = array_diff($list, $cachedList); - $deletedIds = array_diff($cachedList, $list); - $editedIds = array_intersect($list, $cachedList); - - foreach ($newIds as $newId) { - try { - if ($backend instanceof IResourceBackend) { - $resource = $backend->getResource($newId); - } else { - $resource = $backend->getRoom($newId); - } - - $metadata = []; - if ($resource instanceof IMetadataProvider) { - $metadata = $this->getAllMetadataOfBackend($resource); - } - } catch (BackendTemporarilyUnavailableException $ex) { - continue; - } - - $id = $this->addToCache($dbTable, $backendId, $resource); - $this->addMetadataToCache($dbTableMetadata, $foreignKey, $id, $metadata); - // we don't create the calendar here, it is created lazily - // when an event is actually scheduled with this resource / room - } - - foreach ($deletedIds as $deletedId) { - $id = $this->getIdForBackendAndResource($dbTable, $backendId, $deletedId); - $this->deleteFromCache($dbTable, $id); - $this->deleteMetadataFromCache($dbTableMetadata, $foreignKey, $id); - - $principalName = implode('-', [$backendId, $deletedId]); - $this->deleteCalendarDataForResource($principalPrefix, $principalName); - } - foreach ($editedIds as $editedId) { - $id = $this->getIdForBackendAndResource($dbTable, $backendId, $editedId); - - try { - if ($backend instanceof IResourceBackend) { - $resource = $backend->getResource($editedId); - } else { - $resource = $backend->getRoom($editedId); - } - - $metadata = []; - if ($resource instanceof IMetadataProvider) { - $metadata = $this->getAllMetadataOfBackend($resource); - } - } catch (BackendTemporarilyUnavailableException $ex) { - continue; - } - - $this->updateCache($dbTable, $id, $resource); - - if ($resource instanceof IMetadataProvider) { - $cachedMetadata = $this->getAllMetadataOfCache($dbTableMetadata, $foreignKey, $id); - $this->updateMetadataCache($dbTableMetadata, $foreignKey, $id, $metadata, $cachedMetadata); - } - } - } - } - - /** - * add entry to cache that exists remotely but not yet in cache - * - * @param string $table - * @param string $backendId - * @param IResource|IRoom $remote - * - * @return int Insert id - */ - private function addToCache(string $table, - string $backendId, - $remote): int { - $query = $this->dbConnection->getQueryBuilder(); - $query->insert($table) - ->values([ - 'backend_id' => $query->createNamedParameter($backendId), - 'resource_id' => $query->createNamedParameter($remote->getId()), - 'email' => $query->createNamedParameter($remote->getEMail()), - 'displayname' => $query->createNamedParameter($remote->getDisplayName()), - 'group_restrictions' => $query->createNamedParameter( - $this->serializeGroupRestrictions( - $remote->getGroupRestrictions() - )) - ]) - ->executeStatement(); - return $query->getLastInsertId(); - } - - /** - * @param string $table - * @param string $foreignKey - * @param int $foreignId - * @param array $metadata - */ - private function addMetadataToCache(string $table, - string $foreignKey, - int $foreignId, - array $metadata): void { - foreach ($metadata as $key => $value) { - $query = $this->dbConnection->getQueryBuilder(); - $query->insert($table) - ->values([ - $foreignKey => $query->createNamedParameter($foreignId), - 'key' => $query->createNamedParameter($key), - 'value' => $query->createNamedParameter($value), - ]) - ->executeStatement(); - } - } - - /** - * delete entry from cache that does not exist anymore remotely - * - * @param string $table - * @param int $id - */ - private function deleteFromCache(string $table, - int $id): void { - $query = $this->dbConnection->getQueryBuilder(); - $query->delete($table) - ->where($query->expr()->eq('id', $query->createNamedParameter($id))) - ->executeStatement(); - } - - /** - * @param string $table - * @param string $foreignKey - * @param int $id - */ - private function deleteMetadataFromCache(string $table, - string $foreignKey, - int $id): void { - $query = $this->dbConnection->getQueryBuilder(); - $query->delete($table) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) - ->executeStatement(); + $this->syncService = $syncService; } /** - * update an existing entry in cache - * - * @param string $table - * @param int $id - * @param IResource|IRoom $remote + * @param mixed $argument */ - private function updateCache(string $table, - int $id, - $remote): void { - $query = $this->dbConnection->getQueryBuilder(); - $query->update($table) - ->set('email', $query->createNamedParameter($remote->getEMail())) - ->set('displayname', $query->createNamedParameter($remote->getDisplayName())) - ->set('group_restrictions', $query->createNamedParameter( - $this->serializeGroupRestrictions( - $remote->getGroupRestrictions() - ))) - ->where($query->expr()->eq('id', $query->createNamedParameter($id))) - ->executeStatement(); - } - - /** - * @param string $dbTable - * @param string $foreignKey - * @param int $id - * @param array $metadata - * @param array $cachedMetadata - */ - private function updateMetadataCache(string $dbTable, - string $foreignKey, - int $id, - array $metadata, - array $cachedMetadata): void { - $newMetadata = array_diff_key($metadata, $cachedMetadata); - $deletedMetadata = array_diff_key($cachedMetadata, $metadata); - - foreach ($newMetadata as $key => $value) { - $query = $this->dbConnection->getQueryBuilder(); - $query->insert($dbTable) - ->values([ - $foreignKey => $query->createNamedParameter($id), - 'key' => $query->createNamedParameter($key), - 'value' => $query->createNamedParameter($value), - ]) - ->executeStatement(); - } - - foreach ($deletedMetadata as $key => $value) { - $query = $this->dbConnection->getQueryBuilder(); - $query->delete($dbTable) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) - ->andWhere($query->expr()->eq('key', $query->createNamedParameter($key))) - ->executeStatement(); - } - - $existingKeys = array_keys(array_intersect_key($metadata, $cachedMetadata)); - foreach ($existingKeys as $existingKey) { - if ($metadata[$existingKey] !== $cachedMetadata[$existingKey]) { - $query = $this->dbConnection->getQueryBuilder(); - $query->update($dbTable) - ->set('value', $query->createNamedParameter($metadata[$existingKey])) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) - ->andWhere($query->expr()->eq('key', $query->createNamedParameter($existingKey))) - ->executeStatement(); - } - } - } - - /** - * serialize array of group restrictions to store them in database - * - * @param array $groups - * - * @return string - */ - private function serializeGroupRestrictions(array $groups): string { - return \json_encode($groups); - } - - /** - * Gets all metadata of a backend - * - * @param IResource|IRoom $resource - * - * @return array - */ - private function getAllMetadataOfBackend($resource): array { - if (!($resource instanceof IMetadataProvider)) { - return []; - } - - $keys = $resource->getAllAvailableMetadataKeys(); - $metadata = []; - foreach ($keys as $key) { - $metadata[$key] = $resource->getMetadataForKey($key); - } - - return $metadata; - } - - /** - * @param string $table - * @param string $foreignKey - * @param int $id - * - * @return array - */ - private function getAllMetadataOfCache(string $table, - string $foreignKey, - int $id): array { - $query = $this->dbConnection->getQueryBuilder(); - $query->select(['key', 'value']) - ->from($table) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))); - $result = $query->executeQuery(); - $rows = $result->fetchAll(); - $result->closeCursor(); - - $metadata = []; - foreach ($rows as $row) { - $metadata[$row['key']] = $row['value']; - } - - return $metadata; - } - - /** - * Gets all cached rooms / resources by backend - * - * @param $tableName - * @param $backendId - * - * @return array - */ - private function getAllCachedByBackend(string $tableName, - string $backendId): array { - $query = $this->dbConnection->getQueryBuilder(); - $query->select('resource_id') - ->from($tableName) - ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))); - $result = $query->executeQuery(); - $rows = $result->fetchAll(); - $result->closeCursor(); - - return array_map(function ($row): string { - return $row['resource_id']; - }, $rows); - } - - /** - * @param $principalPrefix - * @param $principalUri - */ - private function deleteCalendarDataForResource(string $principalPrefix, - string $principalUri): void { - $calendar = $this->calDavBackend->getCalendarByUri( - implode('/', [$principalPrefix, $principalUri]), - CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI); - - if ($calendar !== null) { - $this->calDavBackend->deleteCalendar( - $calendar['id'], - true // Because this wasn't deleted by a user - ); - } - } - - /** - * @param $table - * @param $backendId - * @param $resourceId - * - * @return int - */ - private function getIdForBackendAndResource(string $table, - string $backendId, - string $resourceId): int { - $query = $this->dbConnection->getQueryBuilder(); - $query->select('id') - ->from($table) - ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))) - ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId))); - $result = $query->executeQuery(); - - $id = (int) $result->fetchOne(); - $result->closeCursor(); - return $id; + public function run($argument): void { + $this->syncService->sync(); } } diff --git a/apps/dav/lib/CalDAV/CalendarResourcesRoomsSyncService.php b/apps/dav/lib/CalDAV/CalendarResourcesRoomsSyncService.php new file mode 100644 index 0000000000000..e522123692300 --- /dev/null +++ b/apps/dav/lib/CalDAV/CalendarResourcesRoomsSyncService.php @@ -0,0 +1,442 @@ + + * + * @author Christoph Wurst + * @author Georg Ehrke + * @author Roeland Jago Douma + * + * @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\DAV\CalDAV; + +use OCP\Calendar\BackendTemporarilyUnavailableException; +use OCP\Calendar\IMetadataProvider; +use OCP\Calendar\Resource\IBackend as IResourceBackend; +use OCP\Calendar\Resource\IManager as IResourceManager; +use OCP\Calendar\Resource\IResource; +use OCP\Calendar\Room\IManager as IRoomManager; +use OCP\Calendar\Room\IRoom; +use OCP\IDBConnection; + +class CalendarResourcesRoomsSyncService { + + /** @var IResourceManager */ + private $resourceManager; + + /** @var IRoomManager */ + private $roomManager; + + /** @var IDBConnection */ + private $dbConnection; + + /** @var CalDavBackend */ + private $calDavBackend; + + public function __construct(IResourceManager $resourceManager, + IRoomManager $roomManager, + IDBConnection $dbConnection, + CalDavBackend $calDavBackend) { + $this->resourceManager = $resourceManager; + $this->roomManager = $roomManager; + $this->dbConnection = $dbConnection; + $this->calDavBackend = $calDavBackend; + } + + public function sync(): void { + $this->runForBackend( + $this->resourceManager, + 'calendar_resources', + 'calendar_resources_md', + 'resource_id', + 'principals/calendar-resources' + ); + $this->runForBackend( + $this->roomManager, + 'calendar_rooms', + 'calendar_rooms_md', + 'room_id', + 'principals/calendar-rooms' + ); + } + + /** + * Run background-job for one specific backendManager + * either ResourceManager or RoomManager + * + * @param IResourceManager|IRoomManager $backendManager + * @param string $dbTable + * @param string $dbTableMetadata + * @param string $foreignKey + * @param string $principalPrefix + */ + private function runForBackend($backendManager, + string $dbTable, + string $dbTableMetadata, + string $foreignKey, + string $principalPrefix): void { + $backends = $backendManager->getBackends(); + + foreach ($backends as $backend) { + $backendId = $backend->getBackendIdentifier(); + + try { + if ($backend instanceof IResourceBackend) { + $list = $backend->listAllResources(); + } else { + $list = $backend->listAllRooms(); + } + } catch (BackendTemporarilyUnavailableException $ex) { + continue; + } + + $cachedList = $this->getAllCachedByBackend($dbTable, $backendId); + $newIds = array_diff($list, $cachedList); + $deletedIds = array_diff($cachedList, $list); + $editedIds = array_intersect($list, $cachedList); + + foreach ($newIds as $newId) { + try { + if ($backend instanceof IResourceBackend) { + $resource = $backend->getResource($newId); + } else { + $resource = $backend->getRoom($newId); + } + + $metadata = []; + if ($resource instanceof IMetadataProvider) { + $metadata = $this->getAllMetadataOfBackend($resource); + } + } catch (BackendTemporarilyUnavailableException $ex) { + continue; + } + + $id = $this->addToCache($dbTable, $backendId, $resource); + $this->addMetadataToCache($dbTableMetadata, $foreignKey, $id, $metadata); + // we don't create the calendar here, it is created lazily + // when an event is actually scheduled with this resource / room + } + + foreach ($deletedIds as $deletedId) { + $id = $this->getIdForBackendAndResource($dbTable, $backendId, $deletedId); + $this->deleteFromCache($dbTable, $id); + $this->deleteMetadataFromCache($dbTableMetadata, $foreignKey, $id); + + $principalName = implode('-', [$backendId, $deletedId]); + $this->deleteCalendarDataForResource($principalPrefix, $principalName); + } + + foreach ($editedIds as $editedId) { + $id = $this->getIdForBackendAndResource($dbTable, $backendId, $editedId); + + try { + if ($backend instanceof IResourceBackend) { + $resource = $backend->getResource($editedId); + } else { + $resource = $backend->getRoom($editedId); + } + + $metadata = []; + if ($resource instanceof IMetadataProvider) { + $metadata = $this->getAllMetadataOfBackend($resource); + } + } catch (BackendTemporarilyUnavailableException $ex) { + continue; + } + + $this->updateCache($dbTable, $id, $resource); + + if ($resource instanceof IMetadataProvider) { + $cachedMetadata = $this->getAllMetadataOfCache($dbTableMetadata, $foreignKey, $id); + $this->updateMetadataCache($dbTableMetadata, $foreignKey, $id, $metadata, $cachedMetadata); + } + } + } + } + + /** + * add entry to cache that exists remotely but not yet in cache + * + * @param string $table + * @param string $backendId + * @param IResource|IRoom $remote + * + * @return int Insert id + */ + private function addToCache(string $table, + string $backendId, + $remote): int { + $query = $this->dbConnection->getQueryBuilder(); + $query->insert($table) + ->values([ + 'backend_id' => $query->createNamedParameter($backendId), + 'resource_id' => $query->createNamedParameter($remote->getId()), + 'email' => $query->createNamedParameter($remote->getEMail()), + 'displayname' => $query->createNamedParameter($remote->getDisplayName()), + 'group_restrictions' => $query->createNamedParameter( + $this->serializeGroupRestrictions( + $remote->getGroupRestrictions() + )) + ]) + ->executeStatement(); + return $query->getLastInsertId(); + } + + /** + * @param string $table + * @param string $foreignKey + * @param int $foreignId + * @param array $metadata + */ + private function addMetadataToCache(string $table, + string $foreignKey, + int $foreignId, + array $metadata): void { + foreach ($metadata as $key => $value) { + $query = $this->dbConnection->getQueryBuilder(); + $query->insert($table) + ->values([ + $foreignKey => $query->createNamedParameter($foreignId), + 'key' => $query->createNamedParameter($key), + 'value' => $query->createNamedParameter($value), + ]) + ->executeStatement(); + } + } + + /** + * delete entry from cache that does not exist anymore remotely + * + * @param string $table + * @param int $id + */ + private function deleteFromCache(string $table, + int $id): void { + $query = $this->dbConnection->getQueryBuilder(); + $query->delete($table) + ->where($query->expr()->eq('id', $query->createNamedParameter($id))) + ->executeStatement(); + } + + /** + * @param string $table + * @param string $foreignKey + * @param int $id + */ + private function deleteMetadataFromCache(string $table, + string $foreignKey, + int $id): void { + $query = $this->dbConnection->getQueryBuilder(); + $query->delete($table) + ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) + ->executeStatement(); + } + + /** + * update an existing entry in cache + * + * @param string $table + * @param int $id + * @param IResource|IRoom $remote + */ + private function updateCache(string $table, + int $id, + $remote): void { + $query = $this->dbConnection->getQueryBuilder(); + $query->update($table) + ->set('email', $query->createNamedParameter($remote->getEMail())) + ->set('displayname', $query->createNamedParameter($remote->getDisplayName())) + ->set('group_restrictions', $query->createNamedParameter( + $this->serializeGroupRestrictions( + $remote->getGroupRestrictions() + ))) + ->where($query->expr()->eq('id', $query->createNamedParameter($id))) + ->executeStatement(); + } + + /** + * @param string $dbTable + * @param string $foreignKey + * @param int $id + * @param array $metadata + * @param array $cachedMetadata + */ + private function updateMetadataCache(string $dbTable, + string $foreignKey, + int $id, + array $metadata, + array $cachedMetadata): void { + $newMetadata = array_diff_key($metadata, $cachedMetadata); + $deletedMetadata = array_diff_key($cachedMetadata, $metadata); + + foreach ($newMetadata as $key => $value) { + $query = $this->dbConnection->getQueryBuilder(); + $query->insert($dbTable) + ->values([ + $foreignKey => $query->createNamedParameter($id), + 'key' => $query->createNamedParameter($key), + 'value' => $query->createNamedParameter($value), + ]) + ->executeStatement(); + } + + foreach ($deletedMetadata as $key => $value) { + $query = $this->dbConnection->getQueryBuilder(); + $query->delete($dbTable) + ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) + ->andWhere($query->expr()->eq('key', $query->createNamedParameter($key))) + ->executeStatement(); + } + + $existingKeys = array_keys(array_intersect_key($metadata, $cachedMetadata)); + foreach ($existingKeys as $existingKey) { + if ($metadata[$existingKey] !== $cachedMetadata[$existingKey]) { + $query = $this->dbConnection->getQueryBuilder(); + $query->update($dbTable) + ->set('value', $query->createNamedParameter($metadata[$existingKey])) + ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) + ->andWhere($query->expr()->eq('key', $query->createNamedParameter($existingKey))) + ->executeStatement(); + } + } + } + + /** + * serialize array of group restrictions to store them in database + * + * @param array $groups + * + * @return string + */ + private function serializeGroupRestrictions(array $groups): string { + return \json_encode($groups); + } + + /** + * Gets all metadata of a backend + * + * @param IResource|IRoom $resource + * + * @return array + */ + private function getAllMetadataOfBackend($resource): array { + if (!($resource instanceof IMetadataProvider)) { + return []; + } + + $keys = $resource->getAllAvailableMetadataKeys(); + $metadata = []; + foreach ($keys as $key) { + $metadata[$key] = $resource->getMetadataForKey($key); + } + + return $metadata; + } + + /** + * @param string $table + * @param string $foreignKey + * @param int $id + * + * @return array + */ + private function getAllMetadataOfCache(string $table, + string $foreignKey, + int $id): array { + $query = $this->dbConnection->getQueryBuilder(); + $query->select(['key', 'value']) + ->from($table) + ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))); + $result = $query->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + + $metadata = []; + foreach ($rows as $row) { + $metadata[$row['key']] = $row['value']; + } + + return $metadata; + } + + /** + * Gets all cached rooms / resources by backend + * + * @param string $tableName + * @param string $backendId + * + * @return array + */ + private function getAllCachedByBackend(string $tableName, + string $backendId): array { + $query = $this->dbConnection->getQueryBuilder(); + $query->select('resource_id') + ->from($tableName) + ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))); + $result = $query->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + + return array_map(function ($row): string { + return $row['resource_id']; + }, $rows); + } + + /** + * @param string $principalPrefix + * @param string $principalUri + */ + private function deleteCalendarDataForResource(string $principalPrefix, + string $principalUri): void { + $calendar = $this->calDavBackend->getCalendarByUri( + implode('/', [$principalPrefix, $principalUri]), + CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI); + + if ($calendar !== null) { + $this->calDavBackend->deleteCalendar( + $calendar['id'], + true // Because this wasn't deleted by a user + ); + } + } + + /** + * @param string $table + * @param string $backendId + * @param string $resourceId + * + * @return int + */ + private function getIdForBackendAndResource(string $table, + string $backendId, + string $resourceId): int { + $query = $this->dbConnection->getQueryBuilder(); + $query->select('id') + ->from($table) + ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))) + ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId))); + $result = $query->executeQuery(); + + $id = (int) $result->fetchOne(); + $result->closeCursor(); + return $id; + } +} diff --git a/apps/dav/lib/Command/SyncResourcesRooms.php b/apps/dav/lib/Command/SyncResourcesRooms.php new file mode 100644 index 0000000000000..b0394200f764a --- /dev/null +++ b/apps/dav/lib/Command/SyncResourcesRooms.php @@ -0,0 +1,53 @@ + + * + * @author Christoph Wurst + * + * @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\DAV\Command; + +use OCA\DAV\CalDAV\CalendarResourcesRoomsSyncService; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class SyncResourcesRooms extends Command { + + private CalendarResourcesRoomsSyncService $syncService; + + public function __construct(CalendarResourcesRoomsSyncService $syncService) { + parent::__construct(); + $this->syncService = $syncService; + } + + protected function configure() { + $this + ->setName('dav:sync-resources-rooms') + ->setDescription('Sync resources and rooms'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->syncService->sync(); + + return 0; + } +} diff --git a/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php b/apps/dav/tests/unit/CalDAV/UpdateCalendarResourcesRoomsBackgroundJobTest.php similarity index 92% rename from apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php rename to apps/dav/tests/unit/CalDAV/UpdateCalendarResourcesRoomsBackgroundJobTest.php index 59b68452862ed..6c5fea32a3b6a 100644 --- a/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php +++ b/apps/dav/tests/unit/CalDAV/UpdateCalendarResourcesRoomsBackgroundJobTest.php @@ -1,4 +1,24 @@ + * + * @author 2022 Christoph Wurst + * + * @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 . + */ declare(strict_types=1); @@ -29,10 +49,8 @@ */ namespace OCA\DAV\Tests\unit\BackgroundJob; -use OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob; - use OCA\DAV\CalDAV\CalDavBackend; -use OCP\AppFramework\Utility\ITimeFactory; +use OCA\DAV\CalDAV\CalendarResourcesRoomsSyncService; use OCP\Calendar\BackendTemporarilyUnavailableException; use OCP\Calendar\IMetadataProvider; use OCP\Calendar\Resource\IBackend; @@ -47,9 +65,6 @@ interface tmpI extends IResource, IMetadataProvider { class UpdateCalendarResourcesRoomsBackgroundJobTest extends TestCase { - /** @var ITimeFactory|MockObject */ - private $time; - /** @var IResourceManager|MockObject */ private $resourceManager; @@ -59,19 +74,17 @@ class UpdateCalendarResourcesRoomsBackgroundJobTest extends TestCase { /** @var CalDavBackend|MockObject */ private $calDavBackend; - /** @var UpdateCalendarResourcesRoomsBackgroundJob */ - private $backgroundJob; + /** @var CalendarResourcesRoomsSyncService */ + private $syncService; protected function setUp(): void { parent::setUp(); - $this->time = $this->createMock(ITimeFactory::class); $this->resourceManager = $this->createMock(IResourceManager::class); $this->roomManager = $this->createMock(IRoomManager::class); $this->calDavBackend = $this->createMock(CalDavBackend::class); - $this->backgroundJob = new UpdateCalendarResourcesRoomsBackgroundJob( - $this->time, + $this->syncService = new CalendarResourcesRoomsSyncService( $this->resourceManager, $this->roomManager, self::$realDatabase, @@ -116,7 +129,7 @@ protected function tearDown(): void { * [backend4, res9, Beamer2, {}] - [] */ - public function testRun() { + public function testRun(): void { $this->createTestResourcesInCache(); $backend2 = $this->createMock(IBackend::class); @@ -226,7 +239,7 @@ public function testRun() { ['backend4', $backend4], ]); - $this->backgroundJob->run([]); + $this->syncService->sync(); $query = self::$realDatabase->getQueryBuilder(); $query->select('*')->from('calendar_resources'); @@ -353,7 +366,7 @@ public function testRun() { ], $rows2); } - protected function createTestResourcesInCache() { + protected function createTestResourcesInCache(): void { $query = self::$realDatabase->getQueryBuilder(); $query->insert('calendar_resources') ->values([ From 077f8553feb2b533d77ae6b92b1bff250544b9b2 Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Thu, 24 Nov 2022 09:41:31 +0100 Subject: [PATCH 2/2] fixup! Make CalDAV resource sync a service and expose as occ command Signed-off-by: Christoph Wurst --- apps/dav/lib/AppInfo/Application.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index f25c076103a63..fbafd090ebe65 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -32,7 +32,6 @@ */ namespace OCA\DAV\AppInfo; -use Exception; use OCA\DAV\BackgroundJob\CalendarResourcesRoomsSyncService; use OCA\DAV\CalDAV\Activity\Backend; use OCA\DAV\CalDAV\CalendarManager; @@ -44,7 +43,6 @@ use OCA\DAV\CalDAV\Reminder\Notifier; use OCA\DAV\Capabilities; -use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\CardDAV\ContactsManager; use OCA\DAV\CardDAV\PhotoCache; use OCA\DAV\CardDAV\SyncService; @@ -243,20 +241,6 @@ public function registerHooks(HookManager $hm, // Here we should recalculate if reminders should be sent to new or old sharees }); - - $eventHandler = function () use ($container, $serverContainer): void { - try { - /** @var CalendarResourcesRoomsSyncService $job */ - $job = $container->query(CalendarResourcesRoomsSyncService::class); - $job->run([]); - $serverContainer->getJobList()->setLastRun($job); - } catch (Exception $ex) { - $serverContainer->get(LoggerInterface::class)->error($ex->getMessage(), ['exception' => $ex]); - } - }; - - $dispatcher->addListener('\OCP\Calendar\Resource\ForceRefreshEvent', $eventHandler); - $dispatcher->addListener('\OCP\Calendar\Room\ForceRefreshEvent', $eventHandler); } public function registerContactsManager(IContactsManager $cm, IAppContainer $container): void {