Aller au contenu principal
Making DigitalMaking Digital
Note technique

storage:link sous Laravel : le lien public/storage sous Linux et Windows

En bref

php artisan storage:link crée le lien public/storage vers storage/app/public : tout ce qui s'y trouve devient accessible depuis le web, les fichiers privés restent sur le disque local. Sous Linux, c'est un lien symbolique ; sous Windows, une jonction créée sans droits administrateur, que --force et storage:unlink ne savent pas supprimer (rmdir public\storage). Le chemin d'un fichier s'obtient avec storage_path() ou Storage::disk(...)->path().

Une image enregistrée avec Storage::disk('public')->put() est bien dans storage/app/public, mais son adresse /storage/... ne répond pas : le lien public/storage n'existe pas encore. La commande php artisan storage:link le crée, sous Linux comme sous Windows, sans droits administrateur. Sous Windows, elle crée en réalité une jonction, ce qui change la façon de la supprimer. Et pour connaître le chemin d'un fichier sur le disque, storage_path() et Storage::disk(...)->path() remplacent l'ancien getAdapter()->getPathPrefix(), qui ne fonctionne plus.

À quoi sert le lien public/storage ?

Le serveur web ne sert que le dossier public. Le disque public de Laravel range ses fichiers dans storage/app/public ; la documentation demande donc un lien de public/storage vers storage/app/public pour les rendre accessibles depuis le web.

Ce lien ne protège rien, au contraire : il rend public tout ce que contient storage/app/public. N'importe qui connaissant l'adresse peut télécharger le fichier. Les documents privés (factures, pièces d'identité...) vont sur le disque local, dont la racine est storage/app/private. Dans notre test, /storage/test.txt (disque public) répondait 200, et /storage/secret.txt (disque local) 403. Pour donner un accès ponctuel à un fichier privé, Storage::disk('local')->temporaryUrl('secret.txt', now()->addMinutes(5)) produit une adresse signée : 200 avec la signature, 403 sans.

Terminal
php artisan storage:link

La commande répond The [.../public/storage] link has been connected to [.../storage/app/public]. Ensuite, asset('storage/test.txt') ou Storage::disk('public')->url('test.txt') donne l'adresse du fichier (dans notre test, http://localhost:8000/storage/test.txt, à partir de APP_URL).

D'autres liens peuvent être ajoutés dans le tableau links de config/filesystems.php ; storage:link les crée tous :

PHP
// config/filesystems.php
'links' => [
    public_path('storage') => storage_path('app/public'),
    public_path('images') => storage_path('app/images'),
],

La commande a deux options : --force recrée un lien existant, --relative crée un lien relatif. --relative exige le paquet symfony/filesystem, absent d'une installation neuve : sans lui, la commande échoue avec To enable support for relative links, please install the symfony/filesystem package. Attention, sous Linux, --relative --force sans ce paquet a d'abord supprimé le lien existant avant d'échouer : nous nous sommes retrouvés sans lien. Après composer require symfony/filesystem, le lien obtenu était storage -> ../storage/app/public/.

php artisan storage:unlink supprime les liens configurés. Le dossier /public/storage figure dans le .gitignore d'une application neuve : le lien n'est pas versionné et doit être créé sur chaque serveur.

Et sous Linux ?

storage:link utilise la fonction symlink() de PHP : dans notre test sous Linux, ls -l public affiche storage -> /app/storage/app/public (le projet était dans /app). Le lien manuel équivalent :

Terminal
ln -s /app/storage/app/public /app/public/storage

Et sous Windows ?

Sous Windows, storage:link ne crée pas un lien symbolique mais une jonction (mklink /J), ce qu'affiche dir public : <JUNCTION> storage. Une jonction ne demande aucun droit particulier : la commande a fonctionné depuis une session sans droits administrateur. Trois différences sont à connaître :

  • storage:link --force refuse de recréer la jonction (The [...] link already exists.) et storage:unlink ne la supprime pas, sans aucun message : PHP ne la reconnaît pas comme un lien (is_link() renvoie false).
  • storage:link --relative, même avec symfony/filesystem, a affiché son message de succès sans rien créer.
  • Pour supprimer la jonction sans toucher aux fichiers, utilisez rmdir dans l'invite de commandes : dans notre test, storage/app/public/test.txt était toujours là après.
Terminal
rmdir public\storage

Pour créer le lien à la main, dans l'invite de commandes (cmd), mklink /J fonctionne sans droits particuliers :

Terminal
mklink /J public\storage storage\app\public

mklink /D (vrai lien symbolique) a échoué sans droits administrateur : Vous ne disposez pas de privilèges suffisants pour effectuer cette opération. En PowerShell, New-Item -ItemType SymbolicLink a échoué de même (Cette opération nécessite un privilège d'administrateur.), alors que New-Item -ItemType Junction a fonctionné. Selon la documentation de Microsoft, le droit de créer des liens symboliques est réservé par défaut aux administrateurs, et la création sans élévation suppose d'activer le mode développeur de Windows.

Comment obtenir le chemin du storage ?

Le code des anciens tutoriels, Storage::disk('local')->getDriver()->getAdapter()->getPathPrefix(), provoque aujourd'hui l'erreur Call to undefined method League\Flysystem\Filesystem::getAdapter(). Utilisez plutôt :

PHP
// N'importe où dans l'application
use Illuminate\Support\Facades\Storage;

storage_path();                                  // dossier storage du projet
storage_path('app/public');                      // racine du disque public
Storage::disk('public')->path('avatars/a.jpg');  // chemin absolu d'un fichier
Storage::disk('local')->path('factures/f.pdf');  // fichier sous storage/app/private

D'après la documentation, path() renvoie le chemin absolu avec le pilote local, et le chemin relatif dans le bucket avec S3. Sous Windows, le résultat mélange \ et / (...\storage\app/public\avatars/a.jpg), ce que PHP accepte.

Le lien se crée au déploiement, avec les autres commandes : voir dans quel ordre les lancer. Ces réglages font partie des déploiements que nous assurons pour les applications que nous hébergeons et maintenons ou que nous développons sur mesure. Un souci de fichiers introuvables en production ? Parlons-en.

À retenir
  • php artisan storage:link rend public le contenu de storage/app/public, rien d'autre.
  • Les fichiers privés restent sur le disque local (storage/app/private), avec temporaryUrl() si besoin.
  • Sous Windows, c'est une jonction : la supprimer avec rmdir public\storage, pas avec --force.
  • Le chemin d'un fichier : Storage::disk(...)->path(), plus getAdapter().

Testé avec Laravel 13.37.0 et PHP 8.3.6 sous Windows 11, et PHP 8.3.35 sous Linux (Docker).

Questions fréquentes

Faut-il relancer storage:link à chaque déploiement ?

Le lien n'est pas versionné (/public/storage est dans le .gitignore d'une application neuve) : il faut le créer sur chaque serveur. S'il existe déjà, la commande répond "link already exists" sans rien modifier.

Peut-on ranger des fichiers privés dans storage/app/public ?

Non : tout ce qui s'y trouve est téléchargeable par quiconque connaît l'adresse. Rangez-les sur le disque local (storage/app/private) et donnez un accès ponctuel avec temporaryUrl().

Faut-il être administrateur sous Windows pour lancer storage:link ?

Non. La commande crée une jonction, ce qui a fonctionné dans notre test depuis une session sans droits administrateur. Un vrai lien symbolique (mklink /D) demande en revanche des droits administrateur, ou le mode développeur de Windows.

Que renvoie Storage::path() sans préciser de disque ?

Un chemin sur le disque par défaut, défini par FILESYSTEM_DISK. Dans une application neuve, c'est le disque local : Storage::path('') renvoie le dossier storage/app/private.

Making Digital

Nous connectons les logiciels des PME, automatisons leurs tâches répétitives et créons des assistants IA, depuis 2010. Nos articles partent de nos projets réels.

Notre parcours
Une question sur ce sujet ?

Parlons-en 30 minutes.

Vous saurez si votre problème se règle, comment, et en combien de temps. Sans engagement.

Un échange de 30 minutes, en visio ou par téléphone

Gratuit, sans engagement. On parle de votre problème, pas de technique.