404 Not Found

404 Not Found


nginx

Laravel File Storage and Uploads

The file system is Laravel's "repository management"—whether files are stored on a local disk or in the S3 cloud, the code remains exactly the same.

1. What You'll Learn


2. A True Story from the World of Operations

(1) Problem: The server disk is full, and all the images are lost

All of ShopMetrics' product images were stored locally on the server in the public/uploads/ directory—500 GB of images filled up the disk, causing the site to crash. To make matters worse, there were no backups after the server hardware failed, and Alice lost all 2,000 of her product images. Bob wanted to migrate to S3, but file paths were hard-coded all over the code, and after three days of work, he still hadn't finished making the changes.

(2) Solutions for the Storage Abstraction Layer

Laravel Storage facade uses a unified API to manage files—whether local, S3, or any other driver, the code remains the same; migrating simply requires changing the .env configuration.

PHP
// Same code works for local, S3, or any driver
Storage::disk('public')->put('shops/logo.jpg', $file);
$url = Storage::disk('public')->url('shops/logo.jpg');

// Switch to S3 by changing .env
// FILESYSTEM_DISK=s3
// Everything else stays the same!

(3) Revenue

Bob switched to S3 by changing just two lines .env, with no code modifications. Alice's images on S3 have 11 nines of durability, so disk space running out and hardware failures are no longer an issue.


3. Storage Abstraction Layer

(1) Driver Configuration

PHP
// config/filesystems.php
'disks' => [
    'local' => [
        'driver' => 'local',
        'root' => storage_path('app'),
        'throw' => false,
    ],

    'public' => [
        'driver' => 'local',
        'root' => storage_path('app/public'),
        'url' => env('APP_URL') . '/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'),
    ],
],

'default' => env('FILESYSTEM_DISK', 'local'),

(2) Driver Comparison

Drive Storage Location Public Network Access Persistence Cost
local storage/app/ ❌ Requires routing Server Free
public storage/app/public/ ✅ Symbolic link Server Free
s3 AWS S3 ✅ URL 11 nines Pay-as-you-go
s3+CDN S3 + CloudFront ✅ CDN 11 nines Pay-as-you-go

(1) ▶ Example: ShopMetrics Storage Configuration

BASH
# .env — Development: use public disk
FILESYSTEM_DISK=public

# .env — Production: use S3
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=shopmetrics-uploads

# Create symbolic link for public disk
php artisan storage:link
# [OK] Link created: public/storage -> storage/app/public

Output:

TEXT
# Command executed successfully

4. The Complete File Upload Process

(1) Validation → Storage → URL Generation

PHP
// Step 1: Validate
$validated = $request->validate([
    'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
]);

// Step 2: Store
$path = $request->file('logo')->store('shops/logos', 'public');
// => "shops/logos/abc123def456.jpg"

// Step 3: Generate URL
$url = Storage::disk('public')->url($path);
// => "http://shopmetrics.test/storage/shops/logos/abc123def456.jpg"

// Step 4: Save path to database
$shop->update(['logo_path' => $path]);

(2) Comparison of Upload Methods

Method File Name Example Path
store('dir', 'disk') Random Hash Name shops/logos/abc123.jpg
storeAs('dir', name, 'disk') Custom Name shops/logos/alice-store.jpg
storePublicly('dir', 'disk') Random Name + Public Same store
storePubliclyAs(...) Custom Name + Public Same as storeAs

(1) ▶ Example: ShopMetrics Product Image Upload

PHP
// app/Http/Controllers/ProductImageController.php
class ProductImageController extends Controller
{
    public function store(Request $request, Product $product): JsonResponse
    {
        $validated = $request->validate([
            'image' => 'required|image|mimes:jpeg,png,webp|max:5120',
            'is_primary' => 'sometimes|boolean',
        ]);

        $path = $request->file('image')->store(
            "products/{$product->id}/images",
            's3',
        );

        $image = $product->images()->create([
            'path' => $path,
            'is_primary' => $validated['is_primary'] ?? false,
            'mime_type' => $request->file('image')->getMimeType(),
            'size' => $request->file('image')->getSize(),
        ]);

        return response()->json([
            'message' => 'Image uploaded.',
            'data' => [
                'id' => $image->id,
                'url' => Storage::disk('s3')->url($path),
            ],
        ], 201);
    }

    public function destroy(Product $product, Image $image): Response
    {
        Storage::disk('s3')->delete($image->path);
        $image->delete();
        return response()->noContent();
    }
}

Output:

TEXT
// Execution Successful

BASH
php artisan storage:link
# Creates: public/storage → storage/app/public
TEXT
public/
├── index.php
├── storage/ → ../../storage/app/public/   (symbolic link)
│   └── shops/logos/abc123.jpg              (accessible via web)

storage/
└── app/
    └── public/                             (actual file location)
        └── shops/logos/abc123.jpg
Path Purpose Web Access
storage/app/public/ Public File Storage ✅ Via Symbolic Links
storage/app/ Private File Storage ❌ Not via the Web
public/ Web Root Directory ✅ Direct Access

(1) ▶ Example: Downloading Private Files

PHP
// Private file — not accessible via web, must go through controller
Route::get('/reports/{report}/download', [ReportController::class, 'download'])
    ->middleware('auth');

class ReportController extends Controller
{
    public function download(Report $report): StreamedResponse
    {
        $this->authorize('view', $report);

        if (!Storage::disk('local')->exists($report->file_path)) {
            abort(404, 'Report file not found.');
        }

        return Storage::disk('local')->download(
            $report->file_path,
            "report-{$report->id}.pdf",
        );
    }
}

Output:

TEXT
// Execution Successful

6. S3 Cloud Storage and Pre-signed URLs

(1) S3 Integration

BASH
composer require league/flysystem-aws-s3-v3:"^3.0"

(2) Pre-signed URL

Pre-signed URLs allow clients to upload and download S3 files directly, without going through a server—saving bandwidth and reducing server load.

PHP
// Generate a temporary upload URL (client uploads directly to S3)
$uploadUrl = Storage::disk('s3')->temporaryUploadUrl(
    "products/{$product->id}/images/" . $request->filename,
    now()->addMinutes(30),
    ['ContentType' => $request->mime_type],
);

// Generate a temporary download URL
$downloadUrl = Storage::disk('s3')->temporaryUrl(
    $image->path,
    now()->addMinutes(15),
);

(3) S3 vs. On-Premises

Dimension Public Disk S3
File Storage Server Disk AWS S3
Web Access Symbolic Links URL/CDN
Expansion Disk-limited Unlimited
Availability Server Dependency 99.999999999%
CDN Integration Requires Configuration CloudFront
Pre-signed URL
Cost Free Pay-as-you-go

(1) ▶ Example: ShopMetrics S3 Pre-signed Upload

PHP
// app/Http/Controllers/Api/FileUploadController.php
class FileUploadController extends Controller
{
    public function presign(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'filename' => 'required|string',
            'mime_type' => 'required|string|in:image/jpeg,image/png,image/webp',
            'size' => 'required|integer|max:5120',
        ]);

        $path = 'uploads/' . auth()->id() . '/' . Str::uuid() . '/' . $validated['filename'];

        $uploadUrl = Storage::disk('s3')->temporaryUploadUrl(
            $path,
            now()->addMinutes(30),
            ['ContentType' => $validated['mime_type']],
        );

        return response()->json([
            'upload_url' => $uploadUrl,
            'path' => $path,
            'expires_in' => 1800,
        ]);
    }

    public function confirm(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'path' => 'required|string',
            'attachable_type' => 'required|string',
            'attachable_id' => 'required|integer',
        ]);

        $url = Storage::disk('s3')->url($validated['path']);

        return response()->json([
            'url' => $url,
            'path' => $validated['path'],
        ]);
    }
}

Output:

TEXT
// Execution Successful

7. File Operations

(1) Common Operations

PHP
// Read
$content = Storage::disk('s3')->get('shops/logos/abc.jpg');
$exists = Storage::disk('s3')->exists('shops/logos/abc.jpg');
$missing = Storage::disk('s3')->missing('shops/logos/abc.jpg');

// Write
Storage::disk('s3')->put('reports/summary.csv', $csvContent);
Storage::disk('s3')->putFileAs('avatars', $uploadedFile, 'profile.jpg');

// Copy / Move
Storage::disk('s3')->copy('old/path.jpg', 'new/path.jpg');
Storage::disk('s3')->move('temp/file.jpg', 'permanent/file.jpg');

// Delete
Storage::disk('s3')->delete('shops/logos/abc.jpg');
Storage::disk('s3')->delete(['file1.jpg', 'file2.jpg']);

// Visibility
Storage::disk('s3')->setVisibility('file.jpg', 'public');
Storage::disk('s3')->setVisibility('file.jpg', 'private');
$visibility = Storage::disk('s3')->getVisibility('file.jpg');

// Directory
$files = Storage::disk('s3')->files('shops/logos');
$allFiles = Storage::disk('s3')->allFiles('shops');
$directories = Storage::disk('s3')->directories('shops');
Storage::disk('s3')->makeDirectory('shops/new-dir');
Storage::disk('s3')->deleteDirectory('shops/old-dir');

// File metadata
$size = Storage::disk('s3')->size('file.jpg');
$modified = Storage::disk('s3')->lastModified('file.jpg');
$path = Storage::disk('s3')->path('file.jpg');

(1) ▶ Example: ShopMetrics Report Generation and Storage

PHP
// app/Services/ReportService.php
class ReportService
{
    public function generateOrderReport(Tenant $tenant, string $format = 'csv'): string
    {
        $orders = Order::where('tenant_id', $tenant->id)
            ->with(['shop', 'items.product'])
            ->completed()
            ->latest()
            ->get();

        $csv = "Order Number,Shop,Customer,Total,Status,Date\n";
        foreach ($orders as $order) {
            $csv .= implode(',', [
                $order->order_number,
                $order->shop->name,
                $order->user->name,
                $order->total,
                $order->status,
                $order->created_at->format('Y-m-d'),
            ]) . "\n";
        }

        $path = "reports/{$tenant->slug}/orders-" . now()->format('Y-m-d-His') . ".csv";
        Storage::disk('s3')->put($path, $csv);

        return $path;
    }

    public function getDownloadUrl(string $path, int $expiresMinutes = 15): string
    {
        return Storage::disk('s3')->temporaryUrl($path, now()->addMinutes($expiresMinutes));
    }

    public function cleanupOldReports(Tenant $tenant): int
    {
        $cutoff = now()->subDays(90)->format('Y-m-d');
        $files = Storage::disk('s3')->allFiles("reports/{$tenant->slug}");
        $deleted = 0;

        foreach ($files as $file) {
            if (str_contains($file, $cutoff) || Storage::disk('s3')->lastModified($file) < now()->subDays(90)->timestamp) {
                Storage::disk('s3')->delete($file);
                $deleted++;
            }
        }

        return $deleted;
    }
}

Output:

TEXT
// Execution Successful

8. Comprehensive Example: ShopMetrics File Upload System

PHP
// ============================================
// Comprehensive: ShopMetrics File Upload System
// Covers: upload, S3, presigned URLs, cleanup, streaming
// ============================================

// app/Http/Controllers/Api/MediaController.php
class MediaController extends Controller
{
    public function upload(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'file' => 'required|file|max:10240',
            'collection' => 'required|in:logos,products,reports',
            'attachable_type' => 'sometimes|string',
            'attachable_id' => 'sometimes|integer',
        ]);

        $file = $request->file('file');
        $tenantId = tenant()->id;
        $path = $validated['collection'] . "/{$tenantId}/" . Str::uuid() . '.' . $file->extension();

        $stored = Storage::disk('s3')->put($path, $file->getContent(), 'public');

        if (!$stored) {
            return response()->json(['message' => 'Upload failed.'], 500);
        }

        $media = Media::create([
            'tenant_id' => $tenantId,
            'path' => $path,
            'filename' => $file->getClientOriginalName(),
            'mime_type' => $file->getMimeType(),
            'size' => $file->getSize(),
            'collection' => $validated['collection'],
        ]);

        return response()->json([
            'message' => 'File uploaded.',
            'data' => [
                'id' => $media->id,
                'url' => Storage::disk('s3')->url($path),
                'filename' => $media->filename,
                'size' => $media->size,
            ],
        ], 201);
    }

    public function download(Media $media): StreamedResponse
    {
        $this->authorize('view', $media);

        if ($media->isPublic()) {
            return redirect(Storage::disk('s3')->url($media->path));
        }

        return Storage::disk('s3')->download($media->path, $media->filename);
    }

    public function temporaryUrl(Media $media): JsonResponse
    {
        $this->authorize('view', $media);

        $url = Storage::disk('s3')->temporaryUrl(
            $media->path,
            now()->addMinutes(15),
        );

        return response()->json(['url' => $url, 'expires_in' => 900]);
    }

    public function destroy(Media $media): Response
    {
        $this->authorize('delete', $media);
        Storage::disk('s3')->delete($media->path);
        $media->delete();
        return response()->noContent();
    }
}

❓ FAQ

Q How do I create storage:link on Windows?
A Laravel's php artisan storage:link automatically creates symbolic links on Windows, but administrator privileges are required. If that fails, create it manually: mklink /D public\storage storage\app\public.
Q When should I use a pre-signed URL?
A Use a pre-signed URL when a client (browser/mobile device) needs to upload a file directly to S3 to avoid routing the file through a server. Large files (>10 MB) must be uploaded using a pre-signed URL.
Q How do I handle uploading large files?
A Use a pre-signed URL to allow clients to upload directly to S3; or use multipart uploads, which support files in the gigabyte range. On the PHP side, you'll need to modify upload_max_filesize and post_max_size.
Q How do S3 and CloudFront CDN work together?
A In AWS, configure a CloudFront distribution to point to an S3 bucket, and replace AWS_URL with your CloudFront domain name. All URLs returned by Storage::url() will automatically be routed through the CDN, accelerating access from around the world.
Q How do I clean up unused files?
A When deleting a model, listen for the deleting event in the model's boot() to delete associated files; run Artisan commands periodically to clean up orphaned files; use S3 lifecycle policies to automatically expire old files.
Q Can I use both public disk and S3 at the same time?
A Yes. Use public disk for development (free and fast) and S3 for production (reliable and scalable). You can switch the default disk using .env's FILESYSTEM_DISK without modifying the code.

📖 Summary


📝 Exercises

  1. Basic Exercise (⭐): Configure ShopMetrics to use public disk storage, implement product image uploads (validation, storage, and display), and display the uploaded images on a Blade page.

  2. Advanced Exercise (⭐⭐): Switch to S3 storage and implement uploads via pre-signed URLs—the front end retrieves the pre-signed URL and uploads directly to S3, and the back end creates a Media record after verification.

  3. Challenge (⭐⭐⭐): Implement S3 file lifecycle management—set a tag (tenant_id) upon upload, write an Artisan command to clean up report files older than 90 days by tenant, and use the S3 bulk delete API to optimize performance.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏