argument('target') ?: '/'; $data = $this->probe($target); Setting::set('health.disk', $data); Setting::set('health.disk_updated_at', now()->toIso8601String()); // hübsche Konsole $hb = function (int $bytes): string { $b = max(0, $bytes); if ($b >= 1024 ** 3) return number_format($b / 1024 ** 3, 1) . ' GB'; if ($b >= 1024 ** 2) return number_format($b / 1024 ** 2, 2) . ' MiB'; if ($b >= 1024) return number_format($b / 1024, 0) . ' KiB'; return $b . ' B'; }; $bd = $data['breakdown_bytes'] ?? ['system' => 0, 'mails' => 0, 'backup' => 0]; $this->info(sprintf( 'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d) breakdown: system=%s mails=%s backups=%s', $data['mount'], $data['total_gb'], $data['used_gb'], $data['free_gb'], $data['free_plus_reserve_gb'], $data['percent_used_total'], $hb((int)$bd['system']), $hb((int)$bd['mails']), $hb((int)$bd['backup']) )); return self::SUCCESS; } protected function probe(string $target): array { // ── 1) df lesen (Gesamt/Frei inkl. Reserve) ────────────────────────── $line = trim((string)@shell_exec('LC_ALL=C df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1')); $device = $mount = ''; $totalKb = $usedKb = $availKb = 0; if ($line !== '') { $p = preg_split('/\s+/', $line); if (count($p) >= 6) { $device = $p[0]; $totalKb = (int)$p[1]; // TOTAL (inkl. Reserve) $usedKb = (int)$p[2]; // Used $availKb = (int)$p[3]; // Avail (User-sicht) $mount = $p[5]; } } $toGiB_i = static fn($kb) => (int)round(max(0, (int)$kb) / (1024 * 1024)); // Ganzzahl für Kopfzahlen $totalGb = $toGiB_i($totalKb); $freeGb = $toGiB_i($availKb); $usedGb = max(0, $totalGb - $freeGb); $res5Gb = (int)round($totalGb * 0.05); $freePlusReserveGb = min($totalGb, $freeGb + $res5Gb); $percentUsed = $totalGb > 0 ? (int)round($usedGb * 100 / $totalGb) : 0; // Bytes für Breakdown rechnen $totalBytes = (int)$totalKb * 1024; $freeBytes = (int)$availKb * 1024; $usedBytes = max(0, $totalBytes - $freeBytes); // ── 2) Mails: bevorzugt aus Cache (mail:update-stats) ──────────────── [$mailUsersBytes, $mailSystemBytes] = $this->readMailTotals(); // ── 3) Backups schnell messen ──────────────────────────────────────── $bytesBackup = $this->duBytesDir('/var/backups/mailwolt'); // ── 4) Breakdown auflösen und konsistent machen ───────────────────── // alles, was nicht Mails/Backups ist, dem System zuordnen $bytesMails = max(0, (int)$mailUsersBytes); // nur user_mail in "Mails" $bytesSystem = max(0, $usedBytes - ($bytesMails + $bytesBackup)); // Wenn Vorberechnung system_mail existiert, zähle sie explizit zum System. $bytesSystem += max(0, (int)$mailSystemBytes); // negativ verhindern (Messrauschen) if ($bytesSystem < 0) { $bytesSystem = 0; } return [ 'device' => $device ?: 'unknown', 'mount' => $mount ?: $target, 'total_gb' => $totalGb, 'used_gb' => $usedGb, 'free_gb' => $freeGb, 'reserve5_gb' => $res5Gb, 'free_plus_reserve_gb' => $freePlusReserveGb, 'percent_used_total' => $percentUsed, 'breakdown_bytes' => [ 'system' => $bytesSystem, 'mails' => $bytesMails, 'backup' => $bytesBackup, ], ]; } /** * Liest vorberechnete Mail-Summen aus settings: mail.totals. * Fallback: wenn nicht vorhanden/zu alt, ermittelt Mails per du (nur dann). * * @return array{0:int,1:int} [users_bytes, system_bytes] */ private function readMailTotals(): array { $totals = (array)(Setting::get(self::MAILBOX_TOTALS_KEY, []) ?: []); $ts = isset($totals['updated_at']) ? strtotime((string)$totals['updated_at']) : null; $fresh = $ts && (time() - $ts) <= self::MAILBOX_TOTALS_STALE; if ($fresh) { $users = (int)($totals['users_bytes'] ?? 0); $system = (int)($totals['system_bytes'] ?? 0); return [max(0, $users), max(0, $system)]; } // Fallback EINMAL: grob mails via detectMailRoot() messen $root = $this->detectMailRoot(); $usersBytes = $root ? $this->duBytesDir($root) : 0; return [max(0, $usersBytes), 0]; } /** * Versucht, das Wurzelverzeichnis der Maildaten zu finden (Dovecot/Postfix), * ohne auf konkrete Setups festgenagelt zu sein. */ private function detectMailRoot(): ?string { // Dovecot bevorzugt $ml = trim((string)@shell_exec('doveconf -n 2>/dev/null | awk -F= \'/^mail_location/ {print $2}\'')); if ($ml !== '') { if (preg_match('~^(?:maildir|mdbox|sdbox):([^%]+)~i', $ml, $m)) { $root = rtrim($m[1]); foreach (['/Maildir', '/mdbox', '/sdbox'] as $suffix) { if (str_ends_with($root, $suffix)) { $root = dirname($root); break; } } if (is_dir($root)) return $root; } } // Postfix-Konfiguration $vmb = trim((string)@shell_exec('postconf -n 2>/dev/null | awk -F= \'/^virtual_mailbox_base/ {print $2}\'')); if ($vmb !== '' && is_dir($vmb)) return $vmb; // Fallbacks foreach (['/var/vmail', '/var/mail/vhosts', '/srv/mail/vhosts', '/home/vmail'] as $cand) { if (is_dir($cand)) return $cand; } return null; } /** Summe in Bytes für ein Verzeichnisbaum; robust & schnell genug. */ private function duBytesDir(string $path): int { if (!is_dir($path)) return 0; $out = @shell_exec('LC_ALL=C du -sb --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1'); return max(0, (int)trim((string)$out)); } } //namespace App\Console\Commands; // //use Illuminate\Console\Command; //use App\Models\Setting; // //class StorageProbe extends Command //{ // protected $signature = 'health:probe-disk {target=/}'; // protected $description = 'Speichert Storage-Werte (inkl. Breakdown) in settings:health.disk'; // // public function handle(): int // { // $target = $this->argument('target') ?: '/'; // $data = $this->probe($target); // // Setting::set('health.disk', $data); // Setting::set('health.disk_updated_at', now()->toIso8601String()); // // $hb = function (int $bytes): string { // $b = max(0, $bytes); // if ($b >= 1024**3) return number_format($b / 1024**3, 1).' GB'; // if ($b >= 1024**2) return number_format($b / 1024**2, 2).' MiB'; // if ($b >= 1024) return number_format($b / 1024, 0).' KiB'; // return $b.' B'; // }; // // $this->info(sprintf( // 'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d) breakdown: system=%s mails=%s backups=%s', // $data['mount'], // $data['total_gb'], // $data['used_gb'], // $data['free_gb'], // $data['free_plus_reserve_gb'], // $data['percent_used_total'], // $hb((int)$data['breakdown_bytes']['system']), // $hb((int)$data['breakdown_bytes']['mails']), // $hb((int)$data['breakdown_bytes']['backup']), // )); // return self::SUCCESS; // } // // private function detectMailRoot(): ?string // { // // Dovecot // $ml = trim((string) @shell_exec('doveconf -n 2>/dev/null | awk -F= \'/^mail_location/ {print $2}\'')); // if ($ml !== '') { // if (preg_match('~^(?:maildir|mdbox|sdbox):([^%]+)~i', $ml, $m)) { // $root = rtrim($m[1]); // foreach (['/Maildir', '/mdbox', '/sdbox'] as $suffix) { // if (str_ends_with($root, $suffix)) { $root = dirname($root); break; } // } // if (is_dir($root)) return $root; // } // } // // Postfix // $vmb = trim((string) @shell_exec('postconf -n 2>/dev/null | awk -F= \'/^virtual_mailbox_base/ {print $2}\'')); // if ($vmb !== '' && is_dir($vmb)) return $vmb; // // // Fallbacks // foreach (['/var/vmail', '/var/mail/vhosts', '/srv/mail/vhosts', '/home/vmail'] as $cand) { // if (is_dir($cand)) return $cand; // } // return null; // } // // private function duBytesDir(string $path): int // { // if (!is_dir($path)) return 0; // $out = @shell_exec('LC_ALL=C du -sb --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1'); // return max(0, (int) trim((string) $out)); // } // // protected function probe(string $target): array // { // // --- df: Gesamtdaten des Filesystems (inkl. Reserve) ----------------- // $line = trim((string)@shell_exec('LC_ALL=C df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1')); // // $device = $mount = ''; // $totalKb = $usedKb = $availKb = 0; // // if ($line !== '') { // $p = preg_split('/\s+/', $line); // if (count($p) >= 6) { // $device = $p[0]; // $totalKb = (int)$p[1]; // TOTAL (inkl. Reserve) // $usedKb = (int)$p[2]; // Used // $availKb = (int)$p[3]; // Avail (User-sicht) // $mount = $p[5]; // } // } // // $toGiB_i = static fn($kb) => (int)round(max(0, (int)$kb) / (1024 * 1024)); // ganzzahlig (UI: Gesamt/Genutzt/Frei) // $toGiB_f = static fn($kb) => round(max(0, (int)$kb) / (1024 * 1024), 1); // eine Nachkommastelle (Breakdown/Legende) // // $totalGb = $toGiB_i($totalKb); // $freeGb = $toGiB_i($availKb); // user-verfügbar // $usedGb = max(0, $totalGb - $freeGb); // belegt inkl. Reserve // $res5Gb = (int)round($totalGb * 0.05); // 5% von Gesamt // $freePlusReserveGb = min($totalGb, $freeGb + $res5Gb); // $percentUsed = $totalGb > 0 ? (int)round($usedGb * 100 / $totalGb) : 0; // // $duBytes = function (string $path): int { // if (!is_dir($path)) return 0; // $b = (int) trim((string) @shell_exec( // 'LC_ALL=C du -sb --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1' // )); // return max(0, $b); // }; // // $mailRoot = $this->detectMailRoot(); // $bytesMails = $mailRoot ? $this->duBytesDir($mailRoot) : 0; // $bytesBackup = $duBytes('/var/backups/mailwolt'); // // $totalBytes = (int) $totalKb * 1024; // $freeBytes = (int) $availKb * 1024; // $usedBytes = max(0, $totalBytes - $freeBytes); // // $bytesSystem = max(0, $usedBytes - ($bytesMails + $bytesBackup)); // // return [ // 'device' => $device ?: 'unknown', // 'mount' => $mount ?: $target, // // 'total_gb' => $totalGb, // 'used_gb' => $usedGb, // inkl. Reserve // 'free_gb' => $freeGb, // User-sicht // 'reserve5_gb' => $res5Gb, // Info // 'free_plus_reserve_gb' => $freePlusReserveGb, // Anzeige „Frei“ // // 'percent_used_total' => $percentUsed, // // // Reale Breakdown-Werte // 'breakdown_bytes' => [ // 'system' => $bytesSystem, // 'mails' => $bytesMails, // 'backup' => $bytesBackup, // ], // ]; // } //} // //namespace App\Console\Commands; // //use Illuminate\Console\Command; //use App\Models\Setting; // //class StorageProbe extends Command //{ // protected $signature = 'health:probe-disk {target=/}'; // protected $description = 'Speichert Storage-Werte (inkl. Frei+5%) in settings:health.disk'; // // public function handle(): int // { // $target = $this->argument('target') ?: '/'; // $data = $this->probe($target); // // // Persistiert (DB + Redis) über dein Settings-Model // Setting::set('health.disk', $data); // Setting::set('health.disk_updated_at', now()->toIso8601String()); // // $this->info(sprintf( // 'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d)', // $data['mount'], // $data['total_gb'], // $data['used_gb'], // $data['free_gb'], // $data['free_plus_reserve_gb'], // $data['percent_used_total'], // )); // // return self::SUCCESS; // } // // protected function probe(string $target): array // { // $line = trim((string) @shell_exec('df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1')); // // $device = $mount = ''; // $totalKb = $usedKb = $availKb = 0; // // if ($line !== '') { // $p = preg_split('/\s+/', $line); // if (count($p) >= 6) { // $device = $p[0]; // $totalKb = (int) $p[1]; // TOTAL (inkl. Reserve) // $usedKb = (int) $p[2]; // Used // $availKb = (int) $p[3]; // Avail (User-sicht) // $mount = $p[5]; // } // } // // $toGiB = static fn($kb) => (int) round(max(0, (int)$kb) / (1024*1024)); // // $totalGb = $toGiB($totalKb); // $freeGb = $toGiB($availKb); // user-verfügbar // $usedGb = max(0, $totalGb - $freeGb); // belegt inkl. Reserve // $res5Gb = (int) round($totalGb * 0.05); // 5% von Gesamt // $freePlusReserveGb = min($totalGb, $freeGb + $res5Gb); // // $percentUsed = $totalGb > 0 ? (int) round($usedGb * 100 / $totalGb) : 0; // // return [ // 'device' => $device ?: 'unknown', // 'mount' => $mount ?: $target, // // 'total_gb' => $totalGb, // 'used_gb' => $usedGb, // inkl. Reserve // 'free_gb' => $freeGb, // User-sicht // 'reserve5_gb' => $res5Gb, // Info // 'free_plus_reserve_gb' => $freePlusReserveGb, // ← das willst du anzeigen // // 'percent_used_total' => $percentUsed, // fürs Donut (~15%) // 'breakdown' => [ // 'system_gb' => 5.2, // OS, App, Logs … // 'mails_gb' => 2.8, // /var/mail/vhosts // 'backup_gb' => 1.0, // /var/backups/mailwolt (oder wohin du sicherst) // ], // ]; // } //}