Files
fotospiel-app/app/Services/Storage/EventStorageManager.php
Codex Agent 5817270c35 Admin Menü neu geordnet.
Introduced a two-tier media pipeline with dynamic disks, asset tracking, admin controls, and alerting around
  upload/archival workflows.
  - Added storage metadata + asset tables and models so every photo/variant knows where it lives
 (database/migrations/2025_10_20_090000_create_media_storage_targets_table.php, database/  migrations/2025_10_20_090200_create_event_media_assets_table.php, app/Models/MediaStorageTarget.php:1, app/
    Models/EventMediaAsset.php:1, app/Models/EventStorageAssignment.php:1, app/Models/Event.php:27).
  - Rewired guest and tenant uploads to pick the event’s hot disk, persist EventMediaAsset records, compute
    checksums, and clean up on delete (app/Http/Controllers/Api/EventPublicController.php:243, app/Http/
Controllers/Api/Tenant/PhotoController.php:25, app/Models/Photo.php:25).
  - Implemented storage services, archival job scaffolding, monitoring config, and queue-failure notifications for upload issues (app/Services/Storage/EventStorageManager.php:16, app/Services/Storage/
    StorageHealthService.php:9, app/Jobs/ArchiveEventMediaAssets.php:16, app/Providers/AppServiceProvider.php:39, app/Notifications/UploadPipelineFailed.php:8, config/storage-monitor.php:1).
  - Seeded default hot/cold targets and exposed super-admin tooling via a Filament resource and capacity widget (database/seeders/MediaStorageTargetSeeder.php:13, database/seeders/DatabaseSeeder.php:17, app/Filament/Resources/MediaStorageTargetResource.php:1, app/Filament/Widgets/StorageCapacityWidget.php:12, app/Providers/Filament/SuperAdminPanelProvider.php:47).
- Dropped cron skeletons and artisan placeholders to schedule storage monitoring, archival dispatch, and upload queue health checks (cron/storage_monitor.sh, cron/archive_dispatcher.sh, cron/upload_queue_health.sh, routes/console.php:9).
2025-10-17 22:26:13 +02:00

159 lines
4.2 KiB
PHP

<?php
namespace App\Services\Storage;
use App\Models\Event;
use App\Models\EventMediaAsset;
use App\Models\EventStorageAssignment;
use App\Models\MediaStorageTarget;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class EventStorageManager
{
public function getHotDiskForEvent(Event $event): string
{
$target = $event->currentStorageTarget('hot') ?? $this->resolveDefaultHotTarget();
return $target?->key ?? Config::get('filesystems.default', 'local');
}
public function getArchiveDiskForEvent(Event $event): ?string
{
$assignment = $event->storageAssignments()
->where('role', 'archive')
->where('status', 'active')
->latest('assigned_at')
->first();
if ($assignment?->storageTarget) {
return $assignment->storageTarget->key;
}
$default = $this->resolveDefaultArchiveTarget();
if (! $default) {
return null;
}
$assignment = $this->ensureAssignment($event, $default, 'archive');
return $assignment->storageTarget?->key;
}
public function ensureAssignment(Event $event, ?MediaStorageTarget $target = null, string $role = 'hot'): EventStorageAssignment
{
if (! $target) {
$target = $role === 'archive'
? $this->resolveDefaultArchiveTarget()
: $this->resolveDefaultHotTarget();
}
$assignment = $event->storageAssignments()
->where('role', $role)
->where('status', 'active')
->where('media_storage_target_id', $target?->id)
->latest('assigned_at')
->first();
if ($assignment) {
return $assignment;
}
if (! $target) {
throw new \RuntimeException('No storage target available for role '.$role);
}
$assignment = $event->storageAssignments()->create([
'media_storage_target_id' => $target->id,
'role' => $role,
'status' => 'active',
'assigned_at' => now(),
]);
return $assignment;
}
public function recordAsset(
Event $event,
string $disk,
string $path,
array $attributes = [],
?EventMediaAsset $existing = null,
): EventMediaAsset {
$target = MediaStorageTarget::where('key', $disk)->first();
if (! $target) {
$target = $this->resolveDefaultHotTarget();
}
$payload = array_merge([
'event_id' => $event->id,
'media_storage_target_id' => $target?->id,
'disk' => $disk,
'path' => $path,
'status' => 'hot',
'processed_at' => now(),
], $attributes);
if ($existing) {
$existing->fill($payload)->save();
return $existing;
}
return EventMediaAsset::create($payload);
}
public function registerDynamicDisks(): void
{
if (! $this->targetsTableExists()) {
return;
}
$targets = MediaStorageTarget::active()->get();
foreach ($targets as $target) {
$config = $target->toFilesystemConfig();
Config::set('filesystems.disks.'.$target->key, $config);
}
}
protected function resolveDefaultHotTarget(): ?MediaStorageTarget
{
return MediaStorageTarget::active()
->where('is_hot', true)
->orderByDesc('is_default')
->orderByDesc('priority')
->first();
}
protected function resolveDefaultArchiveTarget(): ?MediaStorageTarget
{
return MediaStorageTarget::active()
->where('is_hot', false)
->orderByDesc('priority')
->first();
}
protected function targetsTableExists(): bool
{
static $cached;
if ($cached !== null) {
return $cached;
}
try {
$cached = \Schema::hasTable('media_storage_targets');
} catch (\Throwable $e) {
Log::debug('Skipping storage target bootstrap: '.$e->getMessage());
$cached = false;
}
return $cached;
}
}