Skip to content

TASK-1623 — Hotfix upload image Livewire stockage distant - #624

Merged
cslucki merged 3 commits into
developfrom
TASK-1623-hotfix-upload-image-livewire-stockage-distant
Sep 23, 2026
Merged

cslucki merged 3 commits into
developfrom
TASK-1623-hotfix-upload-image-livewire-stockage-distant

Conversation

@cslucki

@cslucki cslucki commented Sep 23, 2026

Copy link
Copy Markdown
Owner

ROOT_CAUSE — mesurée, pas supposée

Sur un environnement où Livewire stocke ses fichiers temporaires sur un disque distant, tout upload d'image levait :

Intervention\Image\Exceptions\DirectoryNotFoundException
The directory "livewire-tmp" contained in SplFileInfo does not exist

Le mécanisme tient en deux lignes de vendor :

// TemporaryUploadedFile
public function getPathname(): string { return $this->storage->path($this->path); }
// FilesystemAdapter:282
public function path($path) { return $this->prefixer->prefixPath($path); }

path() ne fait que préfixer avec la racine du disque. Absolue en local → le chemin existe. Vide sur s3/gcs → le chemin rendu est relatif (livewire-tmp/xxx.png). Intervention résout un SplFileInfo par son getPathname(), teste is_dir(dirname($path)), et lève.

Pourquoi aucun test existant ne POUVAIT l'attraper

Livewire::test(...)->set('image', $f) passe par FileUploadConfiguration::disk(), qui rend 'tmp-for-tests' en dur dès que l'app tourne en test, et storage() en fait un Storage::fake() — donc un disque local. Le harnais de test de Livewire ne peut structurellement pas produire la condition de production.

Le banc construit donc un vrai TemporaryUploadedFile sur un disque dont le FilesystemAdapter reçoit root vide : path() rend un chemin relatif, comme s3, sans réseau, tandis que le contenu est réellement servi.

FIX

Quatre corrections d'une ligne : Image::decode($file)Image::decode($file->get()). Le contenu, jamais le chemin. C'est l'idiome déjà en place (GenerateServiceThumbnail lit son image par $disk->get($path)). Pas de service générique, pas d'abstraction, pas de refactor.

Hors scope, vérifié et non modifié : ServiceController et ProfileController reçoivent un UploadedFile d'une requête HTTP classique, dans le tmp local de PHP — pas le même défaut.

Mesures

Banc avant fix 5 rouges, 2 verts
Banc après fix 7 verts (27 assertions)
Voisins directs 199 verts, 1 rouge #[Group('ci-known-red')] pré-existant sur develop (rejoué sur le fichier nu pour le prouver)
PostgreSQL 118 verts (286 assertions)
Sabotage redonner l'objet fichier fait rougir la garde de non-retour

Recette navigateur réelle

ChatLoop et création d'annonce sur https://test.laravel : image jointe, aperçu, envoi, RIFF Web/P VP8 640x400 produit dans les deux cas. Zéro erreur console, zéro erreur Laravel. Artefacts de recette supprimés en base.

L'environnement local utilise un disque temporaire local : la recette navigateur couvre les critères 1, 3, 4 et 6 ; le critère 2 (stockage distant) est couvert par le banc, seul endroit où la condition soit reproductible sans S3.

Multi-tenant

Aucune ligne touchée. Organization = Tenant, Loop != Tenant, community_id = dette legacy temporaire, aucune migration Community → Organization.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3

Cyril and others added 3 commits September 23, 2026 07:11
…1623)

Test ROUGE avant correction. Les quatre chemins d'upload image passent
l'objet fichier a Intervention Image, qui le resout par getPathname().
TemporaryUploadedFile::getPathname() rend Storage::disk($tmp)->path($p), et
FilesystemAdapter::path() ne fait que prefixer avec la racine du disque :
absolue en local, VIDE sur s3/gcs. Le chemin rendu est alors relatif —
« livewire-tmp/xxx.png » — et Intervention leve DirectoryNotFoundException.

Le point qui a demande le plus de soin : aucun test existant ne POUVAIT
attraper ce bug. Livewire::test()->set('image', ...) passe par
FileUploadConfiguration::disk(), qui rend « tmp-for-tests » en dur des que
l'app tourne en test, et storage() en fait un Storage::fake() local. Le
harnais de test de Livewire ne peut pas produire la condition de production ;
un test ecrit par ce chemin serait reste vert quoi qu'il arrive.

Le banc construit donc un vrai TemporaryUploadedFile sur un disque dont le
FilesystemAdapter recoit root vide : path() rend un chemin relatif, comme s3,
sans reseau — tandis que le contenu est reellement servi. C'est exactement ce
que fait un stockage distant.

5 rouges, avec l'exception et le message exacts du rapport PROD. Les 2 verts
comptent autant : la premisse et le cas local passent deja, donc le banc ne
rougit pas par construction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
…hemin (TASK-1623)

Quatre corrections ciblees, une ligne chacune : Image::decode($file) devient
Image::decode($file->get()). C'est l'idiome deja en place dans le depot
(GenerateServiceThumbnail lit son image par $disk->get($path)), et il ne
demande ni service generique, ni abstraction, ni refactor.

Sur un stockage temporaire distant, TemporaryUploadedFile::getPathname()
rend « livewire-tmp/xxx.png » — un chemin qui n'existe localement nulle part,
parce que FilesystemAdapter::path() ne fait que prefixer avec la racine du
disque, et que cette racine est vide sur s3/gcs. Le contenu, lui, a toujours
ete lisible : c'est lui qu'il fallait passer.

Le banc de reproduction passe de 5 rouges a 7 verts, et le cas local est
verifie avec le meme objet de production sur un disque a racine absolue.
Sabotage : redonner l'objet fichier a LoopChat fait rougir la garde de
non-retour, qui balaie les quatre surfaces d'un coup.

Hors scope, verifie et non modifie : ServiceController et ProfileController
recoivent un UploadedFile d'une requete HTTP classique, dans le tmp local de
PHP — ils ne presentent pas ce defaut.

118 verts en PostgreSQL. Le seul rouge voisin, « workspace cards shell », est
porte par #[Group('ci-known-red')] et documente comme deja rouge sur develop
avant TASK-1112 : il est hors gate GitHub, et hors de cette TASK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqwjarKwhSbpzdPxLAmNh3
@cslucki
cslucki merged commit 38c12a9 into develop Sep 23, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant