diff --git a/app/Filament/Jabali/Pages/DirectAdminMigration.php b/app/Filament/Jabali/Pages/DirectAdminMigration.php index 4b61758..445ed7b 100644 --- a/app/Filament/Jabali/Pages/DirectAdminMigration.php +++ b/app/Filament/Jabali/Pages/DirectAdminMigration.php @@ -13,6 +13,7 @@ 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; @@ -215,6 +216,113 @@ class DirectAdminMigration extends Page implements HasActions, HasForms ->icon('heroicon-o-folder') ->visible(fn (Get $get): bool => $get('importMethod') === 'backup_file') ->headerActions([ + Action::make('uploadBackup') + ->label(__('Upload')) + ->icon('heroicon-o-arrow-up-tray') + ->color('gray') + ->modalHeading(__('Upload Backup')) + ->modalDescription(fn (): string => ($user = $this->getUser()) + ? __('Upload a DirectAdmin backup archive into /home/:user/backups', ['user' => $user->username]) + : __('Upload a DirectAdmin backup archive into your backups folder')) + ->modalSubmitActionLabel(__('Upload')) + ->form([ + FileUpload::make('backup') + ->label(__('DirectAdmin Backup Archive')) + ->storeFiles(false) + ->required() + ->acceptedFileTypes([ + 'application/octet-stream', + 'application/x-tar', + 'application/zstd', + 'application/x-zstd', + 'application/gzip', + 'application/x-gzip', + ]) + ->maxSize(512000) // 500MB in KB + ->helperText(__('Supported formats: .tar.zst, .tar.gz, .tgz (max 500MB via upload)')), + ]) + ->action(function (array $data): void { + try { + $user = $this->getUser(); + if (! $user) { + throw new Exception(__('You must be logged in.')); + } + + $file = $data['backup'] ?? null; + if (! $file) { + throw new Exception(__('Please select a backup file.')); + } + + $filename = (string) $file->getClientOriginalName(); + $filename = basename($filename); + + if (! preg_match('/\\.(tar\\.zst|tar\\.gz|tgz)$/i', $filename)) { + throw new Exception(__('Backup must be a .tar.zst, .tar.gz or .tgz file.')); + } + + $maxBytes = 500 * 1024 * 1024; + $fileSize = (int) ($file->getSize() ?? 0); + if ($fileSize > $maxBytes) { + throw new Exception(__('File too large for upload (max 500MB). Upload it via SSH/SFTP to /home/:user/backups.', [ + 'user' => $user->username, + ])); + } + + // Ensure backups folder exists + $this->getAgent()->fileMkdir($user->username, 'backups'); + + // Stage into the agent-allowed temp dir, then let the agent move it. + $tmpDir = '/tmp/jabali-uploads'; + if (! is_dir($tmpDir)) { + mkdir($tmpDir, 0700, true); + chmod($tmpDir, 0700); + } else { + @chmod($tmpDir, 0700); + } + + $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename); + $tmpPath = $tmpDir.'/'.uniqid('upload_', true).'_'.$safeName; + + if (! @copy($file->getRealPath(), $tmpPath)) { + throw new Exception(__('Failed to stage upload.')); + } + @chmod($tmpPath, 0600); + + $result = $this->getAgent()->send('file.upload_temp', [ + 'username' => $user->username, + 'path' => 'backups', + 'filename' => $safeName, + 'temp_path' => $tmpPath, + ]); + + if (! ($result['success'] ?? false)) { + if (file_exists($tmpPath)) { + @unlink($tmpPath); + } + throw new Exception((string) ($result['error'] ?? __('Upload failed'))); + } + + $this->loadLocalBackups(); + + $uploadedPath = $result['path'] ?? null; + if (is_string($uploadedPath) && $uploadedPath !== '') { + $this->localBackupPath = $uploadedPath; + $this->selectLocalBackup(); + } + + Notification::make() + ->title(__('Backup uploaded')) + ->body(__('Uploaded :name', ['name' => $safeName])) + ->success() + ->send(); + } catch (Exception $e) { + Notification::make() + ->title(__('Upload failed')) + ->body($e->getMessage()) + ->danger() + ->send(); + } + }), Action::make('refreshLocalBackups') ->label(__('Refresh')) ->icon('heroicon-o-arrow-path')