From 443b05a677e64338b2dfba5f2249426585498b06 Mon Sep 17 00:00:00 2001 From: Shuki Vaknin Date: Wed, 11 Feb 2026 00:38:05 +0200 Subject: [PATCH] Support DirectAdmin .tar.zst backups --- .../Commands/Jabali/ImportProcessCommand.php | 182 +++++++++++++----- .../Admin/Pages/DirectAdminMigration.php | 40 +++- .../Jabali/Pages/DirectAdminMigration.php | 19 +- bin/jabali-agent | 28 ++- 4 files changed, 196 insertions(+), 73 deletions(-) diff --git a/app/Console/Commands/Jabali/ImportProcessCommand.php b/app/Console/Commands/Jabali/ImportProcessCommand.php index 7363b04..fb89722 100644 --- a/app/Console/Commands/Jabali/ImportProcessCommand.php +++ b/app/Console/Commands/Jabali/ImportProcessCommand.php @@ -18,6 +18,7 @@ use Illuminate\Support\Str; class ImportProcessCommand extends Command { protected $signature = 'import:process {import_id : The server import ID to process}'; + protected $description = 'Process a server import job (cPanel/DirectAdmin migration)'; private ?AgentClient $agent = null; @@ -27,8 +28,9 @@ class ImportProcessCommand extends Command $importId = (int) $this->argument('import_id'); $import = ServerImport::with('accounts')->find($importId); - if (!$import) { + if (! $import) { $this->error("Import not found: $importId"); + return 1; } @@ -43,6 +45,7 @@ class ImportProcessCommand extends Command 'current_task' => null, ]); $import->addError('No accounts selected for import'); + return 1; } @@ -67,8 +70,8 @@ class ImportProcessCommand extends Command 'status' => 'failed', 'error' => $e->getMessage(), ]); - $account->addLog("Import failed: " . $e->getMessage()); - $import->addError("Account {$account->source_username}: " . $e->getMessage()); + $account->addLog('Import failed: '.$e->getMessage()); + $import->addError("Account {$account->source_username}: ".$e->getMessage()); } } @@ -96,10 +99,10 @@ class ImportProcessCommand extends Command 'completed_at' => now(), 'progress' => 100, ]); - $import->addLog("All accounts imported successfully"); + $import->addLog('All accounts imported successfully'); } - $this->info("Import completed. Success: " . ($totalAccounts - $failedCount) . ", Failed: $failedCount"); + $this->info('Import completed. Success: '.($totalAccounts - $failedCount).", Failed: $failedCount"); return 0; } @@ -107,8 +110,9 @@ class ImportProcessCommand extends Command private function getAgent(): AgentClient { if ($this->agent === null) { - $this->agent = new AgentClient(); + $this->agent = new AgentClient; } + return $this->agent; } @@ -132,28 +136,28 @@ class ImportProcessCommand extends Command if ($account->main_domain) { $account->update(['current_task' => 'Creating domains...', 'progress' => 20]); $this->createDomains($account, $user); - $account->addLog("Created domains"); + $account->addLog('Created domains'); } // Step 3: Import files if ($options['files'] ?? true) { $account->update(['current_task' => 'Importing files...', 'progress' => 40]); $this->importFiles($import, $account, $user); - $account->addLog("Files imported"); + $account->addLog('Files imported'); } // Step 4: Import databases - if (($options['databases'] ?? true) && !empty($account->databases)) { + if (($options['databases'] ?? true) && ! empty($account->databases)) { $account->update(['current_task' => 'Importing databases...', 'progress' => 60]); $this->importDatabases($import, $account, $user); - $account->addLog("Databases imported"); + $account->addLog('Databases imported'); } // Step 5: Import emails - if (($options['emails'] ?? true) && !empty($account->email_accounts)) { + if (($options['emails'] ?? true) && ! empty($account->email_accounts)) { $account->update(['current_task' => 'Importing email accounts...', 'progress' => 80]); $this->importEmails($import, $account, $user); - $account->addLog("Email accounts imported"); + $account->addLog('Email accounts imported'); } $account->update([ @@ -161,7 +165,7 @@ class ImportProcessCommand extends Command 'progress' => 100, 'current_task' => null, ]); - $account->addLog("Import completed successfully"); + $account->addLog('Import completed successfully'); } private function createUser(ServerImportAccount $account): User @@ -170,6 +174,7 @@ class ImportProcessCommand extends Command $existingUser = User::where('username', $account->target_username)->first(); if ($existingUser) { $account->addLog("User already exists: {$account->target_username}"); + return $existingUser; } @@ -179,8 +184,8 @@ class ImportProcessCommand extends Command // Create user via agent $result = $this->getAgent()->createUser($account->target_username, $password); - if (!($result['success'] ?? false)) { - throw new Exception("Failed to create system user: " . ($result['error'] ?? 'Unknown error')); + if (! ($result['success'] ?? false)) { + throw new Exception('Failed to create system user: '.($result['error'] ?? 'Unknown error')); } // Create user in database @@ -191,7 +196,7 @@ class ImportProcessCommand extends Command 'password' => Hash::make($password), ]); - $account->addLog("Created user with temporary password. User should reset password."); + $account->addLog('Created user with temporary password. User should reset password.'); return $user; } @@ -201,7 +206,7 @@ class ImportProcessCommand extends Command // Create main domain if ($account->main_domain) { $existingDomain = Domain::where('domain', $account->main_domain)->first(); - if (!$existingDomain) { + if (! $existingDomain) { $result = $this->getAgent()->domainCreate($user->username, $account->main_domain); if ($result['success'] ?? false) { @@ -213,7 +218,7 @@ class ImportProcessCommand extends Command ]); $account->addLog("Created main domain: {$account->main_domain}"); } else { - $account->addLog("Warning: Failed to create main domain: " . ($result['error'] ?? 'Unknown')); + $account->addLog('Warning: Failed to create main domain: '.($result['error'] ?? 'Unknown')); } } else { $account->addLog("Main domain already exists: {$account->main_domain}"); @@ -223,7 +228,7 @@ class ImportProcessCommand extends Command // Create addon domains foreach ($account->addon_domains ?? [] as $domain) { $existingDomain = Domain::where('domain', $domain)->first(); - if (!$existingDomain) { + if (! $existingDomain) { $result = $this->getAgent()->domainCreate($user->username, $domain); if ($result['success'] ?? false) { @@ -243,31 +248,38 @@ class ImportProcessCommand extends Command private function importFiles(ServerImport $import, ServerImportAccount $account, User $user): void { - if ($import->import_method !== 'backup_file' || !$import->backup_path) { - $account->addLog("File import skipped - not a backup file import"); + if ($import->import_method !== 'backup_file' || ! $import->backup_path) { + $account->addLog('File import skipped - not a backup file import'); + return; } $backupPath = $this->resolveBackupFullPath($import); if (! $backupPath) { - $account->addLog("Warning: Backup file not found"); + $account->addLog('Warning: Backup file not found'); + return; } - $extractDir = "/tmp/import_{$import->id}_{$account->id}_" . time(); - if (!mkdir($extractDir, 0755, true)) { - $account->addLog("Warning: Failed to create extraction directory"); + $extractDir = "/tmp/import_{$import->id}_{$account->id}_".time(); + if (! mkdir($extractDir, 0755, true)) { + $account->addLog('Warning: Failed to create extraction directory'); + return; } try { $username = $account->source_username; + $tarExtract = $this->getTarExtractCommandPrefix($backupPath); if ($import->source_type === 'cpanel') { // Extract home directory from cPanel backup - $cmd = "tar -xzf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($extractDir) . + $cmd = "{$tarExtract} ".escapeshellarg($backupPath).' -C '.escapeshellarg($extractDir). " --wildcards '*/{$username}/homedir/*' '*/homedir/*' 2>/dev/null"; exec($cmd, $output, $code); + if ($code !== 0) { + $account->addLog('Warning: Failed to extract backup archive'); + } // Find extracted files $homeDirs = glob("$extractDir/**/homedir", GLOB_ONLYDIR) ?: @@ -280,17 +292,20 @@ class ImportProcessCommand extends Command if (is_dir($publicHtml) && $account->main_domain) { $destDir = "/home/{$user->username}/domains/{$account->main_domain}/public"; if (is_dir($destDir)) { - exec("cp -r " . escapeshellarg($publicHtml) . "/* " . escapeshellarg($destDir) . "/ 2>&1"); - exec("chown -R " . escapeshellarg($user->username) . ":" . escapeshellarg($user->username) . " " . escapeshellarg($destDir) . " 2>&1"); + exec('cp -r '.escapeshellarg($publicHtml).'/* '.escapeshellarg($destDir).'/ 2>&1'); + exec('chown -R '.escapeshellarg($user->username).':'.escapeshellarg($user->username).' '.escapeshellarg($destDir).' 2>&1'); $account->addLog("Copied public_html to {$account->main_domain}"); } } } } else { // Extract from DirectAdmin backup - $cmd = "tar -xzf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($extractDir) . + $cmd = "{$tarExtract} ".escapeshellarg($backupPath).' -C '.escapeshellarg($extractDir). " --wildcards 'domains/*' 'backup/domains/*' 2>/dev/null"; exec($cmd, $output, $code); + if ($code !== 0) { + $account->addLog('Warning: Failed to extract DirectAdmin backup archive'); + } // Find domain directories $domainDirs = glob("$extractDir/**/domains/*", GLOB_ONLYDIR) ?: @@ -303,8 +318,8 @@ class ImportProcessCommand extends Command if (is_dir($publicHtml)) { $destDir = "/home/{$user->username}/domains/{$domain}/public"; if (is_dir($destDir)) { - exec("cp -r " . escapeshellarg($publicHtml) . "/* " . escapeshellarg($destDir) . "/ 2>&1"); - exec("chown -R " . escapeshellarg($user->username) . ":" . escapeshellarg($user->username) . " " . escapeshellarg($destDir) . " 2>&1"); + exec('cp -r '.escapeshellarg($publicHtml).'/* '.escapeshellarg($destDir).'/ 2>&1'); + exec('chown -R '.escapeshellarg($user->username).':'.escapeshellarg($user->username).' '.escapeshellarg($destDir).' 2>&1'); $account->addLog("Copied files for domain: {$domain}"); } } @@ -312,14 +327,15 @@ class ImportProcessCommand extends Command } } finally { // Cleanup - exec("rm -rf " . escapeshellarg($extractDir)); + exec('rm -rf '.escapeshellarg($extractDir)); } } private function importDatabases(ServerImport $import, ServerImportAccount $account, User $user): void { - if ($import->import_method !== 'backup_file' || !$import->backup_path) { - $account->addLog("Database import skipped - not a backup file import"); + if ($import->import_method !== 'backup_file' || ! $import->backup_path) { + $account->addLog('Database import skipped - not a backup file import'); + return; } @@ -328,51 +344,98 @@ class ImportProcessCommand extends Command return; } - $extractDir = "/tmp/import_db_{$import->id}_{$account->id}_" . time(); - if (!mkdir($extractDir, 0755, true)) { + $extractDir = "/tmp/import_db_{$import->id}_{$account->id}_".time(); + if (! mkdir($extractDir, 0755, true)) { return; } try { + $tarExtract = $this->getTarExtractCommandPrefix($backupPath); + // Extract MySQL dumps if ($import->source_type === 'cpanel') { - $cmd = "tar -xzf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($extractDir) . - " --wildcards '*/mysql/*.sql' 'mysql/*.sql' 2>/dev/null"; + $cmd = "{$tarExtract} ".escapeshellarg($backupPath).' -C '.escapeshellarg($extractDir). + " --wildcards '*/mysql/*.sql*' 'mysql/*.sql*' 2>/dev/null"; } else { - $cmd = "tar -xzf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($extractDir) . - " --wildcards 'backup/databases/*.sql' 'databases/*.sql' 2>/dev/null"; + $cmd = "{$tarExtract} ".escapeshellarg($backupPath).' -C '.escapeshellarg($extractDir). + " --wildcards 'backup/databases/*.sql*' 'databases/*.sql*' 2>/dev/null"; } exec($cmd, $output, $code); + if ($code !== 0) { + $account->addLog('Warning: Failed to extract database dumps from backup archive'); + } // Find SQL files $sqlFiles = []; - exec("find " . escapeshellarg($extractDir) . " -name '*.sql' -type f 2>/dev/null", $sqlFiles); + exec('find '.escapeshellarg($extractDir)." -type f \\( -name '*.sql' -o -name '*.sql.gz' -o -name '*.sql.zst' \\) 2>/dev/null", $sqlFiles); foreach ($sqlFiles as $sqlFile) { - $dbName = basename($sqlFile, '.sql'); + $fileName = basename($sqlFile); + $dbName = preg_replace('/\\.(sql|sql\\.gz|sql\\.zst)$/i', '', $fileName); + + if (! is_string($dbName) || $dbName === '') { + continue; + } // Create database name with user prefix - $newDbName = substr($user->username . '_' . preg_replace('/^[^_]+_/', '', $dbName), 0, 64); + $newDbName = substr($user->username.'_'.preg_replace('/^[^_]+_/', '', $dbName), 0, 64); // Create database via agent $result = $this->getAgent()->mysqlCreateDatabase($user->username, $newDbName); if ($result['success'] ?? false) { - // Import data - $cmd = "mysql " . escapeshellarg($newDbName) . " < " . escapeshellarg($sqlFile) . " 2>&1"; - exec($cmd, $importOutput, $importCode); + $sqlToImport = $sqlFile; + $tmpSql = null; - if ($importCode === 0) { - $account->addLog("Imported database: {$newDbName}"); - } else { - $account->addLog("Warning: Database created but import failed: {$newDbName}"); + try { + $lower = strtolower($sqlFile); + + if (str_ends_with($lower, '.sql.gz')) { + $tmpSql = $extractDir.'/import_'.$account->id.'_'.$dbName.'_'.uniqid('', true).'.sql'; + $decompressCmd = 'gzip -dc '.escapeshellarg($sqlFile).' > '.escapeshellarg($tmpSql).' 2>/dev/null'; + exec($decompressCmd, $decompressOutput, $decompressCode); + + if ($decompressCode !== 0) { + $account->addLog("Warning: Failed to decompress database dump: {$fileName}"); + + continue; + } + + $sqlToImport = $tmpSql; + } elseif (str_ends_with($lower, '.sql.zst')) { + $tmpSql = $extractDir.'/import_'.$account->id.'_'.$dbName.'_'.uniqid('', true).'.sql'; + $decompressCmd = 'zstd -dc '.escapeshellarg($sqlFile).' > '.escapeshellarg($tmpSql).' 2>/dev/null'; + exec($decompressCmd, $decompressOutput, $decompressCode); + + if ($decompressCode !== 0) { + $account->addLog("Warning: Failed to decompress database dump: {$fileName}"); + + continue; + } + + $sqlToImport = $tmpSql; + } + + // Import data + $cmd = 'mysql '.escapeshellarg($newDbName).' < '.escapeshellarg($sqlToImport).' 2>&1'; + exec($cmd, $importOutput, $importCode); + + if ($importCode === 0) { + $account->addLog("Imported database: {$newDbName}"); + } else { + $account->addLog("Warning: Database created but import failed: {$newDbName}"); + } + } finally { + if ($tmpSql && file_exists($tmpSql)) { + @unlink($tmpSql); + } } } else { $account->addLog("Warning: Failed to create database: {$newDbName}"); } } } finally { - exec("rm -rf " . escapeshellarg($extractDir)); + exec('rm -rf '.escapeshellarg($extractDir)); } } @@ -384,7 +447,7 @@ class ImportProcessCommand extends Command $account->addLog("Email account found (not imported): {$emailAccount}@{$account->main_domain}"); } - $account->addLog("Note: Email accounts must be recreated manually"); + $account->addLog('Note: Email accounts must be recreated manually'); } private function resolveBackupFullPath(ServerImport $import): ?string @@ -410,4 +473,19 @@ class ImportProcessCommand extends Command return file_exists($path) ? $path : null; } + + private function getTarExtractCommandPrefix(string $archivePath): string + { + $archivePath = strtolower($archivePath); + + if (str_ends_with($archivePath, '.tar.zst')) { + return 'tar --zstd -xf'; + } + + if (str_ends_with($archivePath, '.tar.gz') || str_ends_with($archivePath, '.tgz')) { + return 'tar -xzf'; + } + + return 'tar -xf'; + } } diff --git a/app/Filament/Admin/Pages/DirectAdminMigration.php b/app/Filament/Admin/Pages/DirectAdminMigration.php index ae846bc..e58b6ab 100644 --- a/app/Filament/Admin/Pages/DirectAdminMigration.php +++ b/app/Filament/Admin/Pages/DirectAdminMigration.php @@ -69,6 +69,8 @@ class DirectAdminMigration extends Page implements HasActions, HasForms public ?string $backupPath = null; + public ?string $backupFilePath = null; + public bool $importFiles = true; public bool $importDatabases = true; @@ -146,7 +148,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms Grid::make(['default' => 1, 'sm' => 2])->schema([ TextInput::make('name') ->label(__('Import Name')) - ->default(fn (): string => $this->name ?: ('DirectAdmin Import ' . now()->format('Y-m-d H:i'))) + ->default(fn (): string => $this->name ?: ('DirectAdmin Import '.now()->format('Y-m-d H:i'))) ->maxLength(255) ->required(), Radio::make('importMethod') @@ -186,17 +188,23 @@ class DirectAdminMigration extends Page implements HasActions, HasForms FileUpload::make('backupPath') ->label(__('DirectAdmin Backup Archive')) - ->helperText(__('Upload a DirectAdmin backup file to the server backups folder.')) + ->helperText(__('Upload a DirectAdmin backup archive (usually .tar.zst) to the server backups folder.')) ->disk('backups') ->directory('directadmin-migrations') ->preserveFilenames() ->acceptedFileTypes([ 'application/gzip', 'application/x-gzip', + 'application/zstd', + 'application/x-zstd', 'application/x-tar', 'application/octet-stream', ]) - ->required() + ->visible(fn (Get $get): bool => $get('importMethod') === 'backup_file'), + TextInput::make('backupFilePath') + ->label(__('Backup File Path')) + ->placeholder('/var/backups/jabali/directadmin-migrations/user.tar.zst') + ->helperText(__('Use this if the backup file already exists on the server.')) ->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'), @@ -385,7 +393,7 @@ 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 upload a DirectAdmin backup archive or enter its full path.')); } $backupFullPath = $this->resolveBackupFullPath($import->backup_path); @@ -554,6 +562,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms ->title(__('Import job not found')) ->danger() ->send(); + return; } @@ -564,6 +573,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms ->body(__('Please select at least one account to migrate.')) ->danger() ->send(); + return; } @@ -573,6 +583,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms ->body(__('For now, please download a DirectAdmin backup archive and use the "Backup File" method.')) ->warning() ->send(); + return; } @@ -589,6 +600,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms ->body((string) ($result['error'] ?? __('Unknown error'))) ->danger() ->send(); + return; } @@ -617,6 +629,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms $this->remoteUser = null; $this->remotePassword = null; $this->backupPath = null; + $this->backupFilePath = null; $this->importFiles = true; $this->importDatabases = true; $this->importEmails = true; @@ -641,7 +654,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms { $name = trim((string) ($this->name ?: '')); if ($name === '') { - $name = 'DirectAdmin Import ' . now()->format('Y-m-d H:i'); + $name = 'DirectAdmin Import '.now()->format('Y-m-d H:i'); } $attributes = [ @@ -660,7 +673,11 @@ class DirectAdminMigration extends Page implements HasActions, HasForms ]; if ($this->importMethod === 'backup_file') { - $attributes['backup_path'] = $this->backupPath; + $backupPath = filled($this->backupFilePath) + ? trim((string) $this->backupFilePath) + : $this->backupPath; + + $attributes['backup_path'] = $backupPath ?: null; $attributes['remote_host'] = null; $attributes['remote_port'] = null; $attributes['remote_user'] = null; @@ -743,7 +760,16 @@ class DirectAdminMigration extends Page implements HasActions, HasForms $this->name = $import->name; $this->importMethod = (string) ($import->import_method ?? 'remote_server'); - $this->backupPath = $import->backup_path; + + $backupPath = is_string($import->backup_path) ? trim($import->backup_path) : null; + if ($backupPath && str_starts_with($backupPath, '/')) { + $this->backupFilePath = $backupPath; + $this->backupPath = null; + } else { + $this->backupPath = $backupPath; + $this->backupFilePath = null; + } + $this->remoteHost = $import->remote_host; $this->remotePort = (int) ($import->remote_port ?? 2222); $this->remoteUser = $import->remote_user; diff --git a/app/Filament/Jabali/Pages/DirectAdminMigration.php b/app/Filament/Jabali/Pages/DirectAdminMigration.php index 863198c..4b61758 100644 --- a/app/Filament/Jabali/Pages/DirectAdminMigration.php +++ b/app/Filament/Jabali/Pages/DirectAdminMigration.php @@ -141,6 +141,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms { if (! $this->localBackupPath) { $this->backupPath = null; + return; } @@ -235,7 +236,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms ? __('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(__('Supported formats: .tar.zst, .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.')) @@ -497,6 +498,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms ->title(__('Import job not found')) ->danger() ->send(); + return; } @@ -506,6 +508,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms ->title(__('No account selected')) ->danger() ->send(); + return; } @@ -515,6 +518,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms ->body(__('For now, please download a DirectAdmin backup archive and use the "Backup File" method.')) ->warning() ->send(); + return; } @@ -531,6 +535,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms ->body((string) ($result['error'] ?? __('Unknown error'))) ->danger() ->send(); + return; } @@ -615,7 +620,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms } $name = (string) ($item['name'] ?? ''); - if (! preg_match('/\\.(tar\\.gz|tgz)$/i', $name)) { + if (! preg_match('/\\.(tar\\.zst|tar\\.gz|tgz)$/i', $name)) { continue; } @@ -701,16 +706,16 @@ class DirectAdminMigration extends Page implements HasActions, HasForms protected function formatBytes(int $bytes): string { if ($bytes >= 1073741824) { - return number_format($bytes / 1073741824, 2) . ' GB'; + return number_format($bytes / 1073741824, 2).' GB'; } if ($bytes >= 1048576) { - return number_format($bytes / 1048576, 2) . ' MB'; + return number_format($bytes / 1048576, 2).' MB'; } if ($bytes >= 1024) { - return number_format($bytes / 1024, 2) . ' KB'; + return number_format($bytes / 1024, 2).' KB'; } - return $bytes . ' B'; + return $bytes.' B'; } protected function resolveBackupFullPath(?string $path): ?string @@ -749,7 +754,7 @@ class DirectAdminMigration extends Page implements HasActions, HasForms protected function upsertImportForDiscovery(): ServerImport { $user = Auth::user(); - $name = $user ? ('DirectAdmin Import - ' . $user->username . ' - ' . now()->format('Y-m-d H:i')) : ('DirectAdmin Import ' . now()->format('Y-m-d H:i')); + $name = $user ? ('DirectAdmin Import - '.$user->username.' - '.now()->format('Y-m-d H:i')) : ('DirectAdmin Import '.now()->format('Y-m-d H:i')); $attributes = [ 'name' => $name, diff --git a/bin/jabali-agent b/bin/jabali-agent index d030618..ba14373 100755 --- a/bin/jabali-agent +++ b/bin/jabali-agent @@ -12681,23 +12681,30 @@ function discoverDirectAdminBackup(string $backupPath, string $extractDir): arra $accounts = []; + $tarArgs = ''; + if (preg_match('/\\.tar\\.zst$/i', $backupPath)) { + $tarArgs = '--zstd'; + } elseif (preg_match('/\\.(tar\\.gz|tgz)$/i', $backupPath)) { + $tarArgs = '-I pigz'; + } + // List archive contents - $cmd = "tar -I pigz -tf " . escapeshellarg($backupPath) . " 2>/dev/null | head -500"; + $cmd = "tar $tarArgs -tf " . escapeshellarg($backupPath) . " 2>/dev/null | head -500"; exec($cmd, $fileList, $code); if ($code !== 0) { - throw new Exception('Failed to read backup file. Make sure it is a valid tar.gz archive.'); + throw new Exception('Failed to read backup file. Make sure it is a valid DirectAdmin archive (.tar.zst, .tar.gz, .tgz).'); } $fileListStr = implode("\n", $fileList); // DirectAdmin backup structure: backup/user.conf, domains/, databases/ - // Or: user.username.tar.gz containing the above + // Or: user.username.tar.zst (or .tar.gz) containing the above // Check for user.conf file (single user backup) if (preg_match('/(backup\/)?user\.conf/', $fileListStr)) { // Extract user.conf and domains list - $extractCmd = "tar -I pigz -xf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($extractDir) . " --wildcards 'backup/user.conf' 'user.conf' 'domains/*' 'backup/domains/*' 2>/dev/null"; + $extractCmd = "tar $tarArgs -xf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($extractDir) . " --wildcards 'backup/user.conf' 'user.conf' 'domains/*' 'backup/domains/*' 2>/dev/null"; exec($extractCmd); $userConf = null; @@ -12718,18 +12725,25 @@ function discoverDirectAdminBackup(string $backupPath, string $extractDir): arra // Check for multiple user backups (full server backup) foreach ($fileList as $file) { - if (preg_match('/user\.([a-z0-9_]+)\.tar\.gz/i', $file, $matches)) { + if (preg_match('/user\\.([a-z0-9_]+)\\.(?:tar\\.zst|tar\\.gz|tgz)$/i', $file, $matches)) { $username = $matches[1]; // Extract just this user's backup $innerBackup = $file; - exec("tar -I pigz -xf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($extractDir) . " " . escapeshellarg($innerBackup) . " 2>/dev/null"); + exec("tar $tarArgs -xf " . escapeshellarg($backupPath) . " -C " . escapeshellarg($extractDir) . " " . escapeshellarg($innerBackup) . " 2>/dev/null"); $innerPath = "$extractDir/$innerBackup"; if (file_exists($innerPath)) { + $innerTarArgs = ''; + if (preg_match('/\\.tar\\.zst$/i', $innerPath)) { + $innerTarArgs = '--zstd'; + } elseif (preg_match('/\\.(tar\\.gz|tgz)$/i', $innerPath)) { + $innerTarArgs = '-I pigz'; + } + $innerExtract = "$extractDir/$username"; mkdir($innerExtract, 0755, true); - exec("tar -I pigz -xf " . escapeshellarg($innerPath) . " -C " . escapeshellarg($innerExtract) . " --wildcards 'backup/user.conf' 'user.conf' 2>/dev/null"); + exec("tar $innerTarArgs -xf " . escapeshellarg($innerPath) . " -C " . escapeshellarg($innerExtract) . " --wildcards 'backup/user.conf' 'user.conf' 2>/dev/null"); $userConf = glob("$innerExtract/*/user.conf")[0] ?? glob("$innerExtract/user.conf")[0] ?? null; if ($userConf) {