Laravel File Storage: Uploading and Managing Files with the Storage Facade

Published on November 29, 2025
Laravel FileStorage StorageFacade FileUpload CloudStorage

Introduction to Laravel File Storage

Laravel provides a powerful filesystem abstraction thanks to the Flysystem PHP package. This allows you to work with local filesystems and cloud storage services like Amazon S3, FTP, and SFTP using the same elegant API.

Why Use Laravel's Storage Facade?

  • Unified API for multiple storage systems
  • Simple switching between storage drivers
  • Built-in security features
  • File streaming and optimization
  • Easy testing with fake storage

Common Storage Use Cases:

  • User uploads (avatars, documents)
  • Image and file processing
  • Export generation (PDF, CSV, Excel)
  • Backup file storage
  • Temporary file handling
  • Cloud storage integration

Configuration

Filesystem Configuration

// config/filesystems.php

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

    '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',
            'throw' => false,
        ],

        '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'),
            'endpoint' => env('AWS_ENDPOINT'),
            'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
            'throw' => false,
        ],

        'ftp' => [
            'driver' => 'ftp',
            'host' => env('FTP_HOST'),
            'username' => env('FTP_USERNAME'),
            'password' => env('FTP_PASSWORD'),
            'port' => env('FTP_PORT', 21),
            'root' => env('FTP_ROOT'),
            'passive' => true,
            'ssl' => true,
            'timeout' => 30,
        ],

        'backups' => [
            'driver' => 'local',
            'root' => storage_path('app/backups'),
        ],

        'temp' => [
            'driver' => 'local',
            'root' => storage_path('app/temp'),
        ],
    ],

    'links' => [
        public_path('storage') => storage_path('app/public'),
    ],
];

Environment Configuration

# .env file
FILESYSTEM_DISK=local

# S3 Configuration
AWS_ACCESS_KEY_ID=your-aws-access-key-id
AWS_SECRET_ACCESS_KEY=your-aws-secret-access-key
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=your-bucket-name
AWS_URL=https://your-bucket.s3.amazonaws.com

# FTP Configuration
FTP_HOST=ftp.example.com
FTP_USERNAME=your-username
FTP_PASSWORD=your-password
FTP_ROOT=/path/to/root

Creating Symbolic Link

# Create symbolic link for public disk
php artisan storage:link

# For custom disks (create manually or via command)
ln -s /path/to/storage/app/public /path/to/public/storage

Basic File Operations

Storing Files

<?php
// app/Http/Controllers/FileController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class FileController extends Controller
{
    public function store(Request $request)
    {
        // Store uploaded file
        $path = $request->file('avatar')->store('avatars');
        
        // Store with specific disk
        $path = $request->file('avatar')->store('avatars', 's3');
        
        // Store with original filename
        $path = $request->file('avatar')->storeAs('avatars', 'custom-name.jpg');
        
        // Store with generated filename
        $filename = time() . '_' . $request->file('avatar')->getClientOriginalName();
        $path = $request->file('avatar')->storeAs('avatars', $filename, 'public');

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

    public function storeMultiple(Request $request)
    {
        $paths = [];
        
        if ($request->hasFile('documents')) {
            foreach ($request->file('documents') as $file) {
                $paths[] = $file->store('documents', 'public');
            }
        }

        return response()->json(['paths' => $paths]);
    }
}

Retrieving Files

<?php
// app/Http/Controllers/FileController.php

public function show($filename)
{
    // Get file contents
    $contents = Storage::get('avatars/' . $filename);
    
    // Check if file exists
    if (Storage::exists('avatars/' . $filename)) {
        $contents = Storage::get('avatars/' . $filename);
    }
    
    // Get file URL
    $url = Storage::url('avatars/' . $filename);
    
    // Get temporary URL (for S3)
    $temporaryUrl = Storage::temporaryUrl(
        'avatars/' . $filename, 
        now()->addMinutes(5)
    );
    
    return response()->json([
        'url' => $url,
        'temporary_url' => $temporaryUrl,
        'exists' => Storage::exists('avatars/' . $filename)
    ]);
}

public function download($filename)
{
    // Download file
    return Storage::download('avatars/' . $filename);
    
    // Download with custom name
    return Storage::download('avatars/' . $filename, 'user-avatar.jpg');
    
    // Download with headers
    return Storage::download('avatars/' . $filename, null, [
        'Content-Type' => 'image/jpeg',
    ]);
}

File Management

<?php
// app/Http/Controllers/FileController.php

public function update(Request $request, $filename)
{
    // Copy file
    Storage::copy(
        'old/avatars/' . $filename, 
        'new/avatars/' . $filename
    );
    
    // Move file
    Storage::move(
        'temp/avatars/' . $filename, 
        'avatars/' . $filename
    );
    
    return response()->json(['message' => 'File updated']);
}

public function destroy($filename)
{
    // Delete file
    Storage::delete('avatars/' . $filename);
    
    // Delete multiple files
    Storage::delete([
        'avatars/' . $filename,
        'avatars/old-' . $filename
    ]);
    
    return response()->json(['message' => 'File deleted']);
}

Advanced File Operations

File Information and Metadata

<?php
// app/Services/FileService.php

namespace App\Services;

use Illuminate\Support\Facades\Storage;

class FileService
{
    public function getFileInfo($path, $disk = null)
    {
        $disk = $disk ?: config('filesystems.default');
        
        return [
            'size' => Storage::disk($disk)->size($path),
            'last_modified' => Storage::disk($disk)->lastModified($path),
            'mime_type' => Storage::disk($disk)->mimeType($path),
            'visibility' => Storage::disk($disk)->getVisibility($path),
            'url' => Storage::disk($disk)->url($path),
            'exists' => Storage::disk($disk)->exists($path),
        ];
    }
    
    public function getDirectoryInfo($directory, $disk = null)
    {
        $disk = $disk ?: config('filesystems.default');
        
        $files = Storage::disk($disk)->files($directory);
        $directories = Storage::disk($disk)->directories($directory);
        $allFiles = Storage::disk($disk)->allFiles($directory);
        
        return [
            'files_count' => count($files),
            'directories_count' => count($directories),
            'total_size' => $this->calculateDirectorySize($directory, $disk),
            'files' => $files,
            'directories' => $directories,
        ];
    }
    
    protected function calculateDirectorySize($directory, $disk)
    {
        $allFiles = Storage::disk($disk)->allFiles($directory);
        $totalSize = 0;
        
        foreach ($allFiles as $file) {
            $totalSize += Storage::disk($disk)->size($file);
        }
        
        return $totalSize;
    }
}

File Streaming and Chunking

<?php
// app/Http/Controllers/DownloadController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Response;

class DownloadController extends Controller
{
    public function streamLargeFile($filename)
    {
        $path = 'large-files/' . $filename;
        
        if (!Storage::exists($path)) {
            abort(404);
        }
        
        return Storage::response($path);
    }
    
    public function streamWithHeaders($filename)
    {
        $path = 'videos/' . $filename;
        
        $stream = Storage::readStream($path);
        $fileSize = Storage::size($path);
        $mimeType = Storage::mimeType($path);
        
        return response()->stream(
            function () use ($stream) {
                fpassthru($stream);
            },
            200,
            [
                'Content-Type' => $mimeType,
                'Content-Length' => $fileSize,
                'Content-Disposition' => 'inline; filename="' . $filename . '"',
                'Cache-Control' => 'no-cache, no-store, must-revalidate',
                'Pragma' => 'no-cache',
                'Expires' => '0',
            ]
        );
    }
    
    public function downloadChunked($filename)
    {
        $path = 'large-files/' . $filename;
        $disk = Storage::disk('s3'); // or local
        
        $fileSize = $disk->size($path);
        $chunkSize = 1024 * 1024; // 1MB chunks
        
        return response()->streamDownload(
            function () use ($disk, $path, $fileSize, $chunkSize) {
                $stream = $disk->readStream($path);
                $sent = 0;
                
                while (!feof($stream) && $sent < $fileSize) {
                    $buffer = fread($stream, $chunkSize);
                    echo $buffer;
                    $sent += strlen($buffer);
                    flush();
                }
                
                fclose($stream);
            },
            $filename,
            [
                'Content-Type' => $disk->mimeType($path),
                'Content-Length' => $fileSize,
            ]
        );
    }
}

Real-World Examples

User Avatar Management

<?php
// app/Http/Controllers/AvatarController.php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;

class AvatarController extends Controller
{
    public function store(Request $request, User $user)
    {
        $request->validate([
            'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048',
        ]);
        
        try {
            // Delete old avatar if exists
            if ($user->avatar_path) {
                Storage::disk('public')->delete($user->avatar_path);
            }
            
            $file = $request->file('avatar');
            $filename = 'avatar_' . $user->id . '_' . time() . '.' . $file->getClientOriginalExtension();
            $directory = 'avatars/users/' . $user->id;
            
            // Create directory if not exists
            if (!Storage::disk('public')->exists($directory)) {
                Storage::disk('public')->makeDirectory($directory);
            }
            
            // Process image with Intervention Image
            $image = Image::make($file->getRealPath());
            
            // Create multiple sizes
            $sizes = [
                'original' => [null, null], // Keep original size
                'large' => [400, 400],
                'medium' => [200, 200],
                'small' => [100, 100],
                'thumbnail' => [50, 50],
            ];
            
            $avatarPaths = [];
            
            foreach ($sizes as $size => $dimensions) {
                $resizedImage = clone $image;
                
                if ($dimensions[0] && $dimensions[1]) {
                    $resizedImage->fit($dimensions[0], $dimensions[1]);
                }
                
                $sizeFilename = $size . '_' . $filename;
                $fullPath = $directory . '/' . $sizeFilename;
                
                Storage::disk('public')->put($fullPath, $resizedImage->encode());
                $avatarPaths[$size] = $fullPath;
            }
            
            // Update user record
            $user->update([
                'avatar_path' => $avatarPaths['medium'], // Use medium as default
                'avatar_paths' => $avatarPaths,
                'avatar_updated_at' => now(),
            ]);
            
            return response()->json([
                'message' => 'Avatar uploaded successfully',
                'paths' => $avatarPaths,
                'urls' => array_map(function ($path) {
                    return Storage::disk('public')->url($path);
                }, $avatarPaths),
            ]);
            
        } catch (\Exception $e) {
            // Cleanup on failure
            if (isset($avatarPaths)) {
                foreach ($avatarPaths as $path) {
                    Storage::disk('public')->delete($path);
                }
            }
            
            return response()->json([
                'error' => 'Failed to upload avatar',
                'message' => $e->getMessage(),
            ], 500);
        }
    }
    
    public function destroy(User $user)
    {
        try {
            if ($user->avatar_paths) {
                foreach ($user->avatar_paths as $path) {
                    Storage::disk('public')->delete($path);
                }
            }
            
            $user->update([
                'avatar_path' => null,
                'avatar_paths' => null,
            ]);
            
            return response()->json([
                'message' => 'Avatar deleted successfully',
            ]);
            
        } catch (\Exception $e) {
            return response()->json([
                'error' => 'Failed to delete avatar',
                'message' => $e->getMessage(),
            ], 500);
        }
    }
    
    public function getAvatar(User $user, $size = 'medium')
    {
        $paths = $user->avatar_paths ?? [];
        
        if (!isset($paths[$size])) {
            abort(404, 'Avatar size not found');
        }
        
        if (!Storage::disk('public')->exists($paths[$size])) {
            abort(404, 'Avatar file not found');
        }
        
        return response()->file(
            Storage::disk('public')->path($paths[$size])
        );
    }
}

Document Management System

<?php
// app/Http/Controllers/DocumentController.php

namespace App\Http\Controllers;

use App\Models\Document;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class DocumentController extends Controller
{
    public function store(Request $request)
    {
        $request->validate([
            'documents.*' => 'required|file|mimes:pdf,doc,docx,xls,xlsx,txt|max:10240',
            'folder' => 'nullable|string|max:255',
        ]);
        
        $uploadedDocuments = [];
        
        foreach ($request->file('documents') as $file) {
            $originalName = $file->getClientOriginalName();
            $extension = $file->getClientOriginalExtension();
            $filename = pathinfo($originalName, PATHINFO_FILENAME);
            $safeFilename = Str::slug($filename);
            $uniqueName = $safeFilename . '_' . time() . '.' . $extension;
            
            $folder = $request->folder ?: 'documents';
            $path = $file->storeAs($folder, $uniqueName, 'public');
            
            $document = Document::create([
                'user_id' => auth()->id(),
                'original_name' => $originalName,
                'storage_path' => $path,
                'file_size' => $file->getSize(),
                'mime_type' => $file->getMimeType(),
                'extension' => $extension,
                'disk' => 'public',
            ]);
            
            $uploadedDocuments[] = $document;
        }
        
        return response()->json([
            'message' => 'Documents uploaded successfully',
            'documents' => $uploadedDocuments,
        ]);
    }
    
    public function download(Document $document)
    {
        if (!Storage::disk($document->disk)->exists($document->storage_path)) {
            abort(404, 'Document not found');
        }
        
        return Storage::disk($document->disk)->download(
            $document->storage_path, 
            $document->original_name
        );
    }
    
    public function preview(Document $document)
    {
        if (!Storage::disk($document->disk)->exists($document->storage_path)) {
            abort(404, 'Document not found');
        }
        
        $mimeType = $document->mime_type;
        
        // For PDFs and images, show inline
        if (Str::startsWith($mimeType, ['image/', 'application/pdf'])) {
            return response()->file(
                Storage::disk($document->disk)->path($document->storage_path)
            );
        }
        
        // For other types, force download
        return Storage::disk($document->disk)->download(
            $document->storage_path,
            $document->original_name
        );
    }
    
    public function destroy(Document $document)
    {
        try {
            // Delete physical file
            Storage::disk($document->disk)->delete($document->storage_path);
            
            // Delete database record
            $document->delete();
            
            return response()->json([
                'message' => 'Document deleted successfully',
            ]);
            
        } catch (\Exception $e) {
            return response()->json([
                'error' => 'Failed to delete document',
                'message' => $e->getMessage(),
            ], 500);
        }
    }
    
    public function bulkDestroy(Request $request)
    {
        $request->validate([
            'document_ids' => 'required|array',
            'document_ids.*' => 'exists:documents,id',
        ]);
        
        $documents = Document::whereIn('id', $request->document_ids)->get();
        $deletedCount = 0;
        
        foreach ($documents as $document) {
            try {
                Storage::disk($document->disk)->delete($document->storage_path);
                $document->delete();
                $deletedCount++;
            } catch (\Exception $e) {
                // Log error but continue with other deletions
                logger()->error("Failed to delete document {$document->id}: " . $e->getMessage());
            }
        }
        
        return response()->json([
            'message' => "{$deletedCount} documents deleted successfully",
            'failed_count' => count($request->document_ids) - $deletedCount,
        ]);
    }
}

Backup Management System

<?php
// app/Services/BackupService.php

namespace App\Services;

use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;

class BackupService
{
    protected $backupDisk;
    
    public function __construct()
    {
        $this->backupDisk = Storage::disk('backups');
    }
    
    public function createDatabaseBackup()
    {
        $timestamp = Carbon::now()->format('Y-m-d_H-i-s');
        $filename = "database_backup_{$timestamp}.sql";
        $path = "database/{$filename}";
        
        try {
            // Get database Configuration
            $connection = config('database.default');
            $database = config("database.connections.{$connection}.database");
            $username = config("database.connections.{$connection}.username");
            $password = config("database.connections.{$connection}.password");
            $host = config("database.connections.{$connection}.host");
            
            // Create backup using mysqldump
            $command = sprintf(
                'mysqldump --host=%s --user=%s --password=%s %s > %s',
                $host,
                $username,
                $password,
                $database,
                storage_path("app/backups/{$path}")
            );
            
            exec($command, $output, $returnVar);
            
            if ($returnVar !== 0) {
                throw new \Exception("MySQL dump failed with return code: {$returnVar}");
            }
            
            // Compress the backup
            $compressedPath = $this->compressFile($path);
            
            // Delete uncompressed file
            $this->backupDisk->delete($path);
            
            return [
                'success' => true,
                'filename' => basename($compressedPath),
                'path' => $compressedPath,
                'size' => $this->backupDisk->size($compressedPath),
                'created_at' => now(),
            ];
            
        } catch (\Exception $e) {
            // Cleanup on failure
            if ($this->backupDisk->exists($path)) {
                $this->backupDisk->delete($path);
            }
            
            return [
                'success' => false,
                'error' => $e->getMessage(),
            ];
        }
    }
    
    public function createFileBackup($directories = [])
    {
        $timestamp = Carbon::now()->format('Y-m-d_H-i-s');
        $filename = "files_backup_{$timestamp}.zip";
        $path = "files/{$filename}";
        
        $directories = $directories ?: [
            storage_path('app/public'),
            base_path('config'),
            base_path('database'),
        ];
        
        try {
            $zip = new \ZipArchive();
            $fullPath = storage_path("app/backups/{$path}");
            
            // Ensure directory exists
            if (!is_dir(dirname($fullPath))) {
                mkdir(dirname($fullPath), 0755, true);
            }
            
            if ($zip->open($fullPath, \ZipArchive::CREATE) !== TRUE) {
                throw new \Exception('Cannot create zip file');
            }
            
            foreach ($directories as $directory) {
                if (is_dir($directory)) {
                    $this->addDirectoryToZip($zip, $directory);
                }
            }
            
            $zip->close();
            
            return [
                'success' => true,
                'filename' => $filename,
                'path' => $path,
                'size' => $this->backupDisk->size($path),
                'created_at' => now(),
            ];
            
        } catch (\Exception $e) {
            return [
                'success' => false,
                'error' => $e->getMessage(),
            ];
        }
    }
    
    protected function compressFile($path)
    {
        $fullPath = storage_path("app/backups/{$path}");
        $compressedPath = $path . '.gz';
        $compressedFullPath = storage_path("app/backups/{$compressedPath}");
        
        // Read the file and compress it
        $data = file_get_contents($fullPath);
        $compressed = gzencode($data, 9);
        
        file_put_contents($compressedFullPath, $compressed);
        
        return $compressedPath;
    }
    
    protected function addDirectoryToZip($zip, $directory, $relativePath = '')
    {
        $files = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($directory),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );
        
        foreach ($files as $name => $file) {
            if (!$file->isDir()) {
                $filePath = $file->getRealPath();
                $relativeFilePath = $relativePath . substr($filePath, strlen($directory) + 1);
                
                $zip->addFile($filePath, $relativeFilePath);
            }
        }
    }
    
    public function listBackups($type = null)
    {
        $backups = [];
        
        if (!$type || $type === 'database') {
            $databaseBackups = $this->backupDisk->files('database');
            foreach ($databaseBackups as $backup) {
                $backups[] = [
                    'type' => 'database',
                    'filename' => basename($backup),
                    'path' => $backup,
                    'size' => $this->backupDisk->size($backup),
                    'last_modified' => Carbon::createFromTimestamp(
                        $this->backupDisk->lastModified($backup)
                    ),
                ];
            }
        }
        
        if (!$type || $type === 'files') {
            $fileBackups = $this->backupDisk->files('files');
            foreach ($fileBackups as $backup) {
                $backups[] = [
                    'type' => 'files',
                    'filename' => basename($backup),
                    'path' => $backup,
                    'size' => $this->backupDisk->size($backup),
                    'last_modified' => Carbon::createFromTimestamp(
                        $this->backupDisk->lastModified($backup)
                    ),
                ];
            }
        }
        
        // Sort by modification time (newest first)
        usort($backups, function ($a, $b) {
            return $b['last_modified'] <=> $a['last_modified'];
        });
        
        return $backups;
    }
    
    public function cleanupOldBackups($keepDays = 30)
    {
        $cutoffDate = Carbon::now()->subDays($keepDays);
        $backups = $this->listBackups();
        $deletedCount = 0;
        
        foreach ($backups as $backup) {
            if ($backup['last_modified']->lt($cutoffDate)) {
                $this->backupDisk->delete($backup['path']);
                $deletedCount++;
            }
        }
        
        return $deletedCount;
    }
}

Cloud Storage Integration

Amazon S3 Integration

<?php
// app/Services/S3Service.php

namespace App\Services;

use Illuminate\Support\Facades\Storage;
use Aws\S3\S3Client;

class S3Service
{
    protected $s3;
    protected $bucket;
    
    public function __construct()
    {
        $this->s3 = Storage::disk('s3');
        $this->bucket = config('filesystems.disks.s3.bucket');
    }
    
    public function uploadLargeFile($localPath, $s3Path, $chunkSize = 100 * 1024 * 1024)
    {
        // Use multipart upload for large files
        $uploader = new \Aws\S3\MultipartUploader(
            $this->s3->getClient(),
            $localPath,
            [
                'bucket' => $this->bucket,
                'key' => $s3Path,
                'part_size' => $chunkSize,
            ]
        );
        
        try {
            $result = $uploader->upload();
            return [
                'success' => true,
                'path' => $s3Path,
                'url' => $this->s3->url($s3Path),
            ];
        } catch (\Aws\Exception\MultipartUploadException $e) {
            return [
                'success' => false,
                'error' => $e->getMessage(),
            ];
        }
    }
    
    public function generatePresignedUrl($path, $expiryMinutes = 60)
    {
        $client = $this->s3->getClient();
        $command = $client->getCommand('GetObject', [
            'Bucket' => $this->bucket,
            'Key' => $path,
        ]);
        
        $request = $client->createPresignedRequest($command, "+{$expiryMinutes} minutes");
        
        return (string) $request->getUri();
    }
    
    public function getFileInfo($path)
    {
        $client = $this->s3->getClient();
        
        try {
            $result = $client->headObject([
                'Bucket' => $this->bucket,
                'Key' => $path,
            ]);
            
            return [
                'size' => $result['ContentLength'],
                'last_modified' => $result['LastModified'],
                'mime_type' => $result['ContentType'],
                'etag' => $result['ETag'],
                'metadata' => $result['Metadata'],
            ];
        } catch (\Exception $e) {
            return null;
        }
    }
    
    public function setFileVisibility($path, $visibility = 'private')
    {
        $this->s3->setVisibility($path, $visibility);
    }
    
    public function listFiles($prefix = '', $maxKeys = 1000)
    {
        $client = $this->s3->getClient();
        
        $result = $client->listObjectsV2([
            'Bucket' => $this->bucket,
            'Prefix' => $prefix,
            'MaxKeys' => $maxKeys,
        ]);
        
        $files = [];
        
        if (isset($result['Contents'])) {
            foreach ($result['Contents'] as $object) {
                $files[] = [
                    'key' => $object['Key'],
                    'size' => $object['Size'],
                    'last_modified' => $object['LastModified'],
                    'etag' => $object['ETag'],
                ];
            }
        }
        
        return $files;
    }
}

Testing File Storage

Testing with Fake Storage

<?php
// tests/Feature/FileUploadTest.php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

class FileUploadTest extends TestCase
{
    use RefreshDatabase;

    public function test_avatar_upload()
    {
        Storage::fake('public');
        
        $user = User::factory()->create();
        $file = UploadedFile::fake()->image('avatar.jpg', 100, 100);
        
        $response = $this->actingAs($user)
            ->postJson('/api/avatar', ['avatar' => $file]);
        
        $response->assertStatus(200);
        
        // Assert the file was stored
        Storage::disk('public')->assertExists('avatars/users/' . $user->id . '/medium_avatar_' . $user->id . '_*.jpg');
        
        // Assert multiple sizes were created
        $files = Storage::disk('public')->allFiles('avatars/users/' . $user->id);
        $this->assertCount(5, $files); // original, large, medium, small, thumbnail
    }
    
    public function test_document_upload_and_download()
    {
        Storage::fake('public');
        
        $user = User::factory()->create();
        $file = UploadedFile::fake()->create('document.pdf', 1000);
        
        $response = $this->actingAs($user)
            ->postJson('/api/documents', [
                'documents' => [$file],
            ]);
        
        $response->assertStatus(200);
        
        $document = $user->documents()->first();
        
        // Test download
        $downloadResponse = $this->getJson("/api/documents/{$document->id}/download");
        $downloadResponse->assertStatus(200);
    }
    
    public function test_file_validation()
    {
        Storage::fake('public');
        
        $user = User::factory()->create();
        
        // Test invalid file type
        $file = UploadedFile::fake()->create('virus.exe', 1000);
        
        $response = $this->actingAs($user)
            ->postJson('/api/documents', [
                'documents' => [$file],
            ]);
        
        $response->assertStatus(422);
        $response->assertJsonValidationErrors(['documents.0']);
    }
    
    public function test_file_deletion()
    {
        Storage::fake('public');
        
        $user = User::factory()->create();
        $file = UploadedFile::fake()->image('avatar.jpg');
        
        // Upload file
        $uploadResponse = $this->actingAs($user)
            ->postJson('/api/avatar', ['avatar' => $file]);
        
        $uploadResponse->assertStatus(200);
        
        // Delete file
        $deleteResponse = $this->actingAs($user)
            ->deleteJson('/api/avatar');
        
        $deleteResponse->assertStatus(200);
        
        // Assert file was deleted
        Storage::disk('public')->assertMissing('avatars/users/' . $user->id . '/medium_avatar_' . $user->id . '_*.jpg');
    }
}

Testing Backup Service

<?php
// tests/Unit/Services/BackupServiceTest.php

namespace Tests\Unit\Services;

use Tests\TestCase;
use App\Services\BackupService;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\RefreshDatabase;

class BackupServiceTest extends TestCase
{
    use RefreshDatabase;

    protected $backupService;
    
    protected function setUp(): void
    {
        parent::setUp();
        
        Storage::fake('backups');
        $this->backupService = new BackupService();
    }
    
    public function test_database_backup_creation()
    {
        $result = $this->backupService->createDatabaseBackup();
        
        // Since we're using fake storage, the actual backup won't be created
        // but we can test the service structure and error handling
        $this->assertIsArray($result);
        $this->assertArrayHasKey('success', $result);
    }
    
    public function test_file_backup_creation()
    {
        // Create some test files
        Storage::disk('public')->put('test-file.txt', 'Test content');
        
        $result = $this->backupService->createFileBackup([
            storage_path('app/public'),
        ]);
        
        $this->assertIsArray($result);
        $this->assertArrayHasKey('success', $result);
    }
    
    public function test_backup_listing()
    {
        // Create fake backup files
        Storage::disk('backups')->put('database/test_backup.sql.gz', 'fake content');
        Storage::disk('backups')->put('files/test_backup.zip', 'fake content');
        
        $backups = $this->backupService->listBackups();
        
        $this->assertIsArray($backups);
        $this->assertGreaterThanOrEqual(2, count($backups));
    }
    
    public function test_backup_cleanup()
    {
        // Create old backup files
        $oldBackup = 'database/old_backup.sql.gz';
        Storage::disk('backups')->put($oldBackup, 'fake content');
        
        // Manually set the last modified time to be old
        $backupPath = Storage::disk('backups')->path($oldBackup);
        touch($backupPath, time() - (35 * 24 * 60 * 60)); // 35 days ago
        
        $deletedCount = $this->backupService->cleanupOldBackups(30);
        
        $this->assertEquals(1, $deletedCount);
        Storage::disk('backups')->assertMissing($oldBackup);
    }
}

Security Best Practices

File Upload Security

<?php
// app/Http/Middleware/SecureFileUpload.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SecureFileUpload
{
    public function handle(Request $request, Closure $next)
    {
        if ($request->hasFile('file')) {
            $file = $request->file('file');
            
            // Validate file type by MIME type and extension
            $allowedMimes = [
                'image/jpeg', 'image/png', 'image/gif', 'image/webp',
                'application/pdf',
                'application/msword',
                'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                'application/vnd.ms-excel',
                'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                'text/plain',
            ];
            
            $allowedExtensions = [
                'jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf',
                'doc', 'docx', 'xls', 'xlsx', 'txt',
            ];
            
            if (!in_array($file->getMimeType(), $allowedMimes) ||
                !in_array(strtolower($file->getClientOriginalExtension()), $allowedExtensions)) {
                return response()->json([
                    'error' => 'File type not allowed',
                ], 422);
            }
            
            // Check file size (10MB max)
            if ($file->getSize() > 10 * 1024 * 1024) {
                return response()->json([
                    'error' => 'File size too large',
                ], 422);
            }
            
            // Sanitize filename
            $filename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
            $filename = preg_replace('/[^a-zA-Z0-9\-_\.]/', '', $filename);
            $filename = substr($filename, 0, 255); // Limit length
        }
        
        return $next($request);
    }
}

Secure File Serving

<?php
// app/Http/Controllers/SecureFileController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Gate;

class SecureFileController extends Controller
{
    public function downloadSecureFile(Request $request, $fileId)
    {
        $file = \App\Models\SecureFile::findOrFail($fileId);
        
        // Check authorization
        if (Gate::denies('download', $file)) {
            abort(403, 'Unauthorized access');
        }
        
        // Validate download token (if using temporary URLs)
        if ($request->has('token')) {
            if (!hash_equals($request->token, $file->generateDownloadToken())) {
                abort(403, 'Invalid download token');
            }
        }
        
        // Rate limiting
        if ($request->user()) {
            $key = 'downloads:' . $request->user()->id;
            $maxDownloads = 10; // Maximum downloads per minute
            
            if (\Illuminate\Support\Facades\Redis::get($key) >= $maxDownloads) {
                abort(429, 'Download limit exceeded');
            }
            
            \Illuminate\Support\Facades\Redis::incr($key);
            \Illuminate\Support\Facades\Redis::expire($key, 60);
        }
        
        // Serve file with security headers
        return response()->file(
            Storage::disk($file->disk)->path($file->path),
            [
                'Content-Type' => $file->mime_type,
                'Content-Disposition' => 'inline; filename="' . $file->original_name . '"',
                'X-Content-Type-Options' => 'nosniff',
                'X-Frame-Options' => 'DENY',
                'Content-Security-Policy' => "default-src 'none'",
            ]
        );
    }
    
    public function generateTemporaryUrl($fileId)
    {
        $file = \App\Models\SecureFile::findOrFail($fileId);
        
        if (Gate::denies('download', $file)) {
            abort(403, 'Unauthorized access');
        }
        
        $token = $file->generateDownloadToken();
        $url = route('secure-file.download', [
            'file' => $fileId,
            'token' => $token,
        ]);
        
        return response()->json([
            'url' => $url,
            'expires_at' => now()->addMinutes(30),
        ]);
    }
}

Performance Optimization

File Caching Strategy

<?php
// app/Services/FileCacheService.php

namespace App\Services;

use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Cache;

class FileCacheService
{
    public function getFileWithCache($path, $disk = null, $ttl = 3600)
    {
        $cacheKey = "file:{$disk}:{$path}";
        
        return Cache::remember($cacheKey, $ttl, function () use ($path, $disk) {
            return Storage::disk($disk)->get($path);
        });
    }
    
    public function getFileInfoWithCache($path, $disk = null, $ttl = 1800)
    {
        $cacheKey = "file_info:{$disk}:{$path}";
        
        return Cache::remember($cacheKey, $ttl, function () use ($path, $disk) {
            return [
                'size' => Storage::disk($disk)->size($path),
                'last_modified' => Storage::disk($disk)->lastModified($path),
                'mime_type' => Storage::disk($disk)->mimeType($path),
                'exists' => Storage::disk($disk)->exists($path),
            ];
        };
    }
    
    public function clearFileCache($path, $disk = null)
    {
        $cacheKey = "file:{$disk}:{$path}";
        $infoCacheKey = "file_info:{$disk}:{$path}";
        
        Cache::forget($cacheKey);
        Cache::forget($infoCacheKey);
    }
}

Common Interview Questions & Answers

1. What is Laravel's Storage facade and why use it?
The Storage facade provides a unified API for working with multiple filesystem drivers. It abstracts away the differences between local storage, cloud storage, and other filesystems, allowing you to switch between them easily.

2. What's the difference between local and public disks?
The local disk is for private application files, while the public disk is for user-accessible files. The public disk typically has a symbolic link to the public directory for web access.

3. How do you handle file uploads securely?
Validate file types by MIME type and extension, limit file sizes, sanitize filenames, store files outside web root when possible, and use proper authorization checks.

4. What's the purpose of storage:link?
It creates a symbolic link from public/storage to storage/app/public, making stored files accessible via the web server.

5. How do you switch between different storage drivers?
Change the FILESYSTEM_DISK environment variable or specify the disk when calling Storage methods: Storage::disk('s3')->get('file.jpg').

6. How do you handle large file uploads?
Use chunked uploads, increase PHP memory and execution time limits, use progress tracking, and consider direct-to-cloud uploads for very large files.

7. What's the difference between store() and storeAs()?
store() generates a unique filename automatically, while storeAs() allows you to specify the exact filename to use.

8. How do you test file operations?
Use Storage::fake() to create a fake filesystem for testing, then use assertion methods like assertExists(), assertMissing(), and assertDownloaded().

9. How do you generate temporary URLs for S3 files?
Use Storage::temporaryUrl($path, $expiration) which creates a time-limited URL that doesn't require authentication.

10. What are best practices for file organization?
Organize files by type and date, use meaningful directory structures, implement cleanup routines for temporary files, and consider using a CDN for frequently accessed files.

You've now mastered Laravel file storage! From basic file operations to advanced cloud storage integration and security practices, you have the complete toolkit for handling all file storage needs in your Laravel applications.