parent
3152dc94e2
commit
4645b168f7
|
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
@ -10,6 +11,11 @@ class StorageProbe extends Command
|
||||||
protected $signature = 'health:probe-disk {target=/}';
|
protected $signature = 'health:probe-disk {target=/}';
|
||||||
protected $description = 'Speichert Storage-Werte (inkl. Breakdown) in settings:health.disk';
|
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
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$target = $this->argument('target') ?: '/';
|
$target = $this->argument('target') ?: '/';
|
||||||
|
|
@ -18,14 +24,17 @@ class StorageProbe extends Command
|
||||||
Setting::set('health.disk', $data);
|
Setting::set('health.disk', $data);
|
||||||
Setting::set('health.disk_updated_at', now()->toIso8601String());
|
Setting::set('health.disk_updated_at', now()->toIso8601String());
|
||||||
|
|
||||||
|
// hübsche Konsole
|
||||||
$hb = function (int $bytes): string {
|
$hb = function (int $bytes): string {
|
||||||
$b = max(0, $bytes);
|
$b = max(0, $bytes);
|
||||||
if ($b >= 1024**3) return number_format($b / 1024**3, 1).' GB';
|
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 ** 2) return number_format($b / 1024 ** 2, 2) . ' MiB';
|
||||||
if ($b >= 1024) return number_format($b / 1024, 0).' KiB';
|
if ($b >= 1024) return number_format($b / 1024, 0) . ' KiB';
|
||||||
return $b.' B';
|
return $b . ' B';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$bd = $data['breakdown_bytes'] ?? ['system' => 0, 'mails' => 0, 'backup' => 0];
|
||||||
|
|
||||||
$this->info(sprintf(
|
$this->info(sprintf(
|
||||||
'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d) breakdown: system=%s mails=%s backups=%s',
|
'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d) breakdown: system=%s mails=%s backups=%s',
|
||||||
$data['mount'],
|
$data['mount'],
|
||||||
|
|
@ -34,107 +43,19 @@ class StorageProbe extends Command
|
||||||
$data['free_gb'],
|
$data['free_gb'],
|
||||||
$data['free_plus_reserve_gb'],
|
$data['free_plus_reserve_gb'],
|
||||||
$data['percent_used_total'],
|
$data['percent_used_total'],
|
||||||
$hb((int)$data['breakdown_bytes']['system']),
|
$hb((int)$bd['system']), $hb((int)$bd['mails']), $hb((int)$bd['backup'])
|
||||||
$hb((int)$data['breakdown_bytes']['mails']),
|
|
||||||
$hb((int)$data['breakdown_bytes']['backup']),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
// $this->info(sprintf(
|
|
||||||
// 'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d) breakdown: system=%.1fGB mails=%.1fGB backups=%.1fGB',
|
|
||||||
// $data['mount'],
|
|
||||||
// $data['total_gb'],
|
|
||||||
// $data['used_gb'],
|
|
||||||
// $data['free_gb'],
|
|
||||||
// $data['free_plus_reserve_gb'],
|
|
||||||
// $data['percent_used_total'],
|
|
||||||
// $data['breakdown_bytes']['system'],
|
|
||||||
// $data['breakdown_bytes']['mails'],
|
|
||||||
// $data['breakdown_bytes']['backup'],
|
|
||||||
// ));
|
|
||||||
|
|
||||||
return self::SUCCESS;
|
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// … oberhalb deiner probe()-Methode einfügen:
|
|
||||||
// private function detectMailRoot(): ?string
|
|
||||||
// {
|
|
||||||
// // 1) Dovecot: mail_location = maildir:/var/vmail/%d/%n/Maildir
|
|
||||||
// $ml = trim((string) @shell_exec('doveconf -n 2>/dev/null | awk -F= \'/^mail_location/ {print $2}\''));
|
|
||||||
// if ($ml !== '') {
|
|
||||||
// $ml = trim($ml);
|
|
||||||
// // maildir:/path/to/root/%d/%n/Maildir oder mdbox:/path/…/mdbox
|
|
||||||
// if (preg_match('~^(?:maildir|mdbox|sdbox):([^%]+)~i', $ml, $m)) {
|
|
||||||
// $root = rtrim($m[1]);
|
|
||||||
// // häufig endet es auf /Maildir oder /mdbox → ein Level hoch als Root nehmen
|
|
||||||
// foreach (['/Maildir', '/mdbox', '/sdbox'] as $suffix) {
|
|
||||||
// if (str_ends_with($root, $suffix)) {
|
|
||||||
// $root = dirname($root);
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if (is_dir($root)) return $root;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // 2) Postfix: virtual_mailbox_base = /var/vmail
|
|
||||||
// $vmb = trim((string) @shell_exec('postconf -n 2>/dev/null | awk -F= \'/^virtual_mailbox_base/ {print $2}\''));
|
|
||||||
// $vmb = $vmb !== '' ? trim($vmb) : '';
|
|
||||||
// if ($vmb !== '' && is_dir($vmb)) return $vmb;
|
|
||||||
//
|
|
||||||
// // 3) Fallbacks – nimm den ersten existierenden
|
|
||||||
// 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;
|
|
||||||
// // apparent-size zählt logisch (kleine Dateien werden nicht „weggerundet“)
|
|
||||||
// $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
|
protected function probe(string $target): array
|
||||||
{
|
{
|
||||||
// --- df: Gesamtdaten des Filesystems (inkl. Reserve) -----------------
|
// ── 1) df lesen (Gesamt/Frei inkl. Reserve) ──────────────────────────
|
||||||
$line = trim((string)@shell_exec('LC_ALL=C df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1'));
|
$line = trim((string)@shell_exec('LC_ALL=C df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1'));
|
||||||
|
|
||||||
$device = $mount = '';
|
$device = $mount = '';
|
||||||
$totalKb = $usedKb = $availKb = 0;
|
$totalKb = $usedKb = $availKb = 0;
|
||||||
|
|
||||||
if ($line !== '') {
|
if ($line !== '') {
|
||||||
$p = preg_split('/\s+/', $line);
|
$p = preg_split('/\s+/', $line);
|
||||||
if (count($p) >= 6) {
|
if (count($p) >= 6) {
|
||||||
|
|
@ -146,56 +67,265 @@ class StorageProbe extends Command
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$toGiB_i = static fn($kb) => (int)round(max(0, (int)$kb) / (1024 * 1024)); // ganzzahlig (UI: Gesamt/Genutzt/Frei)
|
$toGiB_i = static fn($kb) => (int)round(max(0, (int)$kb) / (1024 * 1024)); // Ganzzahl für Kopfzahlen
|
||||||
$toGiB_f = static fn($kb) => round(max(0, (int)$kb) / (1024 * 1024), 1); // eine Nachkommastelle (Breakdown/Legende)
|
|
||||||
|
|
||||||
$totalGb = $toGiB_i($totalKb);
|
$totalGb = $toGiB_i($totalKb);
|
||||||
$freeGb = $toGiB_i($availKb); // user-verfügbar
|
$freeGb = $toGiB_i($availKb);
|
||||||
$usedGb = max(0, $totalGb - $freeGb); // belegt inkl. Reserve
|
$usedGb = max(0, $totalGb - $freeGb);
|
||||||
$res5Gb = (int)round($totalGb * 0.05); // 5% von Gesamt
|
$res5Gb = (int)round($totalGb * 0.05);
|
||||||
$freePlusReserveGb = min($totalGb, $freeGb + $res5Gb);
|
$freePlusReserveGb = min($totalGb, $freeGb + $res5Gb);
|
||||||
$percentUsed = $totalGb > 0 ? (int)round($usedGb * 100 / $totalGb) : 0;
|
$percentUsed = $totalGb > 0 ? (int)round($usedGb * 100 / $totalGb) : 0;
|
||||||
|
|
||||||
$duBytes = function (string $path): int {
|
// Bytes für Breakdown rechnen
|
||||||
if (!is_dir($path)) return 0;
|
$totalBytes = (int)$totalKb * 1024;
|
||||||
$b = (int) trim((string) @shell_exec(
|
$freeBytes = (int)$availKb * 1024;
|
||||||
'LC_ALL=C du -sb --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1'
|
$usedBytes = max(0, $totalBytes - $freeBytes);
|
||||||
));
|
|
||||||
return max(0, $b);
|
|
||||||
};
|
|
||||||
|
|
||||||
$mailRoot = $this->detectMailRoot();
|
// ── 2) Mails: bevorzugt aus Cache (mail:update-stats) ────────────────
|
||||||
$bytesMails = $mailRoot ? $this->duBytesDir($mailRoot) : 0;
|
[$mailUsersBytes, $mailSystemBytes] = $this->readMailTotals();
|
||||||
$bytesBackup = $duBytes('/var/backups/mailwolt');
|
|
||||||
|
|
||||||
$totalBytes = (int) $totalKb * 1024;
|
// ── 3) Backups schnell messen ────────────────────────────────────────
|
||||||
$freeBytes = (int) $availKb * 1024;
|
$bytesBackup = $this->duBytesDir('/var/backups/mailwolt');
|
||||||
$usedBytes = max(0, $totalBytes - $freeBytes);
|
|
||||||
|
|
||||||
|
// ── 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));
|
$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 [
|
return [
|
||||||
'device' => $device ?: 'unknown',
|
'device' => $device ?: 'unknown',
|
||||||
'mount' => $mount ?: $target,
|
'mount' => $mount ?: $target,
|
||||||
|
|
||||||
'total_gb' => $totalGb,
|
'total_gb' => $totalGb,
|
||||||
'used_gb' => $usedGb, // inkl. Reserve
|
'used_gb' => $usedGb,
|
||||||
'free_gb' => $freeGb, // User-sicht
|
'free_gb' => $freeGb,
|
||||||
'reserve5_gb' => $res5Gb, // Info
|
'reserve5_gb' => $res5Gb,
|
||||||
'free_plus_reserve_gb' => $freePlusReserveGb, // Anzeige „Frei“
|
'free_plus_reserve_gb' => $freePlusReserveGb,
|
||||||
|
|
||||||
'percent_used_total' => $percentUsed,
|
'percent_used_total' => $percentUsed,
|
||||||
|
|
||||||
// Reale Breakdown-Werte
|
|
||||||
'breakdown_bytes' => [
|
'breakdown_bytes' => [
|
||||||
'system' => $bytesSystem,
|
'system' => $bytesSystem,
|
||||||
'mails' => $bytesMails,
|
'mails' => $bytesMails,
|
||||||
'backup' => $bytesBackup,
|
'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,
|
||||||
|
// ],
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -15,39 +15,119 @@ class UpdateMailboxStats extends Command
|
||||||
protected $signature = 'mail:update-stats {--user=}';
|
protected $signature = 'mail:update-stats {--user=}';
|
||||||
protected $description = 'Aktualisiert Quota & Nachrichtenzahl (Settings/Redis; ohne DB-Spalten).';
|
protected $description = 'Aktualisiert Quota & Nachrichtenzahl (Settings/Redis; ohne DB-Spalten).';
|
||||||
|
|
||||||
|
// public function handle(): int
|
||||||
|
// {
|
||||||
|
// $log = Log::channel('mailstats');
|
||||||
|
// $onlyUser = trim((string)$this->option('user')) ?: null;
|
||||||
|
// $t0 = microtime(true);
|
||||||
|
//
|
||||||
|
// // Basis-Query: nur aktive, keine System-Mailboxen und keine System-Domains
|
||||||
|
// $base = MailUser::query()
|
||||||
|
// ->select(['id', 'domain_id', 'localpart', 'email', 'is_active', 'is_system'])
|
||||||
|
// ->with(['domain:id,domain,is_system'])
|
||||||
|
// ->where('is_active', true)
|
||||||
|
// ->where('is_system', false)
|
||||||
|
// ->whereHas('domain', fn($d) => $d->where('is_system', false));
|
||||||
|
//
|
||||||
|
// if ($onlyUser) {
|
||||||
|
// $base->where('email', $onlyUser);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $checked = 0;
|
||||||
|
// $changed = 0;
|
||||||
|
//
|
||||||
|
// $log->info('mail:update-stats START', ['only' => $onlyUser]);
|
||||||
|
//
|
||||||
|
// $base->orderBy('id')->chunkById(200, function ($users) use (&$checked, &$changed, $log) {
|
||||||
|
// foreach ($users as $u) {
|
||||||
|
// $checked++;
|
||||||
|
//
|
||||||
|
// // Email robust bestimmen (raw -> accessor -> zusammengesetzt)
|
||||||
|
// $raw = (string)($u->getRawOriginal('email') ?? '');
|
||||||
|
// $email = $raw !== '' ? $raw : ($u->email ?? $u->address ?? null);
|
||||||
|
//
|
||||||
|
// if (!is_string($email) || !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) {
|
||||||
|
// // still – kein Log-Spam
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// [$local, $domain] = explode('@', $email, 2);
|
||||||
|
// $maildir = "/var/mail/vhosts/{$domain}/{$local}";
|
||||||
|
//
|
||||||
|
// // Größe in Bytes (rekursiv)
|
||||||
|
// $usedBytes = 0;
|
||||||
|
// if (is_dir($maildir)) {
|
||||||
|
// $it = new RecursiveIteratorIterator(
|
||||||
|
// new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS)
|
||||||
|
// );
|
||||||
|
// foreach ($it as $f) {
|
||||||
|
// if ($f->isFile()) $usedBytes += $f->getSize();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Message-Count
|
||||||
|
// $messageCount = $this->countViaDoveadm($email);
|
||||||
|
// if ($messageCount === null) {
|
||||||
|
// $messageCount = $this->countViaFilesystem($maildir);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $key = "mailbox.{$email}";
|
||||||
|
// $prev = (array)(Setting::get($key, []) ?: []);
|
||||||
|
// $new = [
|
||||||
|
// 'used_bytes' => (int)$usedBytes,
|
||||||
|
// 'message_count' => (int)$messageCount,
|
||||||
|
// 'updated_at' => now()->toDateTimeString(),
|
||||||
|
// ];
|
||||||
|
//
|
||||||
|
// if (($prev['used_bytes'] ?? null) !== $new['used_bytes']
|
||||||
|
// || ($prev['message_count'] ?? null) !== $new['message_count']) {
|
||||||
|
// Setting::set($key, $new);
|
||||||
|
// $changed++;
|
||||||
|
//
|
||||||
|
// // kurze Ausgabe & Info-Log NUR bei Änderung
|
||||||
|
// $this->line(sprintf("%-35s %7.1f MiB %5d msgs",
|
||||||
|
// $email, $usedBytes / 1048576, $messageCount));
|
||||||
|
// $log->info('updated', ['email' => $email, 'used_bytes' => $new['used_bytes'], 'message_count' => $new['message_count']]);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// $ms = (int)((microtime(true) - $t0) * 1000);
|
||||||
|
// $log->info('mail:update-stats DONE', compact('checked', 'changed', 'ms'));
|
||||||
|
// $this->info('Mailbox-Statistiken aktualisiert.');
|
||||||
|
// return self::SUCCESS;
|
||||||
|
// }
|
||||||
|
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$log = Log::channel('mailstats');
|
$log = Log::channel('mailstats');
|
||||||
$onlyUser = trim((string)$this->option('user')) ?: null;
|
$onlyUser = trim((string)$this->option('user')) ?: null;
|
||||||
$t0 = microtime(true);
|
$t0 = microtime(true);
|
||||||
|
|
||||||
// Basis-Query: nur aktive, keine System-Mailboxen und keine System-Domains
|
// Summen
|
||||||
|
$sumUserBytes = 0;
|
||||||
|
$sumSystemBytes = 0;
|
||||||
|
|
||||||
|
// aktiver Benutzerbestand (inkl. Domains, um system/non-system zu unterscheiden)
|
||||||
$base = MailUser::query()
|
$base = MailUser::query()
|
||||||
->select(['id', 'domain_id', 'localpart', 'email', 'is_active', 'is_system'])
|
->select(['id','domain_id','localpart','email','is_active','is_system'])
|
||||||
->with(['domain:id,domain,is_system'])
|
->with(['domain:id,domain,is_system'])
|
||||||
->where('is_active', true)
|
->where('is_active', true);
|
||||||
->where('is_system', false)
|
|
||||||
->whereHas('domain', fn($d) => $d->where('is_system', false));
|
|
||||||
|
|
||||||
if ($onlyUser) {
|
if ($onlyUser) {
|
||||||
$base->where('email', $onlyUser);
|
$base->where('email', $onlyUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
$checked = 0;
|
$checked = 0; $changed = 0;
|
||||||
$changed = 0;
|
|
||||||
|
|
||||||
$log->info('mail:update-stats START', ['only' => $onlyUser]);
|
$log->info('mail:update-stats START', ['only' => $onlyUser]);
|
||||||
|
|
||||||
$base->orderBy('id')->chunkById(200, function ($users) use (&$checked, &$changed, $log) {
|
$base->orderBy('id')->chunkById(200, function ($users) use (&$checked,&$changed,&$sumUserBytes,&$sumSystemBytes,$log) {
|
||||||
foreach ($users as $u) {
|
foreach ($users as $u) {
|
||||||
$checked++;
|
$checked++;
|
||||||
|
|
||||||
// Email robust bestimmen (raw -> accessor -> zusammengesetzt)
|
|
||||||
$raw = (string)($u->getRawOriginal('email') ?? '');
|
$raw = (string)($u->getRawOriginal('email') ?? '');
|
||||||
$email = $raw !== '' ? $raw : ($u->email ?? $u->address ?? null);
|
$email = $raw !== '' ? $raw : ($u->email ?? $u->address ?? null);
|
||||||
|
|
||||||
if (!is_string($email) || !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) {
|
if (!is_string($email) || !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) {
|
||||||
// still – kein Log-Spam
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,47 +137,59 @@ class UpdateMailboxStats extends Command
|
||||||
// Größe in Bytes (rekursiv)
|
// Größe in Bytes (rekursiv)
|
||||||
$usedBytes = 0;
|
$usedBytes = 0;
|
||||||
if (is_dir($maildir)) {
|
if (is_dir($maildir)) {
|
||||||
$it = new RecursiveIteratorIterator(
|
$it = new \RecursiveIteratorIterator(
|
||||||
new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS)
|
new \RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS)
|
||||||
);
|
);
|
||||||
foreach ($it as $f) {
|
foreach ($it as $f) if ($f->isFile()) $usedBytes += $f->getSize();
|
||||||
if ($f->isFile()) $usedBytes += $f->getSize();
|
}
|
||||||
}
|
|
||||||
|
$isSystemDomain = (bool)($u->domain->is_system ?? false);
|
||||||
|
|
||||||
|
if ($isSystemDomain || $u->is_system) {
|
||||||
|
// systemische Mailboxen fließen nur in die System-Summe ein
|
||||||
|
$sumSystemBytes += $usedBytes;
|
||||||
|
continue; // KEIN per-user Setting
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message-Count
|
// Message-Count
|
||||||
$messageCount = $this->countViaDoveadm($email);
|
$messageCount = $this->countViaDoveadm($email);
|
||||||
if ($messageCount === null) {
|
if ($messageCount === null) $messageCount = $this->countViaFilesystem($maildir);
|
||||||
$messageCount = $this->countViaFilesystem($maildir);
|
|
||||||
}
|
|
||||||
|
|
||||||
$key = "mailbox.{$email}";
|
$sumUserBytes += $usedBytes;
|
||||||
|
|
||||||
|
$key = "mailbox.{$email}";
|
||||||
$prev = (array)(Setting::get($key, []) ?: []);
|
$prev = (array)(Setting::get($key, []) ?: []);
|
||||||
$new = [
|
$new = [
|
||||||
'used_bytes' => (int)$usedBytes,
|
'used_bytes' => (int)$usedBytes,
|
||||||
'message_count' => (int)$messageCount,
|
'message_count' => (int)$messageCount,
|
||||||
'updated_at' => now()->toDateTimeString(),
|
'updated_at' => now()->toDateTimeString(),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (($prev['used_bytes'] ?? null) !== $new['used_bytes']
|
if (($prev['used_bytes'] ?? null) !== $new['used_bytes']
|
||||||
|| ($prev['message_count'] ?? null) !== $new['message_count']) {
|
|| ($prev['message_count'] ?? null) !== $new['message_count']) {
|
||||||
Setting::set($key, $new);
|
Setting::set($key, $new);
|
||||||
$changed++;
|
$changed++;
|
||||||
|
$this->line(sprintf("%-35s %7.2f MiB %5d msgs",
|
||||||
// kurze Ausgabe & Info-Log NUR bei Änderung
|
|
||||||
$this->line(sprintf("%-35s %7.1f MiB %5d msgs",
|
|
||||||
$email, $usedBytes / 1048576, $messageCount));
|
$email, $usedBytes / 1048576, $messageCount));
|
||||||
$log->info('updated', ['email' => $email, 'used_bytes' => $new['used_bytes'], 'message_count' => $new['message_count']]);
|
$log->info('updated', ['email'=>$email]+$new);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$ms = (int)((microtime(true) - $t0) * 1000);
|
// Totals persistieren (Nutzen wir später im StorageProbe)
|
||||||
$log->info('mail:update-stats DONE', compact('checked', 'changed', 'ms'));
|
Setting::set('mailbox.totals', [
|
||||||
|
'users_bytes' => (int)$sumUserBytes, // alle nicht-systemischen Mailboxen
|
||||||
|
'system_bytes' => (int)$sumSystemBytes, // systemische Mailboxen
|
||||||
|
'updated_at' => now()->toIso8601String(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ms = (int)((microtime(true)-$t0)*1000);
|
||||||
|
$log->info('mail:update-stats DONE', compact('checked','changed','ms','sumUserBytes','sumSystemBytes'));
|
||||||
$this->info('Mailbox-Statistiken aktualisiert.');
|
$this->info('Mailbox-Statistiken aktualisiert.');
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private function countViaDoveadm(string $email): ?int
|
private function countViaDoveadm(string $email): ?int
|
||||||
{
|
{
|
||||||
$cmd = "sudo -n -u vmail /usr/bin/doveadm -f tab mailbox status -u "
|
$cmd = "sudo -n -u vmail /usr/bin/doveadm -f tab mailbox status -u "
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue