findOrFail($this->argument('jobId')); $job->update(['status' => 'running']); $policy = $job->policy; $tmpDir = sys_get_temp_dir() . '/mailwolt_backup_' . $job->id; mkdir($tmpDir, 0700, true); $sources = []; $log = []; $exitCode = 0; // ── 1. External backup script (takes full control if present) ────── $script = '/usr/local/sbin/mailwolt-backup'; if (file_exists($script)) { $output = []; exec("bash " . escapeshellarg($script) . ' 2>&1', $output, $exitCode); $artifact = null; foreach ($output as $line) { if (str_starts_with($line, 'ARTIFACT:')) { $artifact = trim(substr($line, 9)); } } $this->finalize($job, $exitCode, implode("\n", array_slice($output, -30)), $artifact); $this->cleanTmp($tmpDir); return $exitCode === 0 ? self::SUCCESS : self::FAILURE; } // ── 2. Built-in backup ──────────────────────────────────────────── // Database if ($policy?->include_db ?? true) { [$ok, $msg, $dumpFile] = $this->dumpDatabase($tmpDir); if ($ok) { $sources[] = $dumpFile; $log[] = '✓ Datenbank gesichert'; } else { $log[] = '✗ Datenbank: ' . $msg; $exitCode = 1; } } // Mail directories if ($policy?->include_maildirs ?? true) { $mailDir = (string) Setting::get('backup_mail_dir', ''); if (empty($mailDir)) { foreach (['/var/vmail', '/var/mail/vhosts', '/home/vmail'] as $c) { if (is_dir($c)) { $mailDir = $c; break; } } } if ($mailDir && is_dir($mailDir)) { $sources[] = $mailDir; $log[] = '✓ Maildirs: ' . $mailDir; } else { $log[] = '✗ Maildir nicht gefunden (konfiguriere den Pfad in den Einstellungen)'; } } // Config files + SSL if ($policy?->include_configs ?? true) { foreach (['/etc/postfix', '/etc/dovecot', '/etc/opendkim', '/etc/rspamd', '/etc/letsencrypt'] as $dir) { if (is_dir($dir)) { $sources[] = $dir; $log[] = '✓ ' . $dir; } } if (file_exists(base_path('.env'))) { $sources[] = base_path('.env'); $log[] = '✓ .env'; } } if (empty($sources)) { $msg = 'Keine Quellen zum Sichern gefunden.'; $job->update(['status' => 'failed', 'finished_at' => now(), 'error' => $msg]); $this->cleanTmp($tmpDir); return self::FAILURE; } // Build archive — use storage/app/backups so www-data always has write access $outDir = storage_path('app/backups'); if (!is_dir($outDir) && !mkdir($outDir, 0750, true) && !is_dir($outDir)) { // Final fallback: /tmp (always writable) $outDir = sys_get_temp_dir() . '/mailwolt_backups'; mkdir($outDir, 0750, true); } $stamp = now()->format('Y-m-d_H-i-s'); $outFile = "{$outDir}/mailwolt_{$stamp}.tar.gz"; $srcArgs = implode(' ', array_map('escapeshellarg', $sources)); $tarOutput = []; $tarExit = 0; exec("tar --ignore-failed-read -czf " . escapeshellarg($outFile) . " {$srcArgs} 2>&1", $tarOutput, $tarExit); $this->cleanTmp($tmpDir); // tar exit 1 = some files unreadable but archive was written — treat as ok if ($tarExit <= 1 && file_exists($outFile)) { $tarExit = 0; } if ($tarExit !== 0) { $exitCode = $tarExit; } $fullLog = implode("\n", $log) . "\n\n" . implode("\n", array_slice($tarOutput, -20)); $this->finalize($job, $exitCode, $fullLog, $tarExit === 0 ? $outFile : null); return $exitCode === 0 ? self::SUCCESS : self::FAILURE; } private function dumpDatabase(string $tmpDir): array { $conn = config('database.connections.' . config('database.default')); if (($conn['driver'] ?? '') !== 'mysql') { return [false, 'Nur MySQL/MariaDB wird unterstützt.', null]; } $host = $conn['host'] ?? '127.0.0.1'; $port = (string)($conn['port'] ?? '3306'); $username = $conn['username'] ?? ''; $password = $conn['password'] ?? ''; $database = $conn['database'] ?? ''; $dumpFile = "{$tmpDir}/database.sql"; $cmd = 'MYSQL_PWD=' . escapeshellarg($password) . ' mysqldump' . ' -h ' . escapeshellarg($host) . ' -P ' . escapeshellarg($port) . ' -u ' . escapeshellarg($username) . ' --single-transaction --routines --triggers' . ' ' . escapeshellarg($database) . ' > ' . escapeshellarg($dumpFile) . ' 2>&1'; $output = []; $exit = 0; exec($cmd, $output, $exit); if ($exit !== 0 || !file_exists($dumpFile)) { return [false, implode('; ', $output), null]; } return [true, '', $dumpFile]; } private function finalize(BackupJob $job, int $exitCode, string $log, ?string $artifact): void { $sizeBytes = ($artifact && file_exists($artifact)) ? filesize($artifact) : 0; if ($exitCode === 0) { $job->update([ 'status' => 'ok', 'finished_at' => now(), 'log_excerpt' => $log, 'artifact_path' => $artifact, 'size_bytes' => $sizeBytes, ]); $job->policy?->update([ 'last_run_at' => now(), 'last_status' => 'ok', 'last_size_bytes' => $sizeBytes, ]); } else { $job->update([ 'status' => 'failed', 'finished_at' => now(), 'error' => $log, ]); $job->policy?->update(['last_status' => 'failed']); } } private function cleanTmp(string $dir): void { if (!is_dir($dir)) return; foreach (glob("{$dir}/*") ?: [] as $f) { is_file($f) ? unlink($f) : null; } @rmdir($dir); } }