Add tests for featured thumbnail generation; apply Pint formatting and related edits

This commit is contained in:
2026-05-06 18:55:40 +02:00
parent 7a8bc8e22a
commit 82f2b1f660
65 changed files with 11325 additions and 49545 deletions

View File

@@ -5,42 +5,15 @@ 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 Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\Drivers\Gd\Driver as GdDriver;
use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\ImageManager;
use RuntimeException;
final class StudioNewsMediaApiController extends Controller
{
private const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
private const MAX_FILE_SIZE_KB = 6144;
private const MAX_WIDTH = 2200;
private const MAX_HEIGHT = 1400;
private const MIN_WIDTH = 1200;
private const MIN_HEIGHT = 630;
private ?ImageManager $manager = null;
public function __construct()
public function __construct(private readonly NewsCoverImageService $covers)
{
try {
$this->manager = extension_loaded('gd')
? new ImageManager(new GdDriver())
: new ImageManager(new ImagickDriver());
} catch (\Throwable) {
$this->manager = null;
}
}
public function store(Request $request): JsonResponse
@@ -52,26 +25,28 @@ final class StudioNewsMediaApiController extends Controller
'required',
'file',
'image',
'max:' . self::MAX_FILE_SIZE_KB,
'max:' . $this->covers->maxFileSizeKb(),
'mimes:jpg,jpeg,png,webp',
'mimetypes:image/jpeg,image/png,image/webp',
],
]);
/** @var UploadedFile $file */
$file = $validated['image'];
try {
$stored = $this->storeMediaFile($file);
$stored = $this->covers->storeUploadedFile($file);
return response()->json([
'success' => true,
'path' => $stored['path'],
'url' => $this->publicUrlForPath($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([
@@ -99,7 +74,7 @@ final class StudioNewsMediaApiController extends Controller
'path' => ['required', 'string', 'max:2048'],
]);
$this->deleteMediaFile((string) $validated['path']);
$this->covers->deleteManagedFiles((string) $validated['path']);
return response()->json([
'success' => true,
@@ -176,56 +151,4 @@ final class StudioNewsMediaApiController extends Controller
{
abort_unless($request->user() && ($request->user()->isAdmin() || $request->user()->isModerator()), 403);
}
private function mediaDiskName(): string
{
return (string) config('uploads.object_storage.disk', 's3');
}
private function mediaPath(string $hash): string
{
return sprintf(
'news/covers/%s/%s/%s.webp',
substr($hash, 0, 2),
substr($hash, 2, 2),
$hash,
);
}
private function publicUrlForPath(string $path): string
{
return rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/' . ltrim($path, '/');
}
private function deleteMediaFile(string $path): void
{
$trimmed = ltrim(trim($path), '/');
if ($trimmed === '' || ! Str::startsWith($trimmed, 'news/covers/')) {
return;
}
Storage::disk($this->mediaDiskName())->delete($trimmed);
}
private function assertImageManager(): void
{
if ($this->manager !== null) {
return;
}
throw new RuntimeException('Image processing is not available on this environment.');
}
private function assertStorageIsAllowed(): void
{
if (! app()->environment('production')) {
return;
}
$diskName = $this->mediaDiskName();
if (in_array($diskName, ['local', 'public'], true)) {
throw new RuntimeException('Production news media storage must use object storage, not local/public disks.');
}
}
}