*/ public const ALLOWED_MIME_TYPES = [ 'application/pdf', 'image/jpeg', 'image/png', 'image/webp', 'image/gif', ]; /** * Taille maximale autorisee : 10 Mo. */ public const MAX_SIZE_BYTES = 10 * 1024 * 1024; public function __construct( // Racine de stockage des fichiers televerses (hors web root, sous var/). #[Autowire('%kernel.project_dir%/var/uploads')] private readonly string $uploadBaseDir, private readonly ClockInterface $clock, ) {} /** * Valide, calcule l empreinte, deplace le fichier sur disque et retourne * un UploadedDocument NON persiste (le caller le persiste). * * @throws UnsupportedMimeTypeException si le MIME server-side est hors whitelist * @throws FileTooLargeException si le fichier depasse MAX_SIZE_BYTES */ public function upload(UploadedFile $file): UploadedDocument { // Detection MIME server-side (finfo sur le contenu) — jamais le MIME // declare par le client. $mimeType = $file->getMimeType() ?? 'application/octet-stream'; if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)) { throw new UnsupportedMimeTypeException($mimeType, self::ALLOWED_MIME_TYPES); } // getSize() peut renvoyer false si le fichier est illisible. $size = $file->getSize(); if (false === $size || $size > self::MAX_SIZE_BYTES) { throw new FileTooLargeException(false === $size ? 0 : $size, self::MAX_SIZE_BYTES); } // Checksum AVANT le move : le chemin du fichier change apres deplacement. // hash_file renvoie false si le fichier temporaire est illisible (I/O) : // on echoue proprement plutot que de propager un TypeError opaque au // constructeur (parametre $checksum type string). $checksum = hash_file('sha256', $file->getPathname()); if (false === $checksum) { throw new FileUploadException('Impossible de lire le fichier televerse pour en calculer l\'empreinte.'); } $now = $this->clock->now(); $relativeDir = $now->format('Y').'/'.$now->format('m'); $targetDir = $this->uploadBaseDir.'/'.$relativeDir; // Nom de stockage genere aleatoirement : evite les collisions et toute // injection via le nom client. Extension deduite du MIME. $extension = $file->guessExtension() ?: 'bin'; $storedName = bin2hex(random_bytes(16)).'.'.$extension; // Le nom d origine est conserve uniquement comme metadonnee d affichage, // borne a la longueur de colonne (255). $originalFilename = mb_substr($file->getClientOriginalName(), 0, 255); $file->move($targetDir, $storedName); return new UploadedDocument( originalFilename: $originalFilename, storedPath: $relativeDir.'/'.$storedName, mimeType: $mimeType, sizeBytes: $size, checksum: $checksum, createdAt: $now, ); } /** * Supprime le fichier physique d'un document (compensation lorsque la * persistance echoue APRES l'ecriture disque). Best-effort : silencieux si * le fichier a deja disparu. Evite d'accumuler des binaires orphelins non * references en base. */ public function remove(UploadedDocument $document): void { $path = $this->uploadBaseDir.'/'.$document->getStoredPath(); if (is_file($path)) { @unlink($path); } } }