0

Flysystem abstraction — local, S3, GCS disks

Intermediate5 min read·lv-23-001

Concept

Laravel's filesystem abstraction is built on top of the Flysystem library by Frank de Jonge. Flysystem provides a unified interface (FilesystemAdapter) that works identically regardless of whether the underlying storage is a local directory, Amazon S3, Google Cloud Storage, Azure Blob Storage, Cloudflare R2, or any other supported driver. Your application code calls Storage::put(), Storage::get(), Storage::delete() — and Flysystem translates those calls to the correct provider API.

The Flysystem version bundled with Laravel has evolved: Laravel 8 upgraded from Flysystem v1 to v3, which introduced breaking changes in the adapter API. If you maintain older Laravel applications, be aware that the local adapter path handling and visibility constants changed between versions.

A "disk" in Laravel is a named, preconfigured Flysystem instance. You define disks in config/filesystem.php under the disks key. Each disk has a driver (local, s3, gcs, ftp, sftp) and driver-specific configuration. The default disk key determines which disk Storage:: calls use without an explicit ->disk() selector.

The local driver stores files on the server filesystem. By default, Laravel ships with two local disks: local (stored in storage/app, not accessible via HTTP) and public (stored in storage/app/public, symlinked to public/storage by artisan storage:link). The S3 driver uses the league/flysystem-aws-s3-v3 package and supports any S3-compatible API — including Cloudflare R2, DigitalOcean Spaces, and MinIO.

A critical architectural point: the Flysystem abstraction means swapping from local to S3 is a one-line change to your .env file, with zero changes to application code. This is the pattern's primary value — code against Storage:: and your application remains cloud-agnostic.

DriverPackageUse Case
local(built-in)Development, single-server apps
s3league/flysystem-aws-s3-v3AWS, R2, Spaces, MinIO
gcssuperbalist/flysystem-google-storageGoogle Cloud
ftpleague/flysystem-ftpLegacy FTP servers
sftpleague/flysystem-sftp-v3Secure FTP

Code Example

php
// 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'), // for S3-compatible APIs like MinIO
            'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
        ],

        // Cloudflare R2 — S3-compatible, no egress costs
        'r2' => [
            'driver'   => 's3',
            'key'      => env('R2_ACCESS_KEY_ID'),
            'secret'   => env('R2_SECRET_ACCESS_KEY'),
            'region'   => 'auto',
            'bucket'   => env('R2_BUCKET'),
            'endpoint' => 'https://' . env('R2_ACCOUNT_ID') . '.r2.cloudflarestorage.com',
            'use_path_style_endpoint' => true,
        ],
    ],
];

// Usage — driver-agnostic regardless of disk configured
use Illuminate\Support\Facades\Storage;

Storage::put('reports/monthly.csv', $csvContent);
$contents = Storage::get('reports/monthly.csv');
$exists   = Storage::exists('reports/monthly.csv');
Storage::delete('reports/monthly.csv');

// Switch disk explicitly
Storage::disk('s3')->put('uploads/avatar.jpg', $imageData);
Storage::disk('r2')->url('media/video.mp4');

Interview Q&A

Q: What is Flysystem and why does Laravel use it instead of direct filesystem calls?

Flysystem is a filesystem abstraction library that normalizes the API for working with different storage backends behind a single interface. Laravel uses it so application code remains driver-agnostic — the same Storage::put() call works whether the disk is configured as local, S3, GCS, or SFTP. This matters in practice because development environments typically use local storage while production uses S3 or a CDN-backed object store. Without an abstraction layer, every storage operation would need conditional logic or a manual switch, and adding new storage backends would require touching application code. Flysystem also handles path normalization, atomic stream operations, and visibility management in a consistent way across adapters.


Q: What is the difference between the local and public disks that ship with Laravel, and what does artisan storage:link do?

The local disk is rooted at storage/app and is not reachable via a web URL — it's for files that should never be publicly accessible (documents, temporary files, private data). The public disk is rooted at storage/app/public and is intended for user-uploaded files that need to be served over HTTP. artisan storage:link creates a symbolic link from public/storage to storage/app/public, making the public disk's files accessible at https://yourdomain.com/storage/filename. This works in development but many production deployments serve files directly from S3 URLs, making the symlink unnecessary.


Q: How do you swap from local storage to S3 without changing any application code?

Change FILESYSTEM_DISK=s3 in your .env file and ensure the S3 credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKET) are set. As long as all storage operations go through the Storage facade or a disk instance rather than direct PHP filesystem calls (file_put_contents, fopen, etc.), no code changes are needed. This is exactly why you should never bypass the Storage abstraction — if even one place calls file_put_contents(storage_path('app/upload.jpg'), $data), it breaks when you move to S3. Enforce discipline: all I/O goes through Storage::.