0

Storage::put, get, delete, url, temporaryUrl

Beginner5 min read·lv-23-002

Concept

Public vs private storage is a fundamental file storage design decision. Laravel's built-in local public disk stores files in storage/app/public/. They're not directly web-accessible until php artisan storage:link creates a symbolic link from public/storagestorage/app/public/.

public disk: Files accessible via Storage::url(). Only use for files that ALL visitors should access: user avatars, product images, public downloads. Never store sensitive files here.

local disk (default): Files in storage/app/. NOT web-accessible. Use for private files, temp files, processing artifacts.

storage:link: php artisan storage:link creates public/storage as a symlink. Called once during setup/deploy. Without it, files on the public disk have no web-accessible URL. The command also shows all configured links.

Custom link paths: In config/filesystems.php, 'links' array maps symlink source → target: public_path('uploads') => storage_path('app/uploads').

Temporary URLs for private files: For private S3/R2/GCS files, use Storage::temporaryUrl() which generates a signed URL valid for a limited time. For local private files, serve them through a controller that checks authorization.

Serving private local files through a controller:

php
return response()->file(Storage::path($privatePath));

This reads the file from storage and streams it to the response — secure because it goes through your authorization middleware.

Code Example

php
<?php
// config/filesystems.php
'disks' => [
    'local' => [
        'driver' => 'local',
        'root'   => storage_path('app'),         // storage/app/ — not web accessible
    ],
    'public' => [
        'driver'     => 'local',
        'root'       => storage_path('app/public'), // storage/app/public/
        'url'        => env('APP_URL') . '/storage', // http://app.com/storage/
        'visibility' => 'public',
    ],
    's3' => [
        'driver' => 's3',
        'key'    => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION'),
        'bucket' => env('AWS_BUCKET'),
        'url'    => env('AWS_URL'),             // CDN URL if configured
    ],
],
'links' => [
    public_path('storage') => storage_path('app/public'), // default link
    // Add custom links:
    // public_path('downloads') => storage_path('app/downloads'),
],

// Storage link setup (one-time, run on deploy)
// php artisan storage:link

// Store PUBLIC file (world-accessible)
$path = $request->file('avatar')->store('avatars', 'public');
// File: storage/app/public/avatars/uuid.jpg
// URL: http://app.com/storage/avatars/uuid.jpg
$url = Storage::disk('public')->url($path);

// Store PRIVATE file
$path = $request->file('contract')->store('contracts', 'local');
// File: storage/app/contracts/uuid.pdf
// No direct URL — must serve through controller

// Serving private files through authorization
class ContractController extends Controller
{
    public function download(Request $request, Contract $contract)
    {
        $this->authorize('download', $contract);  // check permission
        $path = Storage::path($contract->file_path); // full filesystem path
        return response()->file($path, [
            'Content-Disposition' => 'attachment; filename="' . $contract->original_name . '"',
        ]);
    }
}