Alexander Musikhin 12 часов назад
Родитель
Сommit
879c057749

+ 3 - 0
.env.example

@@ -39,6 +39,9 @@ GENERATED_DOCUMENTS_RETENTION_DAYS=14
 IMPORT_FILES_RETENTION_DAYS=14
 GENERATED_DOCUMENTS_CLEANUP_ENABLED=true
 GENERATED_DOCUMENTS_CLEANUP_TIME=03:30
+DISK_SPACE_WARNING_PATH=/var/www/storage
+DISK_SPACE_WARNING_THRESHOLD_GB=1
+DISK_SPACE_WARNING_CACHE_SECONDS=300
 
 BROADCAST_DRIVER=redis
 CACHE_DRIVER=redis

+ 9 - 0
app/Providers/AppServiceProvider.php

@@ -4,8 +4,10 @@ namespace App\Providers;
 
 use App\Models\SparePartOrder;
 use App\Observers\SparePartOrderObserver;
+use App\Services\DiskSpaceMonitor;
 use Illuminate\Pagination\Paginator;
 use Illuminate\Support\Facades\URL;
+use Illuminate\Support\Facades\View;
 use Illuminate\Support\ServiceProvider;
 
 class AppServiceProvider extends ServiceProvider
@@ -30,5 +32,12 @@ class AppServiceProvider extends ServiceProvider
 
         // Регистрация Observer для автоматической обработки дефицитов
         SparePartOrder::observe(SparePartOrderObserver::class);
+
+        View::composer('layouts.app', function ($view): void {
+            $diskSpaceMonitor = app(DiskSpaceMonitor::class);
+
+            $view->with('diskSpaceStatus', $diskSpaceMonitor->status());
+            $view->with('diskSpaceMonitor', $diskSpaceMonitor);
+        });
     }
 }

+ 81 - 0
app/Services/DiskSpaceMonitor.php

@@ -0,0 +1,81 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Cache;
+use Throwable;
+
+class DiskSpaceMonitor
+{
+    private const CACHE_KEY = 'disk_space_monitor.status';
+
+    /**
+     * @return array{warning: bool, free_bytes: int|null, threshold_bytes: int, path: string, checked_at: string|null}
+     */
+    public function status(): array
+    {
+        try {
+            return Cache::remember(
+                self::CACHE_KEY,
+                now()->addSeconds($this->cacheSeconds()),
+                fn (): array => $this->freshStatus()
+            );
+        } catch (Throwable) {
+            return $this->freshStatus();
+        }
+    }
+
+    public function formatBytes(?int $bytes): string
+    {
+        if ($bytes === null) {
+            return 'неизвестно';
+        }
+
+        $units = ['Б', 'КБ', 'МБ', 'ГБ', 'ТБ'];
+        $value = (float) $bytes;
+        $unitIndex = 0;
+
+        while ($value >= 1024 && $unitIndex < count($units) - 1) {
+            $value /= 1024;
+            $unitIndex++;
+        }
+
+        return sprintf('%s %s', round($value, $unitIndex === 0 ? 0 : 2), $units[$unitIndex]);
+    }
+
+    /**
+     * @return array{warning: bool, free_bytes: int|null, threshold_bytes: int, path: string, checked_at: string|null}
+     */
+    private function freshStatus(): array
+    {
+        $path = $this->path();
+        $thresholdBytes = $this->thresholdBytes();
+        $freeBytes = @disk_free_space($path);
+        $freeBytes = $freeBytes === false ? null : (int) $freeBytes;
+
+        return [
+            'warning' => $freeBytes !== null && $freeBytes < $thresholdBytes,
+            'free_bytes' => $freeBytes,
+            'threshold_bytes' => $thresholdBytes,
+            'path' => $path,
+            'checked_at' => now()->toDateTimeString(),
+        ];
+    }
+
+    private function path(): string
+    {
+        return (string) config('app.disk_space_warning.path', storage_path());
+    }
+
+    private function thresholdBytes(): int
+    {
+        return max(0, (int) config('app.disk_space_warning.threshold_bytes', 1024 * 1024 * 1024));
+    }
+
+    private function cacheSeconds(): int
+    {
+        return max(1, (int) config('app.disk_space_warning.cache_seconds', 300));
+    }
+}

+ 9 - 0
config/app.php

@@ -129,4 +129,13 @@ return [
 
     'default_maf_order_user_id' => env('APP_DEFAULT_MAF_ORDER_USER_ID', 1),
 
+    'disk_space_warning' => [
+        'path' => env('DISK_SPACE_WARNING_PATH', storage_path()),
+        'threshold_bytes' => (int) env(
+            'DISK_SPACE_WARNING_THRESHOLD_BYTES',
+            (int) env('DISK_SPACE_WARNING_THRESHOLD_GB', 1) * 1024 * 1024 * 1024
+        ),
+        'cache_seconds' => (int) env('DISK_SPACE_WARNING_CACHE_SECONDS', 300),
+    ],
+
 ];

+ 10 - 0
resources/sass/app.scss

@@ -677,6 +677,16 @@ td p {
   background: #dc3545;
 }
 
+.disk-space-warning {
+  width: 100%;
+  padding: 0.75rem 1rem;
+  background: #dc3545;
+  color: #fff;
+  font-size: 0.95rem;
+  line-height: 1.35;
+  text-align: center;
+}
+
 .notifications-list {
   display: flex;
   flex-direction: column;

+ 8 - 0
resources/views/layouts/app.blade.php

@@ -78,6 +78,14 @@
     </div>
 
     <div id="app">
+        @if(($diskSpaceStatus['warning'] ?? false) === true)
+            <div class="disk-space-warning" role="alert">
+                <strong>Внимание: заканчивается место на диске.</strong>
+                Доступно {{ $diskSpaceMonitor->formatBytes($diskSpaceStatus['free_bytes'] ?? null) }}
+                из минимально необходимых {{ $diskSpaceMonitor->formatBytes($diskSpaceStatus['threshold_bytes'] ?? null) }}.
+            </div>
+        @endif
+
         <nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
             <div class="container-fluid">
                 <a class="navbar-brand" href="{{ url('/') }}">

+ 50 - 0
tests/Unit/Services/DiskSpaceMonitorTest.php

@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Tests\Unit\Services;
+
+use App\Services\DiskSpaceMonitor;
+use Illuminate\Support\Facades\Cache;
+use Tests\TestCase;
+
+class DiskSpaceMonitorTest extends TestCase
+{
+    public function testItWarnsWhenFreeSpaceIsBelowThreshold(): void
+    {
+        Cache::forget('disk_space_monitor.status');
+        config([
+            'app.disk_space_warning.path' => storage_path(),
+            'app.disk_space_warning.threshold_bytes' => PHP_INT_MAX,
+            'app.disk_space_warning.cache_seconds' => 300,
+        ]);
+
+        $status = app(DiskSpaceMonitor::class)->status();
+
+        $this->assertTrue($status['warning']);
+        $this->assertNotNull($status['free_bytes']);
+    }
+
+    public function testItDoesNotWarnWhenFreeSpaceIsAboveThreshold(): void
+    {
+        Cache::forget('disk_space_monitor.status');
+        config([
+            'app.disk_space_warning.path' => storage_path(),
+            'app.disk_space_warning.threshold_bytes' => 1,
+            'app.disk_space_warning.cache_seconds' => 300,
+        ]);
+
+        $status = app(DiskSpaceMonitor::class)->status();
+
+        $this->assertFalse($status['warning']);
+        $this->assertNotNull($status['free_bytes']);
+    }
+
+    public function testItFormatsBytes(): void
+    {
+        $monitor = new DiskSpaceMonitor();
+
+        $this->assertSame('1 ГБ', $monitor->formatBytes(1024 * 1024 * 1024));
+        $this->assertSame('неизвестно', $monitor->formatBytes(null));
+    }
+}