mailwolt/app/Console/Commands/StorageProbe.php

411 lines
16 KiB
PHP

<?php
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';
// Quelle für vorberechnete Mail-Summen (kommt aus mail:update-stats)
private const MAILBOX_TOTALS_KEY = 'mailbox.totals';
// Wie lange dürfen Mail-Summen alt sein, bevor wir auf du-Fallback gehen (Sekunden)
private const MAILBOX_TOTALS_STALE = 900; // 15 Min
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());
// 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)
// ],
// ];
// }
//}