Files
SkinbaseNova/app/Http/Controllers/Studio/StudioNewsMediaApiController.php

154 lines
4.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Studio;
use App\Http\Controllers\Controller;
use App\Services\News\NewsCoverImageService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use RuntimeException;
final class StudioNewsMediaApiController extends Controller
{
public function __construct(private readonly NewsCoverImageService $covers)
{
}
public function store(Request $request): JsonResponse
{
$this->authorizeNews($request);
$validated = $request->validate([
'image' => [
'required',
'file',
'image',
'max:' . $this->covers->maxFileSizeKb(),
'mimes:jpg,jpeg,png,webp',
'mimetypes:image/jpeg,image/png,image/webp',
],
]);
$file = $validated['image'];
try {
$stored = $this->covers->storeUploadedFile($file);
return response()->json([
'success' => true,
'path' => $stored['path'],
'url' => $stored['url'],
'width' => $stored['width'],
'height' => $stored['height'],
'mime_type' => 'image/webp',
'size_bytes' => $stored['size_bytes'],
'mobile_url' => $stored['mobile_url'],
'desktop_url' => $stored['desktop_url'],
'srcset' => $stored['srcset'],
]);
} catch (RuntimeException $e) {
return response()->json([
'error' => 'Validation failed',
'message' => $e->getMessage(),
], 422);
} catch (\Throwable $e) {
logger()->error('News media upload failed', [
'user_id' => (int) ($request->user()?->id ?? 0),
'message' => $e->getMessage(),
]);
return response()->json([
'error' => 'Upload failed',
'message' => 'Could not upload image right now.',
], 500);
}
}
public function destroy(Request $request): JsonResponse
{
$this->authorizeNews($request);
$validated = $request->validate([
'path' => ['required', 'string', 'max:2048'],
]);
$this->covers->deleteManagedFiles((string) $validated['path']);
return response()->json([
'success' => true,
]);
}
/**
* @return array{path:string,width:int,height:int,size_bytes:int}
*/
private function storeMediaFile(UploadedFile $file): array
{
$this->assertImageManager();
$this->assertStorageIsAllowed();
$uploadPath = (string) ($file->getRealPath() ?: $file->getPathname());
if ($uploadPath === '' || ! is_readable($uploadPath)) {
throw new RuntimeException('Unable to resolve uploaded image path.');
}
$raw = file_get_contents($uploadPath);
if ($raw === false || $raw === '') {
throw new RuntimeException('Unable to read uploaded image.');
}
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mime = strtolower((string) $finfo->buffer($raw));
if (! in_array($mime, self::ALLOWED_MIME_TYPES, true)) {
throw new RuntimeException('Unsupported image mime type.');
}
$size = @getimagesizefromstring($raw);
if (! is_array($size) || ($size[0] ?? 0) < 1 || ($size[1] ?? 0) < 1) {
throw new RuntimeException('Uploaded file is not a valid image.');
}
$width = (int) ($size[0] ?? 0);
$height = (int) ($size[1] ?? 0);
if ($width < self::MIN_WIDTH || $height < self::MIN_HEIGHT) {
throw new RuntimeException(sprintf(
'Image is too small. Minimum required size is %dx%d.',
self::MIN_WIDTH,
self::MIN_HEIGHT,
));
}
$image = $this->manager->read($raw)->scaleDown(width: self::MAX_WIDTH, height: self::MAX_HEIGHT);
$encoded = (string) $image->encode(new WebpEncoder(85));
$hash = hash('sha256', $encoded);
$path = $this->mediaPath($hash);
$disk = Storage::disk($this->mediaDiskName());
$written = $disk->put($path, $encoded, [
'visibility' => 'public',
'CacheControl' => 'public, max-age=31536000, immutable',
'ContentType' => 'image/webp',
]);
if ($written !== true) {
throw new RuntimeException('Unable to store image in object storage.');
}
return [
'path' => $path,
'width' => (int) $image->width(),
'height' => (int) $image->height(),
'size_bytes' => strlen($encoded),
];
}
private function authorizeNews(Request $request): void
{
abort_unless($request->user() && ($request->user()->isAdmin() || $request->user()->isModerator()), 403);
}
}