- Event-Medien tragen Typ (image/video/audio), Dauer und Notiz; Videos bekommen ein Poster, Audio wird per ffmpeg nach AAC (audio/mp4) gewandelt, Originale werden mit Range-Unterstützung gestreamt. - Upload nimmt eine Notiz (caption) direkt mit, PATCH ändert sie später. - Neue Event-Felder: Zeitraum (end_date), Kategorie, Personen, Meilenstein. Regeln zentral in EventFields, damit CRUD und Sync gleich prüfen. - Titel darf leer sein: ein Event entsteht mit dem ersten Eintrag. - Artisan-Befehl media:transcode-voice-notes wandelt alte WebM/Ogg-Notizen, die Safari nicht abspielen kann. - Login-Throttling, Settings-Validierung, Sync-Fehlercodes ohne Rohtext. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
91 lines
2.4 KiB
PHP
91 lines
2.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Event;
|
|
use App\Models\EventMedia;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
/**
|
|
* Removes media files for every deletion path.
|
|
*
|
|
* Database rows disappear on their own through `cascadeOnDelete`, but the
|
|
* files behind them do not. Paths are therefore collected *before* the rows
|
|
* are deleted, and each deletion path routes through this service.
|
|
*/
|
|
class MediaCleaner
|
|
{
|
|
/**
|
|
* Delete the files of an event's media. The rows are left to the
|
|
* caller / database cascade.
|
|
*/
|
|
public function purgeEventMedia(Event $event): void
|
|
{
|
|
$this->deleteFiles($event->media()->get());
|
|
}
|
|
|
|
/**
|
|
* Delete the files of every media row belonging to a user, plus the
|
|
* user's settings background.
|
|
*/
|
|
public function purgeUserMedia(User $user): void
|
|
{
|
|
$this->deleteFiles(EventMedia::query()->where('user_id', $user->id)->get());
|
|
|
|
$this->delete('local', ["settings-media/{$user->id}/background.jpg"]);
|
|
}
|
|
|
|
/**
|
|
* Delete the files of a single media row.
|
|
*/
|
|
public function purgeMedia(EventMedia $media): void
|
|
{
|
|
$this->deleteFiles([$media]);
|
|
}
|
|
|
|
/**
|
|
* @param iterable<int, EventMedia> $mediaItems
|
|
*/
|
|
private function deleteFiles(iterable $mediaItems): void
|
|
{
|
|
$pathsByDisk = [];
|
|
|
|
foreach ($mediaItems as $media) {
|
|
$disk = $media->disk ?: 'local';
|
|
|
|
foreach ([$media->path, $media->preview_path, $media->thumbnail_path] as $path) {
|
|
if (is_string($path) && $path !== '') {
|
|
$pathsByDisk[$disk][] = $path;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($pathsByDisk as $disk => $paths) {
|
|
$this->delete($disk, $paths);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $paths
|
|
*/
|
|
private function delete(string $disk, array $paths): void
|
|
{
|
|
if ($paths === []) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
Storage::disk($disk)->delete($paths);
|
|
} catch (\Throwable $e) {
|
|
// A failed file delete must not abort the surrounding deletion —
|
|
// an orphaned file is recoverable, a half-deleted account is not.
|
|
Log::warning('Media cleanup failed', [
|
|
'disk' => $disk,
|
|
'paths' => $paths,
|
|
'exception' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
}
|