photo-upload und ansicht im admin gefixt
This commit is contained in:
@@ -2789,6 +2789,7 @@ class EventPublicController extends BaseController
|
|||||||
|
|
||||||
$photoId = DB::table('photos')->insertGetId([
|
$photoId = DB::table('photos')->insertGetId([
|
||||||
'event_id' => $eventId,
|
'event_id' => $eventId,
|
||||||
|
'tenant_id' => $tenantModel->id,
|
||||||
'task_id' => $validated['task_id'] ?? null,
|
'task_id' => $validated['task_id'] ?? null,
|
||||||
'guest_name' => $validated['guest_name'] ?? $deviceId,
|
'guest_name' => $validated['guest_name'] ?? $deviceId,
|
||||||
'file_path' => $url,
|
'file_path' => $url,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use Illuminate\Support\Facades\Log;
|
|||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class PhotoController extends Controller
|
class PhotoController extends Controller
|
||||||
@@ -136,6 +137,117 @@ class PhotoController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function asset(Request $request, Event $event, Photo $photo, string $variant): StreamedResponse|JsonResponse
|
||||||
|
{
|
||||||
|
if ($photo->event_id !== $event->id) {
|
||||||
|
return ApiError::response(
|
||||||
|
'photo_not_found',
|
||||||
|
'Foto nicht gefunden',
|
||||||
|
'Das Foto gehört nicht zu diesem Event.',
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
['photo_id' => $photo->id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$preferOriginals = (bool) ($event->settings['watermark_serve_originals'] ?? false);
|
||||||
|
[$disk, $path, $mime] = $this->resolvePhotoVariant($photo, $variant, $preferOriginals);
|
||||||
|
|
||||||
|
if (! $path) {
|
||||||
|
return ApiError::response(
|
||||||
|
'photo_unavailable',
|
||||||
|
'Photo Unavailable',
|
||||||
|
'The requested photo could not be loaded.',
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
[
|
||||||
|
'photo_id' => $photo->id,
|
||||||
|
'event_id' => $event->id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Str::startsWith($path, ['http://', 'https://'])) {
|
||||||
|
return redirect()->away($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$storage = Storage::disk($disk);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('Tenant photo asset disk unavailable', [
|
||||||
|
'event_id' => $event->id,
|
||||||
|
'photo_id' => $photo->id,
|
||||||
|
'disk' => $disk,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiError::response(
|
||||||
|
'photo_unavailable',
|
||||||
|
'Photo Unavailable',
|
||||||
|
'The requested photo could not be loaded.',
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
[
|
||||||
|
'photo_id' => $photo->id,
|
||||||
|
'event_id' => $event->id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->storagePathCandidates($path) as $candidate) {
|
||||||
|
if (! $storage->exists($candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stream = $storage->readStream($candidate);
|
||||||
|
if (! $stream) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = null;
|
||||||
|
try {
|
||||||
|
$size = $storage->size($candidate);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$size = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $mime) {
|
||||||
|
try {
|
||||||
|
$mime = $storage->mimeType($candidate);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$mime = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = pathinfo($candidate, PATHINFO_EXTENSION) ?: ($photo->mime_type ? explode('/', $photo->mime_type)[1] ?? 'jpg' : 'jpg');
|
||||||
|
$suffix = $variant === 'thumbnail' ? '-thumb' : '';
|
||||||
|
$filename = sprintf('fotospiel-event-%s-photo-%s%s.%s', $event->id, $photo->id, $suffix, $extension ?: 'jpg');
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Content-Type' => $mime ?? 'image/jpeg',
|
||||||
|
'Cache-Control' => 'private, max-age=1800',
|
||||||
|
'Content-Disposition' => 'inline; filename="'.$filename.'"',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($size) {
|
||||||
|
$headers['Content-Length'] = $size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->stream(function () use ($stream) {
|
||||||
|
fpassthru($stream);
|
||||||
|
fclose($stream);
|
||||||
|
}, 200, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiError::response(
|
||||||
|
'photo_unavailable',
|
||||||
|
'Photo Unavailable',
|
||||||
|
'The requested photo could not be loaded.',
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
[
|
||||||
|
'photo_id' => $photo->id,
|
||||||
|
'event_id' => $event->id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a newly uploaded photo.
|
* Store a newly uploaded photo.
|
||||||
*/
|
*/
|
||||||
@@ -856,4 +968,57 @@ class PhotoController extends Controller
|
|||||||
'status' => 'pending',
|
'status' => 'pending',
|
||||||
], 201);
|
], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolvePhotoVariant(Photo $record, string $variant, bool $preferOriginals = false): array
|
||||||
|
{
|
||||||
|
if ($variant === 'thumbnail') {
|
||||||
|
$asset = EventMediaAsset::where('photo_id', $record->id)->where('variant', 'thumbnail')->first();
|
||||||
|
$watermarked = $preferOriginals
|
||||||
|
? null
|
||||||
|
: EventMediaAsset::where('photo_id', $record->id)->where('variant', 'watermarked_thumbnail')->first();
|
||||||
|
$disk = $asset?->disk ?? $record->mediaAsset?->disk;
|
||||||
|
$path = $watermarked?->path ?? $asset?->path ?? ($record->thumbnail_path ?: $record->file_path);
|
||||||
|
$mime = $watermarked?->mime_type ?? $asset?->mime_type ?? 'image/jpeg';
|
||||||
|
} else {
|
||||||
|
$watermarked = $preferOriginals
|
||||||
|
? null
|
||||||
|
: EventMediaAsset::where('photo_id', $record->id)->where('variant', 'watermarked')->first();
|
||||||
|
$asset = $record->mediaAsset ?? $watermarked ?? EventMediaAsset::where('photo_id', $record->id)->where('variant', 'original')->first();
|
||||||
|
$disk = $asset?->disk ?? $record->mediaAsset?->disk;
|
||||||
|
$path = $watermarked?->path ?? $asset?->path ?? ($record->file_path ?? null);
|
||||||
|
$mime = $watermarked?->mime_type ?? $asset?->mime_type ?? ($record->mime_type ?? 'image/jpeg');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
$disk ?: config('filesystems.default', 'public'),
|
||||||
|
$path,
|
||||||
|
$mime,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function storagePathCandidates(string $path): array
|
||||||
|
{
|
||||||
|
$normalized = str_replace('\\', '/', $path);
|
||||||
|
$candidates = [$normalized];
|
||||||
|
|
||||||
|
$trimmed = ltrim($normalized, '/');
|
||||||
|
if ($trimmed !== $normalized) {
|
||||||
|
$candidates[] = $trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($trimmed, 'storage/')) {
|
||||||
|
$candidates[] = substr($trimmed, strlen('storage/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($trimmed, 'public/')) {
|
||||||
|
$candidates[] = substr($trimmed, strlen('public/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$needle = '/storage/app/public/';
|
||||||
|
if (str_contains($normalized, $needle)) {
|
||||||
|
$candidates[] = substr($normalized, strpos($normalized, $needle) + strlen($needle));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique(array_filter($candidates)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,15 +54,11 @@ class PhotoResource extends JsonResource
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$route = $variant === 'thumbnail'
|
|
||||||
? 'api.v1.gallery.photos.asset'
|
|
||||||
: 'api.v1.gallery.photos.asset';
|
|
||||||
|
|
||||||
return \URL::temporarySignedRoute(
|
return \URL::temporarySignedRoute(
|
||||||
$route,
|
'api.v1.tenant.events.photos.asset',
|
||||||
now()->addMinutes(30),
|
now()->addMinutes(30),
|
||||||
[
|
[
|
||||||
'token' => $this->event->slug, // tenant/admin views are trusted; token not used server-side for signed validation
|
'event' => $this->event->slug,
|
||||||
'photo' => $this->id,
|
'photo' => $this->id,
|
||||||
'variant' => $variant === 'thumbnail' ? 'thumbnail' : 'full',
|
'variant' => $variant === 'thumbnail' ? 'thumbnail' : 'full',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -127,6 +127,12 @@ Route::prefix('v1')->name('api.v1.')->group(function () {
|
|||||||
|
|
||||||
Route::post('/photobooth/sparkbooth/upload', [SparkboothUploadController::class, 'store'])
|
Route::post('/photobooth/sparkbooth/upload', [SparkboothUploadController::class, 'store'])
|
||||||
->name('photobooth.sparkbooth.upload');
|
->name('photobooth.sparkbooth.upload');
|
||||||
|
|
||||||
|
Route::get('/tenant/events/{event:slug}/photos/{photo}/{variant}/asset', [PhotoController::class, 'asset'])
|
||||||
|
->whereNumber('photo')
|
||||||
|
->where('variant', 'thumbnail|full')
|
||||||
|
->middleware('signed')
|
||||||
|
->name('tenant.events.photos.asset');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware(['auth:sanctum', 'tenant.collaborator', 'tenant.isolation', 'throttle:tenant-api'])->prefix('tenant')->group(function () {
|
Route::middleware(['auth:sanctum', 'tenant.collaborator', 'tenant.isolation', 'throttle:tenant-api'])->prefix('tenant')->group(function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user