Skip to content

Commit

Permalink
New sync mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
jvillafanez committed Oct 9, 2023
1 parent f3534f5 commit 92ef028
Show file tree
Hide file tree
Showing 16 changed files with 1,789 additions and 0 deletions.
209 changes: 209 additions & 0 deletions core/Command/Sync/Sync.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<?php
/**
* @copyright Copyright (c) 2023, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/

namespace OC\Core\Command\Sync;

use OCP\Sync\ISyncManager;
use OCP\Sync\ISyncer;
use OCP\Sync\SyncException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\ProgressBar;

class Sync extends Command {
/** @var ISyncManager */
private $syncManager;

public function __construct(ISyncManager $syncManager) {
parent::__construct();
$this->syncManager = $syncManager;
}

protected function configure() {
$this->setName('sync:sync')
->setDescription('sync any of the registered sync services')
->addArgument(
'service',
InputArgument::REQUIRED,
'The service that will sync'
)->addOption(
'only-one',
null,
InputOption::VALUE_REQUIRED,
'check and sync only the item with the id provided',
)->addOption(
'option',
'o',
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'options to be used with the service, in "<key>=<value>" form'
);
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$service = $input->getArgument('service');

$syncer = $this->syncManager->getSyncer($service);
if ($syncer === null) {
$output->writeln("{$service} not found");
return 1;
}

$opts = [];
if ($input->getOption('option') !== null) {
foreach ($input->getOption('option') as $option) {
$keyValue = \explode('=', $option, 2);
$opts[$keyValue[0]] = $keyValue[1];
}
}

$target = $input->getOption('only-one');
if ($target !== null) {
$checkOk = $this->checkOnlyOne($syncer, $target, $opts, $output);
$syncOk = $this->syncOnlyOne($syncer, $target, $opts, $output);
} else {
$checkOk = $this->checkLocalData($syncer, $opts, $output);
$syncOk = $this->syncRemoteData($syncer, $opts, $output);
}

if (!$checkOk || !$syncOk) {
return 2;
}

return 0;
}

private function checkOnlyOne(ISyncer $syncer, $target, $opts, OutputInterface $output) {
try {
$state = $syncer->checkOne($target, $opts);
if ($state !== ISyncer::CHECK_STATE_NO_CHANGE) {
$output->writeln("{$target} state changed to {$state}");
}
return $state === ISyncer::CHECK_STATE_NO_CHANGE;
} catch (SyncException $ex) {
$this->outputExceptions($output, $ex);
}
return false;
}

private function syncOnlyOne(ISyncer $syncer, $target, $opts, OutputInterface $output) {
try {
$syncOk = $syncer->syncOne($target, $opts);
if (!$syncOk) {
$output->writeln("{$target} cannot be synced because it isn't found remotely");
}
return $syncOk;
} catch (SyncException $ex) {
$this->outputExceptions($output, $ex);
}
return false;
}

private function checkLocalData(ISyncer $syncer, $opts, OutputInterface $output) {
$itemStateList = [];
$errorList = [];

// Check local data
$output->writeln('Checking local data');
$progress = new ProgressBar($output);
$progress->start($syncer->localItemCount($opts));
$syncer->check(function ($item, $state) use ($output, $progress, &$itemStateList, &$errorList) {
if (\is_array($item) && $state !== ISyncer::CHECK_STATE_NO_CHANGE) {
$key = \array_key_first($item);
$itemStateList[] = [
'key' => $key,
'value' => $item[$key],
'state' => $state,
];
}
if ($item instanceof \Exception) {
$errorList[] = $item;
} else {
$progress->advance();
}
}, $opts);
$progress->finish();
$output->writeln('');

if (!empty($itemStateList)) {
$output->writeln('');
foreach ($itemStateList as $itemState) {
$output->writeln("- State for item with {$itemState['key']} = {$itemState['value']} has changed to {$itemState['state']}");
}
}

if (!empty($errorList)) {
$output->writeln('');
$output->writeln('Following errors happened:');
$this->outputExceptions($output, $errorList);

return false;
}

return true;
}

private function syncRemoteData(ISyncer $syncer, $opts, OutputInterface $output) {
$output->writeln('');
$errorList = [];
// Sync remote data
$output->writeln('Syncing remote data');
$progress = new ProgressBar($output);
$progress->start($syncer->remoteItemCount($opts));
$syncer->sync(function ($item) use ($output, $progress, &$errorList) {
if ($item instanceof \Exception) {
$errorList[] = $item;
} else {
$progress->advance();
}
}, $opts);
$progress->finish();
$output->writeln('');

if (!empty($errorList)) {
$output->writeln('');
$output->writeln('Following errors happened:');
$this->outputExceptions($output, $errorList);

return false;
}

return true;
}

private function outputExceptions(OutputInterface $output, $exList) {
$prefix = '- ';
if (!\is_array($exList)) {
$prefix = "Error: ";
$exList = [$exList];
}

foreach ($exList as $errorItem) {
$output->writeln("{$prefix}{$errorItem->getMessage()}");
$previous = $errorItem->getPrevious();
while ($previous !== null) {
$previousClass = \get_class($previous);
$output->writeln(" Caused by: {$previousClass}: {$previous->getMessage()}");
$previous = $previous->getPrevious();
}
}
}
}
2 changes: 2 additions & 0 deletions core/register_command.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@
$application->add(new OC\Core\Command\Group\ListGroups(\OC::$server->getGroupManager()));
$application->add(new OC\Core\Command\Group\ListGroupMembers(\OC::$server->getGroupManager()));

$application->add(new OC\Core\Command\Sync\Sync(\OC::$server->getSyncManager()));

$application->add(new OC\Core\Command\Security\ListCertificates(\OC::$server->getCertificateManager(null), \OC::$server->getL10N('core')));
$application->add(new OC\Core\Command\Security\ImportCertificate(\OC::$server->getCertificateManager(null)));
$application->add(new OC\Core\Command\Security\RemoveCertificate(\OC::$server->getCertificateManager(null)));
Expand Down
20 changes: 20 additions & 0 deletions lib/private/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
use OC\User\AccountTermMapper;
use OC\User\Session;
use OC\User\SyncService;
use OC\Sync\User\UserSyncDBBackend;
use OC\Sync\User\UserSyncer;
use OC\Sync\SyncManager;
use OCP\App\IServiceLoader;
use OCP\AppFramework\QueryException;
use OCP\AppFramework\Utility\ITimeFactory;
Expand All @@ -111,6 +114,7 @@
use OCP\Shutdown\IShutdownManager;
use OCP\Theme\IThemeService;
use OCP\Util\UserSearch;
use OCP\Sync\ISyncManager;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use OC\Files\External\StoragesBackendService;
Expand Down Expand Up @@ -976,6 +980,18 @@ public function __construct($webRoot, \OC\Config $config) {
);
return $policyManager;
});

$this->registerService(SyncManager::class, function ($c) {
$userSyncDbBackend = new UserSyncDBBackend(new \OC\User\Database()); // anything better?
$userSyncer = new UserSyncer($c->getUserManager(), $c->getAccountMapper(), $c->getConfig(), $c->getLogger());
$userSyncer->registerBackend($userSyncDbBackend);

$syncManager = new SyncManager();
$syncManager->registerSyncer('user', $userSyncer);
return $syncManager;
});
$this->registerAlias(ISyncManager::class, SyncManager::class);
}

/**
Expand Down Expand Up @@ -1759,4 +1775,8 @@ public function getLicenseManager() {
public function getLoginPolicyManager() {
return $this->query(LoginPolicyManager::class);
}

public function getSyncManager() {
return $this->query(ISyncManager::class);
}
}
72 changes: 72 additions & 0 deletions lib/private/Sync/SyncManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
/**
* @copyright Copyright (c) 2023, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/

namespace OC\Sync;

use OCP\Sync\ISyncManager;
use OCP\Sync\ISyncer;
use OCP\Sync\User\IUserSyncer;

class SyncManager implements ISyncManager {
/** @var array */
private $register = [];

/**
* @inheritDoc
*/
public function registerSyncer(string $name, ISyncer $syncer): bool {
if (isset($this->register[$name])) {
return false;
}
$this->register[$name] = $syncer;
return true;
}

/**
* {@inheritDoc}
*
* This method will return false if the name is NOT taken (and we can't
* overwrite it)
*/
public function overwriteSyncer(string $name, ISyncer $syncer): bool {
if (isset($this->register[$name])) {
$this->register[$name] = $syncer;
return true;
}
return false;
}

/**
* @inheritDoc
*/
public function getSyncer(string $name): ?ISyncer {
return $this->register[$name] ?? null;
}

/**
* @inheritDoc
*/
public function getUserSyncer(): ?IUserSyncer {
$syncer = $this->register['user'] ?? null;
if ($syncer !== null && $syncer instanceof IUserSyncer) {
return $syncer;
}
return null;
}
}
Loading

0 comments on commit 92ef028

Please sign in to comment.