label(__('Start Over')) ->icon('heroicon-o-arrow-path') ->color('gray') ->requiresConfirmation() ->modalHeading(__('Start Over')) ->modalDescription(__('This will reset all migration data. Are you sure?')) ->action('resetMigration'), ]; } public function mount(): void { $this->restoreMigrationStateFromSession(); } public function updatedHostname(): void { $this->resetConnection(); } public function updatedCpanelUsername(): void { $this->resetConnection(); } public function updatedApiToken(): void { $this->resetConnection(); } public function updatedPort(): void { $this->resetConnection(); } public function updatedUseSSL(): void { $this->resetConnection(); } public function updatedSourceType(): void { $this->backupInitiated = false; $this->backupPid = null; $this->backupInProgress = false; $this->remoteBackupPath = null; $this->backupFilename = null; $this->backupPath = null; $this->backupSize = 0; $this->downloadProgress = 0; $this->discoveredData = []; $this->statusLog = []; $this->analysisLog = []; $this->isAnalyzing = false; $this->step1Complete = false; $this->localBackupPath = null; if ($this->sourceType === 'local') { $this->loadLocalBackups(); } $this->storeCredentialsInSession(); } public function updatedLocalBackupPath(): void { if (! $this->localBackupPath) { $this->backupFilename = null; $this->backupPath = null; $this->backupSize = 0; $this->isConnected = false; $this->discoveredData = []; $this->statusLog = []; $this->analysisLog = []; $this->storeCredentialsInSession(); return; } $this->selectLocalBackup(); } public function getAgent(): AgentClient { return $this->agent ??= new AgentClient; } protected function getUser(): User { return Auth::user(); } protected function resetConnection(): void { $this->cpanel = null; $this->isConnected = false; $this->connectionInfo = []; } protected function getCpanel(): ?CpanelApiService { if (! $this->hostname || ! $this->cpanelUsername || ! $this->apiToken) { $this->restoreCredentialsFromSession(); } if (! $this->hostname || ! $this->cpanelUsername || ! $this->apiToken) { return null; } return $this->cpanel ??= new CpanelApiService( $this->hostname, $this->cpanelUsername, $this->apiToken, $this->port, $this->useSSL ); } protected function getBackupDestPath(): string { $user = $this->getUser(); return "/home/{$user->username}/cpanel-migration"; } protected function getLocalBackupRoot(): string { $user = $this->getUser(); return "/home/{$user->username}/backups"; } protected function loadLocalBackups(): void { $this->availableBackups = []; $result = $this->getAgent()->send('file.list', [ 'username' => $this->getUser()->username, 'path' => 'backups', ]); if (! ($result['success'] ?? false)) { $this->getAgent()->send('file.mkdir', [ 'username' => $this->getUser()->username, 'path' => 'backups', ]); $result = $this->getAgent()->send('file.list', [ 'username' => $this->getUser()->username, 'path' => 'backups', ]); if (! ($result['success'] ?? false)) { return; } } $items = $result['items'] ?? []; foreach ($items as $item) { if (($item['is_dir'] ?? false) === true) { continue; } $name = (string) ($item['name'] ?? ''); if (! preg_match('/\.(tar\.gz|tgz)$/i', $name)) { continue; } $this->availableBackups[] = $item; } } public function refreshLocalBackups(): void { $this->loadLocalBackups(); Notification::make() ->title(__('Backup list refreshed')) ->success() ->send(); } protected function getLocalBackupOptions(): array { $options = []; foreach ($this->availableBackups as $item) { $path = $item['path'] ?? null; $name = $item['name'] ?? null; if (! $path || ! $name) { continue; } $size = $this->formatBytes((int) ($item['size'] ?? 0)); $options[$path] = "{$name} ({$size})"; } return $options; } public function selectLocalBackup(): void { if (! $this->localBackupPath) { return; } $info = $this->getAgent()->send('file.info', [ 'username' => $this->getUser()->username, 'path' => $this->localBackupPath, ]); if (! ($info['success'] ?? false)) { Notification::make() ->title(__('Backup file not found')) ->body($info['error'] ?? __('Unable to read backup file')) ->danger() ->send(); return; } $details = $info['info'] ?? []; if (! ($details['is_file'] ?? false)) { Notification::make() ->title(__('Invalid backup selection')) ->body(__('Please select a backup file')) ->warning() ->send(); return; } $this->backupFilename = $details['name'] ?? basename($this->localBackupPath); $this->backupSize = (int) ($details['size'] ?? 0); $this->backupPath = "/home/{$this->getUser()->username}/{$this->localBackupPath}"; $this->isConnected = true; $this->statusLog = []; $this->analysisLog = []; $this->discoveredData = []; $this->addStatusLog(__('Selected local backup: :name', ['name' => $this->backupFilename]), 'success'); $this->storeCredentialsInSession(); } protected function storeCredentialsInSession(): void { session()->put('user_cpanel_migration.sourceType', $this->sourceType); session()->put('user_cpanel_migration.localBackupPath', $this->localBackupPath); session()->put('user_cpanel_migration.hostname', $this->hostname); session()->put('user_cpanel_migration.username', $this->cpanelUsername); session()->put('user_cpanel_migration.token', $this->apiToken); session()->put('user_cpanel_migration.port', $this->port); session()->put('user_cpanel_migration.useSSL', $this->useSSL); session()->put('user_cpanel_migration.isConnected', $this->isConnected); session()->put('user_cpanel_migration.connectionInfo', $this->connectionInfo); session()->put('user_cpanel_migration.backupInitiated', $this->backupInitiated); session()->put('user_cpanel_migration.backupPid', $this->backupPid); session()->put('user_cpanel_migration.backupInProgress', $this->backupInProgress); session()->put('user_cpanel_migration.remoteBackupPath', $this->remoteBackupPath); session()->put('user_cpanel_migration.backupFilename', $this->backupFilename); session()->put('user_cpanel_migration.backupPath', $this->backupPath); session()->put('user_cpanel_migration.backupSize', $this->backupSize); session()->put('user_cpanel_migration.downloadProgress', $this->downloadProgress); session()->put('user_cpanel_migration.discoveredData', $this->discoveredData); session()->put('user_cpanel_migration.statusLog', $this->statusLog); session()->put('user_cpanel_migration.analysisLog', $this->analysisLog); session()->put('user_cpanel_migration.isAnalyzing', $this->isAnalyzing); session()->put('user_cpanel_migration.step1Complete', $this->step1Complete); session()->put('user_cpanel_migration.pollCount', $this->pollCount); session()->save(); } protected function restoreCredentialsFromSession(): void { if (! session()->has('user_cpanel_migration.sourceType') && ! session()->has('user_cpanel_migration.hostname')) { return; } $this->sourceType = session('user_cpanel_migration.sourceType', 'remote'); $this->localBackupPath = session('user_cpanel_migration.localBackupPath'); $this->hostname = session('user_cpanel_migration.hostname'); $this->cpanelUsername = session('user_cpanel_migration.username'); $this->apiToken = session('user_cpanel_migration.token'); $this->port = session('user_cpanel_migration.port', 2083); $this->useSSL = session('user_cpanel_migration.useSSL', true); $this->isConnected = session('user_cpanel_migration.isConnected', false); $this->connectionInfo = session('user_cpanel_migration.connectionInfo', []); $this->backupInitiated = session('user_cpanel_migration.backupInitiated', false); $this->backupPid = session('user_cpanel_migration.backupPid'); $this->backupInProgress = session('user_cpanel_migration.backupInProgress', false); $this->remoteBackupPath = session('user_cpanel_migration.remoteBackupPath'); $this->backupFilename = session('user_cpanel_migration.backupFilename'); $this->backupPath = session('user_cpanel_migration.backupPath'); $this->backupSize = session('user_cpanel_migration.backupSize', 0); $this->downloadProgress = session('user_cpanel_migration.downloadProgress', 0); $this->discoveredData = session('user_cpanel_migration.discoveredData', []); $this->statusLog = session('user_cpanel_migration.statusLog', []); $this->analysisLog = session('user_cpanel_migration.analysisLog', []); $this->isAnalyzing = session('user_cpanel_migration.isAnalyzing', false); $this->step1Complete = session('user_cpanel_migration.step1Complete', false); $this->pollCount = session('user_cpanel_migration.pollCount', 0); } protected function clearSessionCredentials(): void { session()->forget([ 'user_cpanel_migration.sourceType', 'user_cpanel_migration.localBackupPath', 'user_cpanel_migration.hostname', 'user_cpanel_migration.username', 'user_cpanel_migration.token', 'user_cpanel_migration.port', 'user_cpanel_migration.useSSL', 'user_cpanel_migration.isConnected', 'user_cpanel_migration.connectionInfo', 'user_cpanel_migration.backupInitiated', 'user_cpanel_migration.backupPid', 'user_cpanel_migration.backupInProgress', 'user_cpanel_migration.remoteBackupPath', 'user_cpanel_migration.backupFilename', 'user_cpanel_migration.backupPath', 'user_cpanel_migration.backupSize', 'user_cpanel_migration.downloadProgress', 'user_cpanel_migration.discoveredData', 'user_cpanel_migration.statusLog', 'user_cpanel_migration.analysisLog', 'user_cpanel_migration.isAnalyzing', 'user_cpanel_migration.step1Complete', 'user_cpanel_migration.pollCount', 'user_cpanel_restore_job_id', 'user_cpanel_restore_log_path', 'user_cpanel_restore_processing', ]); } protected function restoreMigrationStateFromSession(): void { $this->restoreCredentialsFromSession(); if ($this->sourceType === 'local') { $this->loadLocalBackups(); } $this->restoreJobId = session('user_cpanel_restore_job_id'); $this->restoreLogPath = session('user_cpanel_restore_log_path'); $this->isProcessing = (bool) session('user_cpanel_restore_processing', false); if ($this->restoreJobId && $this->restoreLogPath) { $this->migrationLog = $this->readMigrationLog($this->restoreLogPath); $status = Cache::get($this->getRestoreCacheKey()); if (is_array($status)) { $this->restoreStatus = $status['status'] ?? $this->restoreStatus; } } } protected function getRestoreCacheKey(): string { return $this->restoreJobId ? 'cpanel_restore_status_'.$this->restoreJobId : ''; } protected function getForms(): array { return ['migrationForm']; } public function migrationForm(Schema $schema): Schema { return $schema->schema([ Wizard::make([ $this->getConnectStep(), $this->getBackupStep(), $this->getReviewStep(), $this->getRestoreStep(), ]) ->nextAction(fn (Action $action) => $action->disabled(fn () => $this->isNextStepDisabled())) ->persistStepInQueryString('cpanel-step'), ]); } protected function isNextStepDisabled(): bool { if (! $this->step1Complete) { return ! $this->isConnected; } if (! $this->backupPath || empty($this->discoveredData)) { return true; } return false; } protected function getConnectStep(): Step { return Step::make(__('Connect')) ->id('connect') ->icon('heroicon-o-link') ->description(__('Enter cPanel credentials')) ->schema([ Section::make(__('Migration Source')) ->icon('heroicon-o-arrow-path') ->schema([ Radio::make('sourceType') ->label(__('Source Type')) ->options([ 'remote' => __('Remote cPanel Server'), 'local' => __('Local Backup File'), ]) ->default('remote') ->inline() ->live(), ]), Section::make(__('cPanel Credentials')) ->description(__('Enter the cPanel server connection details')) ->icon('heroicon-o-link') ->visible(fn () => $this->sourceType === 'remote') ->schema([ Grid::make(['default' => 1, 'sm' => 2])->schema([ TextInput::make('hostname') ->label(__('cPanel Hostname')) ->placeholder(__('cpanel.example.com')) ->required() ->helperText(__('Your cPanel server hostname or IP address')), TextInput::make('port') ->label(__('Port')) ->numeric() ->default(2083) ->required() ->helperText(__('Usually 2083 for SSL or 2082 without')), ]), Grid::make(['default' => 1, 'sm' => 2])->schema([ TextInput::make('cpanelUsername') ->label(__('cPanel Username')) ->required() ->helperText(__('Your cPanel account username')), TextInput::make('apiToken') ->label(__('API Token')) ->password() ->required() ->revealable() ->helperText(__('Generate from cPanel → Security → Manage API Tokens')), ]), Checkbox::make('useSSL') ->label(__('Use SSL (HTTPS)')) ->default(true) ->helperText(__('Recommended. Disable only if your cPanel does not support SSL.')), FormActions::make([ Action::make('testConnection') ->label(__('Test Connection')) ->icon('heroicon-o-signal') ->color($this->isConnected ? 'success' : 'primary') ->action('testConnection'), ])->alignEnd(), Section::make(__('Connection Successful')) ->icon('heroicon-o-check-circle') ->iconColor('success') ->visible(fn () => $this->isConnected) ->schema([ Text::make(__('You can proceed to the next step.')), Grid::make(['default' => 2, 'sm' => 4])->schema([ Section::make((string) ($this->connectionInfo['domains'] ?? 0)) ->description(__('Domains')) ->icon('heroicon-o-globe-alt') ->iconColor('primary') ->compact(), Section::make((string) ($this->connectionInfo['databases'] ?? 0)) ->description(__('Databases')) ->icon('heroicon-o-circle-stack') ->iconColor('warning') ->compact(), Section::make((string) ($this->connectionInfo['emails'] ?? 0)) ->description(__('Mailboxes')) ->icon('heroicon-o-envelope') ->iconColor('success') ->compact(), Section::make((string) ($this->connectionInfo['ssl'] ?? 0)) ->description(__('SSL Certs')) ->icon('heroicon-o-lock-closed') ->iconColor('info') ->compact(), ]), ]), ]), Section::make(__('Local Backup')) ->description(__('Select a backup file from your home backups folder')) ->icon('heroicon-o-folder-open') ->visible(fn () => $this->sourceType === 'local') ->headerActions([ Action::make('refreshLocalBackups') ->label(__('Refresh')) ->icon('heroicon-o-arrow-path') ->color('gray') ->action('refreshLocalBackups'), ]) ->schema([ Text::make(__('Folder: :path', ['path' => $this->getLocalBackupRoot()])), Select::make('localBackupPath') ->label(__('Backup File')) ->options($this->getLocalBackupOptions()) ->searchable() ->placeholder(__('Select a backup file')) ->required(), Text::make(__('No backups found in the folder. Upload a cPanel backup file to continue.')) ->color('gray') ->visible(fn () => empty($this->availableBackups)), ]), ]) ->afterValidation(function () { if ($this->sourceType === 'local') { if (! $this->backupPath) { Notification::make() ->title(__('Backup required')) ->body(__('Please select a local backup file before proceeding')) ->danger() ->send(); throw new Exception(__('Please select a local backup file')); } } elseif (! $this->isConnected) { Notification::make() ->title(__('Connection required')) ->body(__('Please test the connection before proceeding')) ->danger() ->send(); throw new Exception(__('Please test the connection first')); } $this->step1Complete = true; $this->storeCredentialsInSession(); }); } protected function getBackupStep(): Step { if ($this->sourceType === 'local') { return Step::make(__('Backup')) ->id('backup') ->icon('heroicon-o-folder-open') ->description(__('Analyze local backup')) ->schema([ Section::make(__('Local Backup')) ->description(__('Analyze the selected backup file before restoring')) ->icon('heroicon-o-folder-open') ->iconColor('primary') ->headerActions([ Action::make('analyzeBackup') ->label(__('Analyze Backup')) ->icon('heroicon-o-magnifying-glass') ->color('primary') ->disabled(fn () => $this->isAnalyzing || ! $this->backupPath || ! empty($this->discoveredData)) ->action('analyzeBackup'), ]) ->schema([ Text::make(__('File: :name', [ 'name' => $this->backupFilename ?? '-', ])), Text::make(__('Size: :size', [ 'size' => $this->backupSize ? $this->formatBytes($this->backupSize) : '-', ])), ]), Section::make(__('Analysis Progress')) ->icon($this->getAnalysisStatusIcon()) ->iconColor($this->getAnalysisStatusColor()) ->schema($this->getAnalysisStatusSchema()) ->extraAttributes($this->isAnalyzing ? ['wire:poll.1s' => 'pollAnalysisStatus'] : []), ]) ->afterValidation(function () { if (empty($this->discoveredData)) { Notification::make() ->title(__('Analysis required')) ->body(__('Please analyze the backup file before proceeding')) ->danger() ->send(); throw new Exception(__('Please analyze the backup file')); } }); } return Step::make(__('Backup')) ->id('backup') ->icon('heroicon-o-cloud-arrow-down') ->description(__('Create and download backup')) ->schema([ Section::make(__('Backup Transfer')) ->description(__('Create a backup on cPanel and download it to this server')) ->icon('heroicon-o-server') ->schema([ Text::make(__('Local Path: :path', ['path' => $this->getBackupDestPath()])), Text::make(__('Note: Large accounts may take several minutes.'))->color('warning'), ]), FormActions::make([ Action::make('startBackup') ->label(__('Start Backup')) ->icon('heroicon-o-cloud-arrow-up') ->color('success') ->visible(fn () => ! $this->backupInitiated) ->requiresConfirmation() ->modalHeading(__('Start Backup')) ->modalDescription(__('This will create a full backup on your cPanel account.')) ->action('startBackup'), Action::make('checkBackup') ->label(__('Check Status')) ->icon('heroicon-o-arrow-path') ->color('info') ->visible(fn () => $this->backupInitiated && ! $this->remoteBackupPath && ! $this->backupFilename) ->action('checkBackupStatus'), Action::make('downloadBackup') ->label(__('Download Backup')) ->icon('heroicon-o-cloud-arrow-down') ->color('success') ->visible(fn () => $this->remoteBackupPath && ! $this->backupFilename) ->action('downloadBackup'), Action::make('analyzeBackup') ->label(__('Analyze Backup')) ->icon('heroicon-o-magnifying-glass') ->color('primary') ->visible(fn () => $this->backupFilename && empty($this->discoveredData)) ->action('analyzeBackup'), ])->alignEnd(), Section::make(__('Transfer Status')) ->icon($this->backupPath ? 'heroicon-o-check-circle' : 'heroicon-o-clock') ->iconColor($this->backupPath ? 'success' : ($this->backupInitiated ? 'warning' : 'gray')) ->schema($this->getStatusLogSchema()) ->extraAttributes($this->backupInitiated && ! $this->backupPath ? ['wire:poll.5s' => 'pollBackupStatus'] : []), Section::make(__('Analysis Progress')) ->icon($this->getAnalysisStatusIcon()) ->iconColor($this->getAnalysisStatusColor()) ->visible(fn () => $this->backupFilename !== null || $this->isAnalyzing || ! empty($this->discoveredData)) ->schema($this->getAnalysisStatusSchema()), ]) ->afterValidation(function () { if (! $this->backupPath || empty($this->discoveredData)) { Notification::make() ->title(__('Backup required')) ->body(__('Please complete the backup and analysis before proceeding')) ->danger() ->send(); throw new Exception(__('Please complete the backup first')); } $this->storeCredentialsInSession(); }); } protected function getReviewStep(): Step { return Step::make(__('Review')) ->id('review') ->icon('heroicon-o-clipboard-document-check') ->description(__('Configure restore options')) ->schema($this->getReviewStepSchema()); } protected function getRestoreStep(): Step { return Step::make(__('Restore')) ->id('restore') ->icon('heroicon-o-play') ->description(__('Migration progress')) ->schema([ FormActions::make([ Action::make('startRestore') ->label(__('Start Restore')) ->icon('heroicon-o-play') ->color('success') ->visible(fn () => ! $this->isProcessing && empty($this->migrationLog)) ->requiresConfirmation() ->modalHeading(__('Start Restore')) ->modalDescription(__('This will restore the selected items. Existing data may be overwritten.')) ->action('startRestore'), Action::make('newMigration') ->label(__('New Migration')) ->icon('heroicon-o-plus') ->color('primary') ->visible(fn () => ! $this->isProcessing && ! empty($this->migrationLog)) ->action('resetMigration'), ])->alignEnd(), Section::make(__('Migration Progress')) ->icon($this->isProcessing ? 'heroicon-o-arrow-path' : ($this->migrationLog ? 'heroicon-o-check-circle' : 'heroicon-o-clock')) ->iconColor($this->isProcessing ? 'warning' : ($this->migrationLog ? 'success' : 'gray')) ->schema($this->getMigrationLogSchema()) ->extraAttributes($this->isProcessing ? ['wire:poll.1s' => 'pollMigrationLog'] : []), ]); } protected function getReviewStepSchema(): array { if (empty($this->discoveredData)) { return [ Section::make(__('Waiting for Backup')) ->icon('heroicon-o-clock') ->iconColor('warning') ->schema([ Text::make(__('Please complete the backup in the previous step.')), ]), ]; } $domainCount = count($this->discoveredData['domains'] ?? []); $dbCount = count($this->discoveredData['databases'] ?? []); $mailCount = count($this->discoveredData['mailboxes'] ?? []); $forwarderCount = count($this->discoveredData['forwarders'] ?? []); $sslCount = count($this->discoveredData['ssl_certificates'] ?? []); return [ Section::make(__('Migration Summary')) ->icon('heroicon-o-clipboard-document-list') ->iconColor('primary') ->schema([ Grid::make(['default' => 2, 'sm' => 5])->schema([ Section::make((string) $domainCount) ->description(__('Domains')) ->icon('heroicon-o-globe-alt') ->iconColor('primary') ->compact(), Section::make((string) $dbCount) ->description(__('Databases')) ->icon('heroicon-o-circle-stack') ->iconColor('warning') ->compact(), Section::make((string) $mailCount) ->description(__('Mailboxes')) ->icon('heroicon-o-envelope') ->iconColor('success') ->compact(), Section::make((string) $forwarderCount) ->description(__('Forwarders')) ->icon('heroicon-o-arrow-uturn-right') ->iconColor('gray') ->compact(), Section::make((string) $sslCount) ->description(__('SSL Certs')) ->icon('heroicon-o-lock-closed') ->iconColor('info') ->compact(), ]), ]), Section::make(__('What to Restore')) ->description(__('Select which parts of the backup to restore')) ->icon('heroicon-o-check-circle') ->schema([ Grid::make(['default' => 1, 'sm' => 2])->schema([ Checkbox::make('restoreFiles') ->label(__('Website Files')) ->helperText(__('Restore all website files to your domains folder')) ->default(true), Checkbox::make('restoreDatabases') ->label(__('MySQL Databases')) ->helperText(__('Restore databases with all data')) ->default(true), Checkbox::make('restoreEmails') ->label(__('Email Mailboxes')) ->helperText(__('Restore email accounts and messages')) ->default(true), Checkbox::make('restoreSsl') ->label(__('SSL Certificates')) ->helperText(__('Restore SSL certificates for domains')) ->default(true), ]), ]), Section::make(__('Discovered Data')) ->icon('heroicon-o-magnifying-glass') ->schema([ Tabs::make('DataTabs') ->tabs([ Tabs\Tab::make(__('Domains')) ->icon('heroicon-o-globe-alt') ->badge((string) $domainCount) ->schema($this->getDomainsTabContent()), Tabs\Tab::make(__('Databases')) ->icon('heroicon-o-circle-stack') ->badge((string) $dbCount) ->schema($this->getDatabasesTabContent()), Tabs\Tab::make(__('Mailboxes')) ->icon('heroicon-o-envelope') ->badge((string) $mailCount) ->schema($this->getMailboxesTabContent()), Tabs\Tab::make(__('Forwarders')) ->icon('heroicon-o-arrow-uturn-right') ->badge((string) $forwarderCount) ->schema($this->getForwardersTabContent()), Tabs\Tab::make(__('SSL')) ->icon('heroicon-o-lock-closed') ->badge((string) $sslCount) ->schema($this->getSslTabContent()), ]), ]), ]; } protected function addStatusLog(string $message, string $status = 'info'): void { $this->statusLog[] = [ 'message' => $message, 'status' => $status, 'time' => now()->format('H:i:s'), ]; } protected function getStatusLogSchema(): array { if (empty($this->statusLog)) { return [ Text::make(__('Click "Start Backup" to begin.'))->color('gray'), ]; } $items = []; foreach ($this->statusLog as $entry) { $color = match ($entry['status']) { 'success' => 'success', 'error' => 'danger', 'pending' => 'warning', default => 'gray', }; $prefix = match ($entry['status']) { 'success' => '✓ ', 'error' => '✗ ', 'pending' => '○ ', default => '• ', }; $items[] = Text::make($prefix.$entry['message']) ->color($color); } if ($this->backupPath) { $items[] = Section::make(__('Backup Complete')) ->icon('heroicon-o-check-circle') ->iconColor('success') ->schema([ Text::make(__('File: :name', ['name' => $this->backupFilename ?? basename($this->backupPath)])), Text::make(__('Size: :size', ['size' => $this->formatBytes($this->backupSize)])), ]) ->compact(); } return $items; } protected function addAnalysisLog(string $message, string $status = 'info'): void { $this->analysisLog[] = [ 'message' => $message, 'status' => $status, 'time' => now()->format('H:i:s'), ]; } public function pollAnalysisStatus(): void { // UI polling refresh during backup analysis. } protected function getAnalysisStatusIcon(): string { if (! empty($this->discoveredData)) { return 'heroicon-o-check-circle'; } if ($this->isAnalyzing) { return 'heroicon-o-arrow-path'; } return 'heroicon-o-clock'; } protected function getAnalysisStatusColor(): string { if (! empty($this->discoveredData)) { return 'success'; } if ($this->isAnalyzing) { return 'warning'; } return 'gray'; } protected function getAnalysisStatusSchema(): array { $items = []; if (! empty($this->analysisLog)) { foreach ($this->analysisLog as $entry) { $prefix = match ($entry['status']) { 'success' => '✓ ', 'error' => '✗ ', 'pending' => '○ ', default => '• ', }; $color = match ($entry['status']) { 'success' => 'success', 'error' => 'danger', 'pending' => 'warning', default => 'gray', }; $items[] = Text::make($prefix.$entry['message'])->color($color); } } if (empty($this->analysisLog) && ! $this->isAnalyzing && empty($this->discoveredData)) { return [ Text::make(__('Click "Analyze Backup" to discover the backup contents.'))->color('gray'), ]; } if (! empty($this->discoveredData)) { $domainCount = count($this->discoveredData['domains'] ?? []); $dbCount = count($this->discoveredData['databases'] ?? []); $mailCount = count($this->discoveredData['mailboxes'] ?? []); $forwarderCount = count($this->discoveredData['forwarders'] ?? []); $sslCount = count($this->discoveredData['ssl_certificates'] ?? []); $items[] = Grid::make(['default' => 2, 'sm' => 5])->schema([ Section::make((string) $domainCount) ->description(__('Domains')) ->icon('heroicon-o-globe-alt') ->iconColor('primary') ->compact(), Section::make((string) $dbCount) ->description(__('Databases')) ->icon('heroicon-o-circle-stack') ->iconColor('warning') ->compact(), Section::make((string) $mailCount) ->description(__('Mailboxes')) ->icon('heroicon-o-envelope') ->iconColor('success') ->compact(), Section::make((string) $forwarderCount) ->description(__('Forwarders')) ->icon('heroicon-o-arrow-uturn-right') ->iconColor('gray') ->compact(), Section::make((string) $sslCount) ->description(__('SSL Certs')) ->icon('heroicon-o-lock-closed') ->iconColor('info') ->compact(), ]); } return $items; } protected function getDomainsTabContent(): array { $domains = $this->discoveredData['domains'] ?? []; if (empty($domains)) { return [Text::make(__('No domains found in backup.'))]; } $items = []; foreach ($domains as $domain) { $typePrefix = match ($domain['type'] ?? 'addon') { 'main' => '★ ', 'addon' => '● ', 'sub' => '◦ ', default => '• ', }; $typeColor = match ($domain['type'] ?? 'addon') { 'main' => 'success', 'addon' => 'primary', 'sub' => 'warning', default => 'gray', }; $items[] = Text::make($typePrefix.$domain['name'].' ('.$domain['type'].')') ->color($typeColor); } return $items; } protected function getDatabasesTabContent(): array { $databases = $this->discoveredData['databases'] ?? []; if (empty($databases)) { return [Text::make(__('No databases found in backup.'))]; } $items = []; foreach ($databases as $db) { $items[] = Text::make('• '.$db['name']) ->color('primary'); } return $items; } protected function getMailboxesTabContent(): array { $mailboxes = $this->discoveredData['mailboxes'] ?? []; if (empty($mailboxes)) { return [Text::make(__('No mailboxes found in backup.'))]; } $items = []; foreach ($mailboxes as $mailbox) { $items[] = Text::make('✉ '.$mailbox['email']) ->color('success'); } return $items; } protected function getForwardersTabContent(): array { $forwarders = $this->discoveredData['forwarders'] ?? []; if (empty($forwarders)) { return [Text::make(__('No forwarders found in backup.'))]; } $items = []; foreach ($forwarders as $forwarder) { $email = $forwarder['email'] ?? ''; $destinations = $forwarder['destinations'] ?? ''; $destPreview = is_array($destinations) ? implode(', ', $destinations) : $destinations; $destPreview = strlen($destPreview) > 40 ? substr($destPreview, 0, 37).'...' : $destPreview; $items[] = Text::make('↪ '.$email.' → '.$destPreview) ->color('gray'); } return $items; } protected function getSslTabContent(): array { $sslCerts = $this->discoveredData['ssl_certificates'] ?? []; if (empty($sslCerts)) { return [Text::make(__('No SSL certificates found in backup.'))]; } $items = []; foreach ($sslCerts as $cert) { $hasComplete = ($cert['has_key'] ?? false) && ($cert['has_cert'] ?? false); $prefix = $hasComplete ? '🔒 ' : '⚠ '; $items[] = Text::make($prefix.$cert['domain']) ->color($hasComplete ? 'success' : 'warning'); } return $items; } protected function getMigrationLogSchema(): array { if (empty($this->migrationLog)) { return [ Text::make(__('Click "Start Restore" to begin the migration.'))->color('gray'), ]; } $items = []; foreach ($this->migrationLog as $entry) { $status = $entry['status'] ?? 'info'; $message = $entry['message'] ?? ''; $color = match ($status) { 'success' => 'success', 'error' => 'danger', 'warning' => 'warning', 'pending' => 'warning', default => 'gray', }; $prefix = match ($status) { 'success' => '✓ ', 'error' => '✗ ', 'warning' => '• ', 'pending' => '○ ', default => '• ', }; $items[] = Text::make($prefix.$message)->color($color); } return $items; } public function testConnection(): void { if (empty($this->hostname) || empty($this->cpanelUsername) || empty($this->apiToken)) { Notification::make() ->title(__('Missing credentials')) ->body(__('Please fill in all required fields')) ->danger() ->send(); return; } try { $cpanel = $this->getCpanel(); $result = $cpanel->testConnection(); if ($result['success']) { $this->isConnected = true; $summary = $cpanel->getMigrationSummary(); $this->connectionInfo = [ 'domains' => $this->countDomains($summary['domains'] ?? []), 'emails' => count($summary['email_accounts'] ?? []), 'databases' => count($summary['databases'] ?? []), 'ssl' => count($summary['ssl_certificates'] ?? []), ]; Notification::make() ->title(__('Connection successful')) ->body(__('Found :domains domains, :emails email accounts, :dbs databases', [ 'domains' => $this->connectionInfo['domains'], 'emails' => $this->connectionInfo['emails'], 'dbs' => $this->connectionInfo['databases'], ])) ->success() ->send(); $this->storeCredentialsInSession(); } else { throw new Exception($result['message'] ?? __('Connection failed')); } } catch (Exception $e) { $this->isConnected = false; Log::error('cPanel connection failed', ['error' => $e->getMessage()]); Notification::make() ->title(__('Connection failed')) ->body($e->getMessage()) ->danger() ->send(); } } protected function countDomains(array $domains): int { $count = 0; if (! empty($domains['main'])) { $count++; } $count += count($domains['addon'] ?? []); $count += count($domains['sub'] ?? []); return $count; } public function startBackup(): void { $user = $this->getUser(); $destPath = $this->getBackupDestPath(); try { $this->statusLog = []; $this->analysisLog = []; $this->discoveredData = []; $this->remoteBackupPath = null; $this->backupFilename = null; $this->backupPath = null; $this->backupSize = 0; $this->downloadProgress = 0; $this->addStatusLog(__('Starting backup on cPanel...'), 'pending'); $this->getAgent()->send('file.mkdir', [ 'path' => $destPath, 'username' => $user->username, ]); $cpanel = $this->getCpanel(); $result = $cpanel->createBackup(); if ($result['success']) { $this->backupInitiated = true; $this->backupInProgress = true; $this->backupPid = $result['pid'] ?? null; $this->pollCount = 0; $this->addStatusLog(__('Backup initiated on cPanel'), 'success'); $this->addStatusLog(__('Waiting for backup to complete on cPanel...'), 'pending'); Notification::make() ->title(__('Backup started')) ->body(__('cPanel is creating a full backup. This may take several minutes.')) ->success() ->send(); $this->storeCredentialsInSession(); } else { throw new Exception($result['message'] ?? __('Failed to start backup')); } } catch (Exception $e) { Log::error('cPanel backup initiation failed', ['error' => $e->getMessage()]); $this->addStatusLog(__('Backup failed: :message', ['message' => $e->getMessage()]), 'error'); Notification::make() ->title(__('Backup failed')) ->body($e->getMessage()) ->danger() ->send(); } } public function pollBackupStatus(): void { $this->checkBackupStatus(true); } public function checkBackupStatus(bool $quiet = false): void { $this->pollCount++; try { $cpanel = $this->getCpanel(); $result = $cpanel->getBackupStatus(); if ($result['success'] ?? false) { $backups = $result['backups'] ?? []; if (! empty($backups)) { $latestBackup = $backups[0]; $this->remoteBackupPath = $latestBackup['path']; $this->backupInProgress = false; $this->addStatusLog(__('Backup ready on cPanel: :name', ['name' => $latestBackup['name']]), 'success'); if (! $quiet) { Notification::make() ->title(__('Backup ready')) ->body(__('Backup file found: :name. Click "Download Backup" to continue.', [ 'name' => $latestBackup['name'], ])) ->success() ->send(); } $this->storeCredentialsInSession(); return; } if ($result['in_progress'] ?? false) { $this->backupInProgress = true; $this->addStatusLog(__('Backup still in progress on cPanel...'), 'pending'); if (! $quiet) { Notification::make() ->title(__('Backup in progress')) ->body(__('cPanel is still creating the backup. Please wait and check again.')) ->info() ->send(); } $this->storeCredentialsInSession(); return; } } $this->addStatusLog(__('Backup not ready yet'), 'pending'); if (! $quiet) { Notification::make() ->title(__('Backup not ready')) ->body(__('No backup files found yet. Please wait and check again.')) ->info() ->send(); } $this->storeCredentialsInSession(); } catch (Exception $e) { $this->addStatusLog(__('Error checking backup: :message', ['message' => $e->getMessage()]), 'error'); if (! $quiet) { Notification::make() ->title(__('Error checking backup')) ->body($e->getMessage()) ->danger() ->send(); } } } public function downloadBackup(): void { if (! $this->remoteBackupPath) { Notification::make() ->title(__('No backup to download')) ->body(__('Please wait for the backup to be created first.')) ->warning() ->send(); return; } $user = $this->getUser(); $destPath = $this->getBackupDestPath(); $filename = basename($this->remoteBackupPath); $localPath = $destPath.'/'.$filename; try { $this->addStatusLog(__('Downloading backup...'), 'pending'); Notification::make() ->title(__('Download started')) ->body(__('Downloading backup from cPanel. This may take several minutes.')) ->info() ->send(); $cpanel = $this->getCpanel(); $result = $cpanel->downloadFileToPath( $this->remoteBackupPath, $localPath, function ($downloaded, $total) { if ($total > 0) { $this->downloadProgress = (int) (($downloaded / $total) * 100); } } ); if ($result['success'] ?? false) { $this->backupFilename = $filename; $this->backupPath = $localPath; $this->backupSize = (int) ($result['size'] ?? 0); $this->downloadProgress = 100; $this->backupInProgress = false; $this->getAgent()->send('file.chown', [ 'path' => $localPath, 'username' => $user->username, ]); $this->addStatusLog(__('Backup downloaded: :name (:size)', [ 'name' => $filename, 'size' => $this->formatBytes($this->backupSize), ]), 'success'); Notification::make() ->title(__('Download completed')) ->body(__('Backup file :name (:size) is ready for analysis', [ 'name' => $filename, 'size' => $this->formatBytes($this->backupSize), ])) ->success() ->send(); $this->storeCredentialsInSession(); } else { throw new Exception($result['message'] ?? __('Failed to download backup')); } } catch (Exception $e) { Log::error('cPanel backup download failed', ['error' => $e->getMessage()]); $this->addStatusLog(__('Download failed: :message', ['message' => $e->getMessage()]), 'error'); Notification::make() ->title(__('Download failed')) ->body($e->getMessage()) ->danger() ->send(); } } public function analyzeBackup(): void { if (! $this->backupPath) { return; } try { $this->isAnalyzing = true; $this->analysisLog = []; $this->addAnalysisLog(__('Analyzing backup contents...'), 'pending'); $result = $this->getAgent()->send('cpanel.analyze_backup', [ 'backup_path' => $this->backupPath, ]); if ($result['success'] ?? false) { $this->discoveredData = $result['data'] ?? []; $this->addAnalysisLog(__('Backup analyzed successfully'), 'success'); $this->addAnalysisLog(__('Found :domains domains, :dbs databases, :mailboxes mailboxes', [ 'domains' => count($this->discoveredData['domains'] ?? []), 'dbs' => count($this->discoveredData['databases'] ?? []), 'mailboxes' => count($this->discoveredData['mailboxes'] ?? []), ]), 'info'); Notification::make() ->title(__('Backup analyzed')) ->body(__('Found :domains domains, :dbs databases, :mailboxes mailboxes', [ 'domains' => count($this->discoveredData['domains'] ?? []), 'dbs' => count($this->discoveredData['databases'] ?? []), 'mailboxes' => count($this->discoveredData['mailboxes'] ?? []), ])) ->success() ->send(); $this->storeCredentialsInSession(); } else { throw new Exception($result['error'] ?? __('Failed to analyze backup')); } } catch (Exception $e) { Log::error('Backup analysis failed', ['error' => $e->getMessage()]); $this->addAnalysisLog(__('Analysis failed: :message', ['message' => $e->getMessage()]), 'error'); Notification::make() ->title(__('Analysis failed')) ->body($e->getMessage()) ->danger() ->send(); } finally { $this->isAnalyzing = false; } } public function startRestore(): void { if (! $this->backupPath) { return; } $this->isProcessing = true; $this->migrationLog = []; $this->restoreStatus = 'queued'; $user = $this->getUser(); $this->enqueueRestore($user); } protected function enqueueRestore(User $user): void { $this->restoreJobId = (string) Str::uuid(); $logDir = storage_path('app/migrations/cpanel'); File::ensureDirectoryExists($logDir); $this->restoreLogPath = $logDir.'/'.$this->restoreJobId.'.log'; File::put($this->restoreLogPath, ''); @chmod($this->restoreLogPath, 0644); $this->appendMigrationLog(__('Restore queued for user: :user', ['user' => $user->username]), 'pending'); Cache::put($this->getRestoreCacheKey(), ['status' => 'queued'], now()->addHours(2)); session()->put('user_cpanel_restore_job_id', $this->restoreJobId); session()->put('user_cpanel_restore_log_path', $this->restoreLogPath); session()->put('user_cpanel_restore_processing', true); session()->save(); RunCpanelRestore::dispatch( jobId: $this->restoreJobId, logPath: $this->restoreLogPath, backupPath: $this->backupPath, username: $user->username, restoreFiles: $this->restoreFiles, restoreDatabases: $this->restoreDatabases, restoreEmails: $this->restoreEmails, restoreSsl: $this->restoreSsl, discoveredData: ! empty($this->discoveredData) ? $this->discoveredData : null, ); } public function pollMigrationLog(): void { if (! $this->restoreJobId || ! $this->restoreLogPath) { return; } $this->migrationLog = $this->readMigrationLog($this->restoreLogPath); $status = Cache::get($this->getRestoreCacheKey()); if (is_array($status)) { $this->restoreStatus = $status['status'] ?? $this->restoreStatus; } if (in_array($this->restoreStatus, ['completed', 'failed'], true)) { $this->isProcessing = false; session()->forget(['user_cpanel_restore_job_id', 'user_cpanel_restore_log_path', 'user_cpanel_restore_processing']); session()->save(); } } protected function readMigrationLog(string $path): array { if (! file_exists($path)) { return []; } $entries = []; $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { $decoded = json_decode($line, true); if (is_array($decoded) && isset($decoded['message'], $decoded['status'])) { $entries[] = [ 'message' => $decoded['message'], 'status' => $decoded['status'], 'time' => $decoded['time'] ?? now()->format('H:i:s'), ]; } } return $entries; } protected function appendMigrationLog(string $message, string $status): void { $entry = [ 'message' => $message, 'status' => $status, 'time' => now()->format('H:i:s'), ]; $this->migrationLog[] = $entry; if ($this->restoreLogPath) { file_put_contents( $this->restoreLogPath, json_encode($entry).PHP_EOL, FILE_APPEND | LOCK_EX ); @chmod($this->restoreLogPath, 0644); } } public function resetMigration(): void { $this->hostname = null; $this->cpanelUsername = null; $this->apiToken = null; $this->port = 2083; $this->useSSL = true; $this->sourceType = 'remote'; $this->localBackupPath = null; $this->availableBackups = []; $this->isConnected = false; $this->backupInitiated = false; $this->backupPid = null; $this->backupInProgress = false; $this->remoteBackupPath = null; $this->backupFilename = null; $this->backupPath = null; $this->backupSize = 0; $this->downloadProgress = 0; $this->discoveredData = []; $this->restoreFiles = true; $this->restoreDatabases = true; $this->restoreEmails = true; $this->restoreSsl = true; $this->isProcessing = false; $this->migrationLog = []; $this->pollCount = 0; $this->cpanel = null; $this->connectionInfo = []; $this->statusLog = []; $this->analysisLog = []; $this->isAnalyzing = false; $this->step1Complete = false; $this->restoreJobId = null; $this->restoreLogPath = null; $this->restoreStatus = null; $this->wizardStep = null; $this->clearSessionCredentials(); $this->redirect(static::getUrl()); } protected function formatBytes(int $bytes, int $precision = 2): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= pow(1024, $pow); return round($bytes, $precision).' '.$units[$pow]; } }