first commit

This commit is contained in:
julien
2026-03-16 01:47:07 +01:00
commit 8f7e61bda0
185 changed files with 27731 additions and 0 deletions

228
src/Media/MediaService.php Normal file
View File

@@ -0,0 +1,228 @@
<?php
declare(strict_types=1);
namespace App\Media;
use App\Media\Exception\FileTooLargeException;
use App\Media\Exception\InvalidMimeTypeException;
use App\Media\Exception\StorageException;
use PDOException;
use Psr\Http\Message\UploadedFileInterface;
final class MediaService implements MediaServiceInterface
{
private const ALLOWED_MIME_TYPES = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
];
private const WEBP_CONVERTIBLE = ['image/jpeg', 'image/png'];
private const MIME_EXTENSIONS = [
'image/jpeg' => 'webp',
'image/png' => 'webp',
'image/gif' => 'gif',
'image/webp' => 'webp',
];
private const MIME_EXTENSIONS_FALLBACK = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
];
private const MAX_PIXELS = 40000000;
public function __construct(
private readonly MediaRepositoryInterface $mediaRepository,
private readonly string $uploadDir,
private readonly string $uploadUrl,
private readonly int $maxSize,
) {
}
public function findAll(): array
{
return $this->mediaRepository->findAll();
}
public function findByUserId(int $userId): array
{
return $this->mediaRepository->findByUserId($userId);
}
public function findById(int $id): ?Media
{
return $this->mediaRepository->findById($id);
}
public function store(UploadedFileInterface $uploadedFile, int $userId): string
{
$size = $uploadedFile->getSize();
if (!is_int($size)) {
throw new StorageException('Impossible de déterminer la taille du fichier uploadé');
}
if ($size > $this->maxSize) {
throw new FileTooLargeException($this->maxSize);
}
$tmpPathRaw = $uploadedFile->getStream()->getMetadata('uri');
if (!is_string($tmpPathRaw) || $tmpPathRaw === '') {
throw new StorageException('Impossible de localiser le fichier temporaire uploadé');
}
$tmpPath = $tmpPathRaw;
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmpPath);
if ($mime === false || !in_array($mime, self::ALLOWED_MIME_TYPES, true)) {
throw new InvalidMimeTypeException($mime === false ? 'unknown' : $mime);
}
$this->assertReasonableDimensions($tmpPath);
$converted = false;
if (in_array($mime, self::WEBP_CONVERTIBLE, true)) {
$convertedPath = $this->convertToWebP($tmpPath);
if ($convertedPath !== null) {
$tmpPath = $convertedPath;
$converted = true;
}
}
$rawHash = hash_file('sha256', $tmpPath);
if ($rawHash === false) {
if ($converted) {
@unlink($tmpPath);
}
throw new StorageException('Impossible de calculer le hash du fichier');
}
$hash = $rawHash;
$existing = $this->mediaRepository->findByHash($hash);
if ($existing !== null) {
if ($converted) {
@unlink($tmpPath);
}
return $existing->getUrl();
}
if (!is_dir($this->uploadDir) && !@mkdir($this->uploadDir, 0755, true)) {
throw new StorageException("Impossible de créer le répertoire d'upload");
}
$extension = $converted
? self::MIME_EXTENSIONS[$mime]
: self::MIME_EXTENSIONS_FALLBACK[$mime];
$filename = uniqid('', true) . '_' . bin2hex(random_bytes(4)) . '.' . $extension;
$destPath = $this->uploadDir . DIRECTORY_SEPARATOR . $filename;
if ($converted) {
if (!copy($tmpPath, $destPath)) {
@unlink($tmpPath);
throw new StorageException('Impossible de déplacer le fichier converti');
}
@unlink($tmpPath);
} else {
$uploadedFile->moveTo($destPath);
}
$url = $this->uploadUrl . '/' . $filename;
$media = new Media(0, $filename, $url, $hash, $userId);
try {
$this->mediaRepository->create($media);
} catch (PDOException $e) {
$duplicate = $this->mediaRepository->findByHash($hash);
if ($duplicate !== null) {
@unlink($destPath);
return $duplicate->getUrl();
}
@unlink($destPath);
throw $e;
}
return $url;
}
public function delete(Media $media): void
{
$filePath = $this->uploadDir . DIRECTORY_SEPARATOR . $media->getFilename();
if (file_exists($filePath)) {
@unlink($filePath);
}
$this->mediaRepository->delete($media->getId());
}
private function assertReasonableDimensions(string $path): void
{
$size = @getimagesize($path);
if ($size === false) {
throw new StorageException('Impossible de lire les dimensions de l\'image');
}
[$width, $height] = $size;
if ($width <= 0 || $height <= 0) {
throw new StorageException('Dimensions d\'image invalides');
}
if (($width * $height) > self::MAX_PIXELS) {
throw new StorageException('Image trop volumineuse en dimensions pour être traitée');
}
}
private function convertToWebP(string $sourcePath): ?string
{
if (!function_exists('imagewebp')) {
return null;
}
$data = file_get_contents($sourcePath);
if ($data === false || $data === '') {
return null;
}
$image = imagecreatefromstring($data);
if ($image === false) {
return null;
}
imagealphablending($image, false);
imagesavealpha($image, true);
$tmpFile = tempnam(sys_get_temp_dir(), 'slim_webp_');
if ($tmpFile === false) {
imagedestroy($image);
return null;
}
@unlink($tmpFile);
$tmpPath = $tmpFile . '.webp';
if (!imagewebp($image, $tmpPath, 85)) {
imagedestroy($image);
@unlink($tmpPath);
return null;
}
imagedestroy($image);
return $tmpPath;
}
}