feat: add app and database modules
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ============================================================
|
||||
*
|
||||
* @project biiproject
|
||||
*
|
||||
* @author Andika Debi Putra
|
||||
*
|
||||
* @email andikadebiputra@gmail.com
|
||||
*
|
||||
* @website https://biiproject.com
|
||||
*
|
||||
* @copyright Copyright (c) 2026 Andika Debi Putra
|
||||
* @license Proprietary - All Rights Reserved
|
||||
*
|
||||
* @version 1.0.0
|
||||
*
|
||||
* @created 2026-05-01
|
||||
* ============================================================
|
||||
*
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file is strictly prohibited without prior written
|
||||
* permission from the author.
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Notifications\BackupFailedNotification;
|
||||
use App\Services\Notification\TelegramService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class VerifyBackups extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'backups:verify
|
||||
{--disk=local : Storage disk to check}
|
||||
{--max-age=7 : Alert if newest backup is older than N days}
|
||||
{--min-size=1024 : Alert if backup is smaller than N bytes}';
|
||||
|
||||
/**
|
||||
* The description of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Verify backups exist, are recent, and have non-trivial file sizes';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(TelegramService $telegram): int
|
||||
{
|
||||
$disk = $this->option('disk');
|
||||
$maxAgeDays = (int) $this->option('max-age');
|
||||
$minBytes = (int) $this->option('min-size');
|
||||
|
||||
$this->info("Verifying backups on disk: {$disk}");
|
||||
|
||||
$storage = Storage::disk($disk);
|
||||
|
||||
if (! $storage->exists('Laravel')) {
|
||||
$reason = 'Backup directory "Laravel" not found on disk.';
|
||||
$this->error($reason);
|
||||
$this->notifyFailure($reason, $disk, $telegram);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$files = collect($storage->allFiles('Laravel'))
|
||||
->filter(fn ($f) => Str::endsWith($f, ['.zip', '.gz', '.sql']))
|
||||
->sortByDesc(fn ($f) => $storage->lastModified($f))
|
||||
->values();
|
||||
|
||||
if ($files->isEmpty()) {
|
||||
$reason = 'No backup files found.';
|
||||
$this->error($reason);
|
||||
$this->notifyFailure($reason, $disk, $telegram);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$latest = $files->first();
|
||||
$latestModified = $storage->lastModified($latest);
|
||||
$latestSize = $storage->size($latest);
|
||||
$ageDays = (int) round((time() - $latestModified) / 86400);
|
||||
|
||||
$this->table(['File', 'Age (days)', 'Size (KB)', 'Status'], [[
|
||||
basename($latest),
|
||||
$ageDays,
|
||||
round($latestSize / 1024, 1),
|
||||
$this->statusLabel($ageDays, $latestSize, $maxAgeDays, $minBytes),
|
||||
]]);
|
||||
|
||||
$problems = [];
|
||||
|
||||
if ($ageDays > $maxAgeDays) {
|
||||
$problems[] = "Latest backup is {$ageDays} days old (threshold: {$maxAgeDays} days)";
|
||||
}
|
||||
|
||||
if ($latestSize < $minBytes) {
|
||||
$problems[] = "Latest backup is suspiciously small ({$latestSize} bytes)";
|
||||
}
|
||||
|
||||
if (! empty($problems)) {
|
||||
foreach ($problems as $problem) {
|
||||
$this->warn($problem);
|
||||
}
|
||||
$this->notifyFailure(implode('; ', $problems), $disk, $telegram);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info("✓ Backup verification passed. {$files->count()} backup(s) found.");
|
||||
Log::channel('single')->info('[VerifyBackups] OK', ['count' => $files->count(), 'age_days' => $ageDays]);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function statusLabel(int $age, int $size, int $maxAge, int $minBytes): string
|
||||
{
|
||||
if ($age > $maxAge || $size < $minBytes) {
|
||||
return '⚠ WARN';
|
||||
}
|
||||
|
||||
return '✓ OK';
|
||||
}
|
||||
|
||||
private function notifyFailure(string $reason, string $disk, TelegramService $telegram): void
|
||||
{
|
||||
Log::channel('single')->error('[VerifyBackups] FAILED', compact('reason', 'disk'));
|
||||
|
||||
// Prepare Telegram Alert
|
||||
$hostname = gethostname();
|
||||
$message = "🚨 <b>BACKUP VERIFICATION FAILED</b> 🚨\n";
|
||||
$message .= "--------------------------------\n";
|
||||
$message .= "<b>Host:</b> {$hostname}\n";
|
||||
$message .= "<b>Disk:</b> {$disk}\n";
|
||||
$message .= "<b>Reason:</b> {$reason}\n";
|
||||
$message .= "--------------------------------\n";
|
||||
$message .= '<i>Please check the backup logs immediately!</i>';
|
||||
|
||||
$telegram->sendMessage($message);
|
||||
|
||||
// Notify developer role via database notification
|
||||
try {
|
||||
$developers = User::role('Developer')->get();
|
||||
foreach ($developers as $dev) {
|
||||
$dev->notify(new BackupFailedNotification($reason));
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Silently skip if notification classes don't exist yet
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user