diff --git a/lib/Command/SyncMissingUsersToCommon.php b/lib/Command/SyncMissingUsersToCommon.php index 2ceba7209c19a959cfbf614d148777fb53bbeb09..c4828dba780854654784081ea4bbd5411e0ab928 100644 --- a/lib/Command/SyncMissingUsersToCommon.php +++ b/lib/Command/SyncMissingUsersToCommon.php @@ -89,14 +89,19 @@ class SyncMissingUsersToCommon extends Command { */ private function processUsers(array $usernames, string $ipAddress, bool $isDryRun, OutputInterface $output): array { $stats = [ + 'total' => 0, 'processed' => 0, 'success' => 0, - 'errors' => 0 + 'errors' => 0, + 'failed_reasons' => [] ]; if (!empty($usernames)) { + $stats['total'] = count($usernames); $this->processSpecificUsers($usernames, $ipAddress, $isDryRun, $output, $stats); } else { + // Don't pre-count for all users - it's an expensive operation + $output->writeln('Processing all users from NextCloud (counting during processing)'); $this->processAllUsers($ipAddress, $isDryRun, $output, $stats); } @@ -107,10 +112,17 @@ class SyncMissingUsersToCommon extends Command { * Process a list of specific users */ private function processSpecificUsers(array $usernames, string $ipAddress, bool $isDryRun, OutputInterface $output, array &$stats): void { - $output->writeln(sprintf('Processing %d specific users', count($usernames))); + $output->writeln(sprintf('Processing %d specific users', $stats['total'])); foreach ($usernames as $username) { - $this->processSingleUser($username, $ipAddress, $isDryRun, $output, $stats); + try { + $this->processSingleUser($username, $ipAddress, $isDryRun, $output, $stats); + } catch (Exception $e) { + $errorMessage = sprintf('Unexpected error processing user %s: %s', $username, $e->getMessage()); + $output->writeln(sprintf('%s', $errorMessage)); + $stats['errors']++; + $stats['failed_reasons'][] = $errorMessage; + } } } @@ -118,52 +130,92 @@ class SyncMissingUsersToCommon extends Command { * Process all users from NextCloud */ private function processAllUsers(string $ipAddress, bool $isDryRun, OutputInterface $output, array &$stats): void { - $userCount = 0; - $output->writeln('Processing all users from NextCloud'); - - $this->userManager->callForAllUsers(function (IUser $user) use (&$userCount, $ipAddress, $isDryRun, $output, &$stats) { - $this->processSingleUser($user->getUID(), $ipAddress, $isDryRun, $output, $stats); - $userCount++; - if ($userCount > 0 && $userCount % 100 === 0) { - $output->writeln(sprintf('Progress: %d processed, %d success, %d errors', - $userCount, $stats['success'], $stats['errors'])); + $this->userManager->callForAllUsers(function (IUser $user) use ($ipAddress, $isDryRun, $output, &$stats) { + try { + $this->processSingleUser($user->getUID(), $ipAddress, $isDryRun, $output, $stats); + + // Reduce output frequency for better performance + if ($stats['processed'] > 0 && $stats['processed'] % 1000 === 0) { + $output->writeln(sprintf('Progress: %d processed, %d success, %d errors', + $stats['processed'], $stats['success'], $stats['errors'])); + } + } catch (Exception $e) { + $errorMessage = sprintf('Unexpected error processing user %s: %s', $user->getUID(), $e->getMessage()); + $output->writeln(sprintf('%s', $errorMessage)); + $stats['errors']++; + $stats['failed_reasons'][] = $errorMessage; } }); + + // Set total after processing for all users + $stats['total'] = $stats['processed']; } /** * Process a single user */ private function processSingleUser(string $username, string $ipAddress, bool $isDryRun, OutputInterface $output, array &$stats): void { - $user = $this->userManager->get($username); - if (!$user) { - $output->writeln(sprintf('User not found: %s', $username)); - $stats['errors']++; - return; - } + $stats['processed']++; // Increment processed count first for all users + + try { + $user = $this->userManager->get($username); + if (!$user) { + $errorMessage = sprintf('User not found: %s', $username); + $output->writeln(sprintf('%s', $errorMessage)); + $stats['errors']++; + $stats['failed_reasons'][] = $errorMessage; + return; + } - // Strip legacy domain from username before checking in common DB - $usernameWithoutDomain = $this->userService->stripLegacyDomainFromUsername($username); - if ($this->userService->isUsernameTaken($usernameWithoutDomain)) { - return; // Skip if already exists - } + // Strip legacy domain from username before checking in common DB + $usernameWithoutDomain = $this->userService->stripLegacyDomainFromUsername($username); + if ($this->userService->isUsernameTaken($usernameWithoutDomain)) { + // This is not an error, just a skip - user already exists + return; + } - try { $this->syncUserToCommon($username, $ipAddress, $isDryRun, $output); $stats['success']++; } catch (Exception $e) { - $output->writeln(sprintf('Error syncing user %s: %s', $username, $e->getMessage())); + $errorMessage = sprintf('Error processing user %s: %s', $username, $e->getMessage()); + $output->writeln(sprintf('%s', $errorMessage)); $stats['errors']++; + $stats['failed_reasons'][] = $errorMessage; } - $stats['processed']++; } /** * Display final results */ private function displayFinalResults(array $stats, OutputInterface $output): void { - $output->writeln(sprintf('Final result: %d users processed, %d success, %d errors.', - $stats['processed'], $stats['success'], $stats['errors'])); + $output->writeln(''); + $output->writeln('=== FINAL RESULTS ==='); + $output->writeln(sprintf('Total users to process: %d', $stats['total'])); + $output->writeln(sprintf('Users processed: %d', $stats['processed'])); + $output->writeln(sprintf('Successfully synced: %d', $stats['success'])); + $output->writeln(sprintf('Errors encountered: %d', $stats['errors'])); + + if (!empty($stats['failed_reasons'])) { + $output->writeln(''); + $output->writeln('=== FAILURE REASONS ==='); + + // Limit the number of reasons shown for performance + $maxReasonsToShow = 50; + $reasonsToShow = array_slice($stats['failed_reasons'], 0, $maxReasonsToShow); + + foreach ($reasonsToShow as $reason) { + $output->writeln(sprintf('• %s', $reason)); + } + + if (count($stats['failed_reasons']) > $maxReasonsToShow) { + $remaining = count($stats['failed_reasons']) - $maxReasonsToShow; + $output->writeln(sprintf('... and %d more errors.', $remaining)); + } + } + + $output->writeln(''); + $output->writeln(sprintf('Summary: %d/%d users processed successfully', + $stats['success'], $stats['total'])); } /** @@ -176,48 +228,64 @@ class SyncMissingUsersToCommon extends Command { * @return void */ private function syncUserToCommon(string $username, string $ipAddress, bool $isDryRun, OutputInterface $output): void { - $output->writeln(sprintf('Processing user: %s', $username)); - - // Check if user exists and is on LDAP backend - $user = $this->userManager->get($username); - if (!$user) { - throw new Exception("User not found in NextCloud"); + // Only show processing message in dry run mode for performance + if ($isDryRun) { + $output->writeln(sprintf('Processing user: %s', $username)); } - if (!$this->ldapConnectionService->isUserOnLDAPBackend($user)) { - throw new Exception("User is not on LDAP backend"); - } + try { + // Get user from userManager (already verified to exist in processSingleUser) + $user = $this->userManager->get($username); + + // Check if user is on LDAP backend - skip if not + if (!$this->ldapConnectionService->isUserOnLDAPBackend($user)) { + // Skip users that are not on LDAP backend + if ($isDryRun) { + $output->writeln(sprintf(' Skipping user %s: not on LDAP backend', $username)); + } + return; // Skip this user gracefully + } - // Get user metadata from LDAP - $userMetadata = $this->ldapConnectionService->getUserMetadata($username); + // Get user metadata from LDAP + $userMetadata = $this->ldapConnectionService->getUserMetadata($username); - // Use usernameWithoutDomain from LDAP metadata - $usernameWithoutDomain = $userMetadata['usernameWithoutDomain']; + // Use usernameWithoutDomain from LDAP metadata + $usernameWithoutDomain = $userMetadata['usernameWithoutDomain']; - // Convert LDAP createTimestamp to MySQL datetime format - $ldapTimestamp = $userMetadata['createTimestamp'] ?? null; - $createdAt = $ldapTimestamp; + // Convert LDAP createTimestamp to MySQL datetime format + $ldapTimestamp = $userMetadata['createTimestamp'] ?? null; + $createdAt = $ldapTimestamp; - if ($ldapTimestamp) { - $dateTime = \DateTime::createFromFormat('YmdHis\Z', $ldapTimestamp, new \DateTimeZone('UTC')); - if ($dateTime !== false) { - $createdAt = $dateTime->format('Y-m-d H:i:s'); // Format compatible with MySQL + if ($ldapTimestamp) { + $dateTime = \DateTime::createFromFormat('YmdHis\Z', $ldapTimestamp, new \DateTimeZone('UTC')); + if ($dateTime !== false) { + $createdAt = $dateTime->format('Y-m-d H:i:s'); // Format compatible with MySQL + } } - } - $output->writeln(sprintf(' Found user in LDAP: %s (created: %s)', $usernameWithoutDomain, $createdAt)); + // Only show detailed output in dry run mode + if ($isDryRun) { + $output->writeln(sprintf(' Found user in LDAP: %s (created: %s)', $usernameWithoutDomain, $createdAt)); + } - if (!$isDryRun) { - // Add user to common database - $this->userService->addUsernameToCommonDataStore( - $usernameWithoutDomain, - $ipAddress, - $userMetadata['recoveryMailAddress'], - $createdAt - ); - $output->writeln(sprintf(' ✓ User synced successfully to common database: %s', $usernameWithoutDomain)); - } else { - $output->writeln(sprintf(' Would sync user to common database with recovery email: %s and created_at: %s', $userMetadata['recoveryMailAddress'], $createdAt)); + if (!$isDryRun) { + // Add user to common database + $this->userService->addUsernameToCommonDataStore( + $usernameWithoutDomain, + $ipAddress, + $userMetadata['recoveryMailAddress'], + $createdAt + ); + // Only show success message in dry run mode for performance + if ($isDryRun) { + $output->writeln(sprintf(' ✓ User synced successfully to common database: %s', $usernameWithoutDomain)); + } + } else { + $output->writeln(sprintf(' Would sync user to common database with recovery email: %s and created_at: %s', $userMetadata['recoveryMailAddress'], $createdAt)); + } + } catch (Exception $e) { + // Re-throw the exception to be caught by the caller + throw new Exception(sprintf("Failed to sync user %s: %s", $username, $e->getMessage()), 0, $e); } } }