Skip to content

Commit 3e5ab8e

Browse files
pteroca-comksroga
andauthored
Fix for 500 error when deleting server in Web UI that does not exist in Pterodactyl Panel anymore (#67)
* PteroCA throws 500 error when deleting server in Web UI that does not exist in Pterodactyl Panel anymore * Added new sync servers command * Moved services to new directory * Removed useless comments * Updated command description --------- Co-authored-by: Konrad Sroga <[email protected]>
1 parent 94414da commit 3e5ab8e

File tree

7 files changed

+294
-11
lines changed

7 files changed

+294
-11
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace App\Core\Command;
4+
5+
use App\Core\Handler\SyncServersHandler;
6+
use Symfony\Component\Console\Attribute\AsCommand;
7+
use Symfony\Component\Console\Command\Command;
8+
use Symfony\Component\Console\Input\InputInterface;
9+
use Symfony\Component\Console\Input\InputOption;
10+
use Symfony\Component\Console\Output\OutputInterface;
11+
use Symfony\Component\Console\Style\SymfonyStyle;
12+
13+
#[AsCommand(
14+
name: 'pteroca:sync-servers',
15+
description: 'Synchronize servers between Pterodactyl and PteroCA (cleanup orphaned servers)',
16+
)]
17+
class PterocaSyncServersCommand extends Command
18+
{
19+
public function __construct(
20+
private readonly SyncServersHandler $syncServersHandler,
21+
)
22+
{
23+
parent::__construct();
24+
}
25+
26+
protected function configure(): void
27+
{
28+
$this->addOption(
29+
'limit',
30+
null,
31+
InputOption::VALUE_OPTIONAL,
32+
'Limit the number of servers to check',
33+
);
34+
$this->addOption(
35+
'dry-run',
36+
null,
37+
InputOption::VALUE_NONE,
38+
'Show what would be done without making changes'
39+
);
40+
$this->addOption(
41+
'auto',
42+
null,
43+
InputOption::VALUE_NONE,
44+
'Automatically delete orphaned servers without asking for confirmation (suitable for cron jobs)'
45+
);
46+
}
47+
48+
protected function execute(InputInterface $input, OutputInterface $output): int
49+
{
50+
$io = new SymfonyStyle($input, $output);
51+
$dryRun = $input->getOption('dry-run');
52+
$auto = $input->getOption('auto');
53+
54+
if ($dryRun) {
55+
$io->note('Running in dry-run mode - no changes will be made');
56+
}
57+
58+
if ($auto) {
59+
$io->note('Running in automatic mode - orphaned servers will be deleted automatically');
60+
}
61+
62+
$this->syncServersHandler
63+
->setLimit($input->getOption('limit') ?: 1000)
64+
->setIo($io)
65+
->handle($dryRun, $auto);
66+
67+
return Command::SUCCESS;
68+
}
69+
}

src/Core/Command/PterodactylMigrateServersCommand.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
#[AsCommand(
1414
name: 'pterodactyl:migrate-servers',
15-
description: 'Migrate servers from Pterodactyl to existing user accounts',
15+
description: 'Migrate servers from Pterodactyl to existing user accounts in PteroCA',
1616
)]
1717
class PterodactylMigrateServersCommand extends Command
1818
{
@@ -31,17 +31,27 @@ protected function configure(): void
3131
InputOption::VALUE_OPTIONAL,
3232
'Limit the number of servers to migrate',
3333
);
34+
$this->addOption(
35+
'dry-run',
36+
null,
37+
InputOption::VALUE_NONE,
38+
'Show what would be done without making changes'
39+
);
3440
}
3541

3642
protected function execute(InputInterface $input, OutputInterface $output): int
3743
{
3844
$io = new SymfonyStyle($input, $output);
45+
$dryRun = $input->getOption('dry-run');
46+
47+
if ($dryRun) {
48+
$io->note('Running in dry-run mode - no changes will be made');
49+
}
50+
3951
$this->migrateServersHandler
4052
->setLimit($input->getOption('limit') ?: 100)
4153
->setIo($io)
42-
->handle();
43-
44-
$io->success('You have a new command! Now make it your own! Pass --help to see your options.');
54+
->handle($dryRun);
4555

4656
return Command::SUCCESS;
4757
}

src/Core/Handler/MigrateServersHandler.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,15 @@ public function setIo(SymfonyStyle $io): self
5454
return $this;
5555
}
5656

57-
public function handle(): void
57+
public function handle(bool $dryRun = false): void
5858
{
5959
$this->io->title('Pterodactyl Server Migration');
6060
$this->pterodactylApi = $this->pterodactylService->getApi();
6161

62+
if ($dryRun) {
63+
$this->io->info('Running in dry-run mode - no changes will be made');
64+
}
65+
6266
$pterodactylServers = $this->getPterodactylServers();
6367
$pterodactylUsers = $this->getPterodactylUsers();
6468
$pterocaServers = $this->getPterocaServers();
@@ -108,13 +112,19 @@ public function handle(): void
108112
$duration = $this->askForDuration();
109113
$price = $this->askForPrice();
110114

111-
$this->migrateServer(
112-
$pterodactylServer->toArray(),
113-
$pterodactylServerOwner->get('email'),
114-
$duration,
115-
$price
116-
);
115+
if (!$dryRun) {
116+
$this->migrateServer(
117+
$pterodactylServer->toArray(),
118+
$pterodactylServerOwner->get('email'),
119+
$duration,
120+
$price
121+
);
122+
} else {
123+
$this->io->info('Dry run: Would migrate server but not saving changes');
124+
}
117125
}
126+
127+
$this->io->success('Server migration completed.');
118128
}
119129

120130
private function migrateServer(
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace App\Core\Handler;
4+
5+
use App\Core\Service\Sync\PterodactylSyncService;
6+
use App\Core\Service\Sync\PterodactylCleanupService;
7+
use Symfony\Component\Console\Style\SymfonyStyle;
8+
9+
class SyncServersHandler implements HandlerInterface
10+
{
11+
private int $limit = 1000;
12+
13+
private SymfonyStyle $io;
14+
15+
public function __construct(
16+
private readonly PterodactylSyncService $syncService,
17+
private readonly PterodactylCleanupService $cleanupService,
18+
)
19+
{
20+
}
21+
22+
public function setLimit(int $limit): self
23+
{
24+
$this->limit = $limit;
25+
26+
return $this;
27+
}
28+
29+
public function setIo(SymfonyStyle $io): self
30+
{
31+
$this->io = $io;
32+
33+
return $this;
34+
}
35+
36+
public function handle(bool $dryRun = false, bool $auto = false): void
37+
{
38+
$this->io->title('PteroCA Server Synchronization');
39+
40+
if ($dryRun) {
41+
$this->io->info('Running in dry-run mode - no changes will be made');
42+
}
43+
44+
if ($auto) {
45+
$this->io->info('Running in automatic mode - orphaned servers will be deleted automatically');
46+
}
47+
48+
$this->io->section('Fetching servers from Pterodactyl...');
49+
$existingPterodactylServerIds = $this->syncService->getExistingPterodactylServerIds($this->limit);
50+
51+
$this->io->section('Cleaning up orphaned servers in PteroCA...');
52+
$deletedServersCount = $this->cleanupService->cleanupOrphanedServers(
53+
$existingPterodactylServerIds,
54+
$auto ? null : $this->io,
55+
$dryRun
56+
);
57+
58+
$this->io->success(sprintf(
59+
'Server synchronization completed. Found %d existing servers in Pterodactyl, %s %d orphaned servers in PteroCA.',
60+
count($existingPterodactylServerIds),
61+
$dryRun ? 'would delete' : 'deleted',
62+
$deletedServersCount
63+
));
64+
}
65+
}

src/Core/Repository/ServerRepository.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,17 @@ public function getAllServersOwnedCount(int $userId): int
9191
->getQuery()
9292
->getSingleScalarResult();
9393
}
94+
95+
public function findOrphanedServers(array $existingPterodactylServerIds): array
96+
{
97+
$queryBuilder = $this->createQueryBuilder('s')
98+
->where('s.deletedAt IS NULL');
99+
100+
if (!empty($existingPterodactylServerIds)) {
101+
$queryBuilder->andWhere('s.pterodactylServerId NOT IN (:existingIds)')
102+
->setParameter('existingIds', $existingPterodactylServerIds);
103+
}
104+
105+
return $queryBuilder->getQuery()->getResult();
106+
}
94107
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace App\Core\Service\Sync;
4+
5+
use App\Core\Repository\ServerRepository;
6+
use Doctrine\ORM\EntityManagerInterface;
7+
use Psr\Log\LoggerInterface;
8+
use Symfony\Component\Console\Style\SymfonyStyle;
9+
10+
class PterodactylCleanupService
11+
{
12+
public function __construct(
13+
private readonly EntityManagerInterface $entityManager,
14+
private readonly ServerRepository $serverRepository,
15+
private readonly LoggerInterface $logger
16+
) {
17+
}
18+
19+
public function cleanupOrphanedServers(array $existingPterodactylServerIds, ?SymfonyStyle $io = null, bool $dryRun = false): int
20+
{
21+
$this->logger->info('Starting cleanup of orphaned servers in PteroCA', ['dry_run' => $dryRun]);
22+
23+
if (empty($existingPterodactylServerIds)) {
24+
$this->logger->warning('No Pterodactyl server IDs provided for cleanup');
25+
return 0;
26+
}
27+
28+
$orphanedServers = $this->serverRepository->findOrphanedServers($existingPterodactylServerIds);
29+
30+
$deletedCount = 0;
31+
32+
foreach ($orphanedServers as $server) {
33+
if ($io && !$this->isUserWantDeleteServer($server, $io)) {
34+
if ($io) {
35+
$io->info(sprintf(
36+
'Skipping server #%s (ID: %d)...',
37+
$server->getPterodactylServerIdentifier(),
38+
$server->getPterodactylServerId()
39+
));
40+
}
41+
continue;
42+
}
43+
44+
if (!$dryRun) {
45+
$server->setDeletedAtValue();
46+
}
47+
48+
$this->logger->info($dryRun ? 'Would mark server as deleted' : 'Marked server as deleted', [
49+
'server_id' => $server->getId(),
50+
'pterodactyl_server_id' => $server->getPterodactylServerId(),
51+
'pterodactyl_server_identifier' => $server->getPterodactylServerIdentifier(),
52+
'dry_run' => $dryRun
53+
]);
54+
$deletedCount++;
55+
}
56+
57+
if ($deletedCount > 0 && !$dryRun) {
58+
$this->entityManager->flush();
59+
} elseif ($dryRun && $deletedCount > 0) {
60+
$this->logger->info('Dry run mode - changes not saved to database');
61+
}
62+
63+
$this->logger->info('Cleanup completed', ['deleted_servers_count' => $deletedCount]);
64+
65+
return $deletedCount;
66+
}
67+
68+
private function isUserWantDeleteServer($server, SymfonyStyle $io): bool
69+
{
70+
$questionMessage = sprintf(
71+
'Server #%s (ID: %d) was not found in Pterodactyl. Do you want to delete it from PteroCA?',
72+
$server->getPterodactylServerIdentifier(),
73+
$server->getPterodactylServerId()
74+
);
75+
76+
return strtolower($io->ask($questionMessage, 'yes')) === 'yes';
77+
}
78+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace App\Core\Service\Sync;
4+
5+
use App\Core\Repository\ServerRepository;
6+
use App\Core\Service\Pterodactyl\PterodactylService;
7+
use Psr\Log\LoggerInterface;
8+
9+
class PterodactylSyncService
10+
{
11+
public function __construct(
12+
private readonly PterodactylService $pterodactylService,
13+
private readonly ServerRepository $serverRepository,
14+
private readonly LoggerInterface $logger
15+
) {
16+
}
17+
18+
public function getExistingPterodactylServerIds(int $limit = 1000): array
19+
{
20+
$this->logger->info('Fetching existing servers from Pterodactyl');
21+
22+
$pterodactylApi = $this->pterodactylService->getApi();
23+
$pterodactylServers = $pterodactylApi->servers->all([
24+
'per_page' => $limit,
25+
]);
26+
27+
$existingServerIds = [];
28+
foreach ($pterodactylServers->toArray() as $server) {
29+
$existingServerIds[] = $server['id'];
30+
}
31+
32+
$this->logger->info('Found existing servers in Pterodactyl', [
33+
'count' => count($existingServerIds)
34+
]);
35+
36+
return $existingServerIds;
37+
}
38+
}

0 commit comments

Comments
 (0)