diff --git a/app/Console/Commands/Jabali/ImportProcessCommand.php b/app/Console/Commands/Jabali/ImportProcessCommand.php index 6fc3e0f..7363b04 100644 --- a/app/Console/Commands/Jabali/ImportProcessCommand.php +++ b/app/Console/Commands/Jabali/ImportProcessCommand.php @@ -248,8 +248,8 @@ class ImportProcessCommand extends Command return; } - $backupPath = Storage::disk('local')->path($import->backup_path); - if (!file_exists($backupPath)) { + $backupPath = $this->resolveBackupFullPath($import); + if (! $backupPath) { $account->addLog("Warning: Backup file not found"); return; } @@ -323,8 +323,8 @@ class ImportProcessCommand extends Command return; } - $backupPath = Storage::disk('local')->path($import->backup_path); - if (!file_exists($backupPath)) { + $backupPath = $this->resolveBackupFullPath($import); + if (! $backupPath) { return; } @@ -386,4 +386,28 @@ class ImportProcessCommand extends Command $account->addLog("Note: Email accounts must be recreated manually"); } + + private function resolveBackupFullPath(ServerImport $import): ?string + { + $path = trim((string) ($import->backup_path ?? '')); + if ($path === '') { + return null; + } + + if (str_starts_with($path, '/') && file_exists($path)) { + return $path; + } + + $localCandidate = Storage::disk('local')->path($path); + if (file_exists($localCandidate)) { + return $localCandidate; + } + + $backupCandidate = Storage::disk('backups')->path($path); + if (file_exists($backupCandidate)) { + return $backupCandidate; + } + + return file_exists($path) ? $path : null; + } } diff --git a/app/Filament/Admin/Pages/DirectAdminMigration.php b/app/Filament/Admin/Pages/DirectAdminMigration.php index 268ffb5..ae846bc 100644 --- a/app/Filament/Admin/Pages/DirectAdminMigration.php +++ b/app/Filament/Admin/Pages/DirectAdminMigration.php @@ -186,9 +186,9 @@ class DirectAdminMigration extends Page implements HasActions, HasForms FileUpload::make('backupPath') ->label(__('DirectAdmin Backup Archive')) - ->helperText(__('Upload a .tar.gz DirectAdmin backup file.')) - ->disk('local') - ->directory('imports/directadmin') + ->helperText(__('Upload a DirectAdmin backup file to the server backups folder.')) + ->disk('backups') + ->directory('directadmin-migrations') ->preserveFilenames() ->acceptedFileTypes([ 'application/gzip', @@ -198,6 +198,8 @@ class DirectAdminMigration extends Page implements HasActions, HasForms ]) ->required() ->visible(fn (Get $get): bool => $get('importMethod') === 'backup_file'), + Text::make(__('Tip: Upload backups to /var/backups/jabali/directadmin-migrations/'))->color('gray') + ->visible(fn (Get $get): bool => $get('importMethod') === 'backup_file'), FormActions::make([ Action::make('discoverAccounts') @@ -386,7 +388,10 @@ class DirectAdminMigration extends Page implements HasActions, HasForms throw new Exception(__('Please upload a DirectAdmin backup archive.')); } - $backupFullPath = Storage::disk('local')->path($import->backup_path); + $backupFullPath = $this->resolveBackupFullPath($import->backup_path); + if (! $backupFullPath) { + throw new Exception(__('Backup file not found: :path', ['path' => $import->backup_path])); + } } else { $remotePassword = $this->remotePassword; @@ -486,6 +491,30 @@ class DirectAdminMigration extends Page implements HasActions, HasForms } } + protected function resolveBackupFullPath(?string $path): ?string + { + $path = trim((string) ($path ?? '')); + if ($path === '') { + return null; + } + + if (str_starts_with($path, '/') && file_exists($path)) { + return $path; + } + + $localCandidate = Storage::disk('local')->path($path); + if (file_exists($localCandidate)) { + return $localCandidate; + } + + $backupCandidate = Storage::disk('backups')->path($path); + if (file_exists($backupCandidate)) { + return $backupCandidate; + } + + return file_exists($path) ? $path : null; + } + public function selectAllAccounts(): void { $import = $this->getImport(); diff --git a/app/Filament/Jabali/Pages/DirectAdminMigration.php b/app/Filament/Jabali/Pages/DirectAdminMigration.php index 86fc761..863198c 100644 --- a/app/Filament/Jabali/Pages/DirectAdminMigration.php +++ b/app/Filament/Jabali/Pages/DirectAdminMigration.php @@ -13,8 +13,8 @@ use Filament\Actions\Action; use Filament\Actions\Concerns\InteractsWithActions; use Filament\Actions\Contracts\HasActions; use Filament\Forms\Components\Checkbox; -use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\Radio; +use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Concerns\InteractsWithForms; use Filament\Forms\Contracts\HasForms; @@ -66,6 +66,10 @@ class DirectAdminMigration extends Page implements HasActions, HasForms public ?string $remotePassword = null; + public ?string $localBackupPath = null; + + public array $availableBackups = []; + public ?string $backupPath = null; public bool $importFiles = true; @@ -111,6 +115,36 @@ class DirectAdminMigration extends Page implements HasActions, HasForms { $this->restoreFromSession(); $this->restoreFromImport(); + + if ($this->importMethod === 'backup_file') { + $this->loadLocalBackups(); + } + } + + public function updatedImportMethod(): void + { + $this->remoteHost = null; + $this->remotePort = 2222; + $this->remoteUser = null; + $this->remotePassword = null; + + $this->localBackupPath = null; + $this->backupPath = null; + $this->availableBackups = []; + + if ($this->importMethod === 'backup_file') { + $this->loadLocalBackups(); + } + } + + public function updatedLocalBackupPath(): void + { + if (! $this->localBackupPath) { + $this->backupPath = null; + return; + } + + $this->selectLocalBackup(); } protected function getForms(): array @@ -175,20 +209,39 @@ class DirectAdminMigration extends Page implements HasActions, HasForms ->visible(fn (Get $get): bool => $get('importMethod') === 'remote_server'), ]), - FileUpload::make('backupPath') - ->label(__('DirectAdmin Backup Archive')) - ->helperText(__('Upload a .tar.gz DirectAdmin backup file.')) - ->disk('local') - ->directory('imports/directadmin') - ->preserveFilenames() - ->acceptedFileTypes([ - 'application/gzip', - 'application/x-gzip', - 'application/x-tar', - 'application/octet-stream', + Section::make(__('Backup File')) + ->description(__('Upload your DirectAdmin backup archive to your backups folder, then select it here.')) + ->icon('heroicon-o-folder') + ->visible(fn (Get $get): bool => $get('importMethod') === 'backup_file') + ->headerActions([ + Action::make('refreshLocalBackups') + ->label(__('Refresh')) + ->icon('heroicon-o-arrow-path') + ->color('gray') + ->action('refreshLocalBackups'), ]) - ->required() - ->visible(fn (Get $get): bool => $get('importMethod') === 'backup_file'), + ->schema([ + Select::make('localBackupPath') + ->label(__('Backup File')) + ->options(fn (): array => $this->getLocalBackupOptions()) + ->searchable() + ->required(fn (Get $get): bool => $get('importMethod') === 'backup_file') + ->live(), + Text::make(fn (): string => $this->backupPath + ? __('Selected file: :file', ['file' => basename($this->backupPath)]) + : __('No backup selected yet.')) + ->color('gray'), + Text::make(fn (): string => ($user = $this->getUser()) + ? __('Upload the file to: /home/:user/backups', ['user' => $user->username]) + : __('Upload the file to your /home//backups folder.')) + ->color('gray'), + Text::make(__('Supported formats: .tar.gz, .tgz'))->color('gray'), + Text::make(fn (): string => ($user = $this->getUser()) + ? __('No backups found in /home/:user/backups. Upload a file there and click Refresh.', ['user' => $user->username]) + : __('No backups found.')) + ->color('gray') + ->visible(fn (): bool => empty($this->availableBackups)), + ]), FormActions::make([ Action::make('discoverAccount') @@ -323,10 +376,13 @@ class DirectAdminMigration extends Page implements HasActions, HasForms if ($this->importMethod === 'backup_file') { if (! $import->backup_path) { - throw new Exception(__('Please upload a DirectAdmin backup archive.')); + throw new Exception(__('Please select a DirectAdmin backup archive.')); } - $backupFullPath = Storage::disk('local')->path($import->backup_path); + $backupFullPath = $this->resolveBackupFullPath($import->backup_path); + if (! $backupFullPath) { + throw new Exception(__('Backup file not found: :path', ['path' => $import->backup_path])); + } } else { $remotePassword = $this->remotePassword; @@ -503,6 +559,8 @@ class DirectAdminMigration extends Page implements HasActions, HasForms $this->remotePort = 2222; $this->remoteUser = null; $this->remotePassword = null; + $this->localBackupPath = null; + $this->availableBackups = []; $this->backupPath = null; $this->importFiles = true; $this->importDatabases = true; @@ -515,6 +573,170 @@ class DirectAdminMigration extends Page implements HasActions, HasForms return $this->agent ??= new AgentClient; } + protected function getUser() + { + return Auth::user(); + } + + protected function loadLocalBackups(): void + { + $this->availableBackups = []; + + $user = $this->getUser(); + if (! $user) { + return; + } + + $result = $this->getAgent()->send('file.list', [ + 'username' => $user->username, + 'path' => 'backups', + ]); + + if (! ($result['success'] ?? false)) { + $this->getAgent()->send('file.mkdir', [ + 'username' => $user->username, + 'path' => 'backups', + ]); + + $result = $this->getAgent()->send('file.list', [ + 'username' => $user->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; + } + + protected function selectLocalBackup(): void + { + $user = $this->getUser(); + if (! $user || ! $this->localBackupPath) { + return; + } + + $info = $this->getAgent()->send('file.info', [ + 'username' => $user->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(); + $this->backupPath = null; + + return; + } + + $details = $info['info'] ?? []; + if (! ($details['is_file'] ?? false)) { + Notification::make() + ->title(__('Invalid backup selection')) + ->body(__('Please select a backup file')) + ->warning() + ->send(); + $this->backupPath = null; + + return; + } + + $this->backupPath = "/home/{$user->username}/{$this->localBackupPath}"; + + Notification::make() + ->title(__('Backup selected')) + ->body(__('Selected :name (:size)', [ + 'name' => $details['name'] ?? basename($this->backupPath), + 'size' => $this->formatBytes((int) ($details['size'] ?? 0)), + ])) + ->success() + ->send(); + } + + protected function formatBytes(int $bytes): string + { + if ($bytes >= 1073741824) { + return number_format($bytes / 1073741824, 2) . ' GB'; + } + if ($bytes >= 1048576) { + return number_format($bytes / 1048576, 2) . ' MB'; + } + if ($bytes >= 1024) { + return number_format($bytes / 1024, 2) . ' KB'; + } + + return $bytes . ' B'; + } + + protected function resolveBackupFullPath(?string $path): ?string + { + $path = trim((string) ($path ?? '')); + if ($path === '') { + return null; + } + + if (str_starts_with($path, '/') && file_exists($path)) { + return $path; + } + + $localCandidate = Storage::disk('local')->path($path); + if (file_exists($localCandidate)) { + return $localCandidate; + } + + $backupCandidate = Storage::disk('backups')->path($path); + if (file_exists($backupCandidate)) { + return $backupCandidate; + } + + return file_exists($path) ? $path : null; + } + protected function getImport(): ?ServerImport { if (! $this->importId) { @@ -597,6 +819,12 @@ class DirectAdminMigration extends Page implements HasActions, HasForms $this->importMethod = (string) ($import->import_method ?? 'backup_file'); $this->backupPath = $import->backup_path; + if ($this->backupPath && ($user = $this->getUser())) { + $prefix = "/home/{$user->username}/"; + if (str_starts_with($this->backupPath, $prefix)) { + $this->localBackupPath = ltrim(substr($this->backupPath, strlen($prefix)), '/'); + } + } $this->remoteHost = $import->remote_host; $this->remotePort = (int) ($import->remote_port ?? 2222); $this->remoteUser = $import->remote_user; @@ -612,4 +840,3 @@ class DirectAdminMigration extends Page implements HasActions, HasForms $this->step1Complete = $import->accounts()->exists(); } } - diff --git a/config/filesystems.php b/config/filesystems.php index 09c9c38..394c7db 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -37,6 +37,13 @@ return [ 'root' => '/tmp', 'throw' => false, ], + + // Server-wide backups folder (created by install.sh) + 'backups' => [ + 'driver' => 'local', + 'root' => env('JABALI_BACKUPS_ROOT', '/var/backups/jabali'), + 'throw' => false, + ], ], 'links' => [ diff --git a/install.sh b/install.sh index 4c021b0..bcfbb5c 100755 --- a/install.sh +++ b/install.sh @@ -2945,8 +2945,9 @@ EOF mkdir -p /var/backups/jabali mkdir -p /var/backups/jabali/cpanel-migrations mkdir -p /var/backups/jabali/whm-migrations + mkdir -p /var/backups/jabali/directadmin-migrations chown -R $JABALI_USER:$JABALI_USER /var/backups/jabali - chmod 755 /var/backups/jabali /var/backups/jabali/cpanel-migrations /var/backups/jabali/whm-migrations + chmod 755 /var/backups/jabali /var/backups/jabali/cpanel-migrations /var/backups/jabali/whm-migrations /var/backups/jabali/directadmin-migrations log "Jabali Panel setup complete" }