When working with files and images in Laravel, you can store them for public and private use. In this tutorial, we will explore how to do both, also looking at local server and Amazon S3 setup.
The public/
directory is the default location where Laravel serves static assets like images. By default, any file in the public directory can be accessed by users using a URL that points to the file.
If we put the cat.jpg
image under the public/images/
folder, it can be accessed via the /images/cat.jpg
URL.
Note: your web server's document root should be set to the
public/
folder of your Laravel project. All files in this directory will be publicly accessible.
It is important to address that if you add any files to the public/
directory during some method calls, and have already configured the repository, your repository will become dirty. So this is suitable only for static files which you do not intend to manage on a software level.
Now let's put the cat.jpg
file in the storage/app/public
directory and try to access it.
The storage/app/public/
directory in Laravel is a directory where you can store files that are publicly accessible to users. This directory provides a convenient location to store user-uploaded files, such as images, videos, and documents.
When files are stored in the storage/app/public/
directory, they are not directly accessible by users via URL. Instead, Laravel provides a symbolic link to the files in the public/
directory, making them accessible to users through a URL.
To create a symbolic link to the storage/app/public
directory, you can use the php artisan storage:link
command. This will create a symbolic link from the public/storage
path to the storage/app/public/
directory.
Once the symbolic link has been created, files stored in the storage/app/public/
directory can be accessed via a URL that starts with storage/
.
If you are using Laravel Valet, the
/storage/
URL of your application is always public in your local environment even if you did not run thephp artisan storage:link
command.
There are cases where files should not be directly accessible by users via URL. These files typically include sensitive data such as financial data, personal data, or invoices.
Let's put the invoice.pdf
file into the storage/app/private
folder. This directory is not accessible via a URL, and files stored there are only accessible to the application.
To access private files we can use the Storage
facade, which provides a download()
method like this:
routes/web.php
use Illuminate\Support\Facades\Storage; Route::middleware('auth')->get('/download', function () { // There can be more logic to check if the user is eligible for a download $condition = true; if ($condition) { return Storage::download('invoice.pdf'); } return response()->noContent();});
In our case user should be logged in because of auth
middleware and satisfy additional conditions if needed, to validate eligibility to download the invoice.
If you want to store the files separately on Amazon S3 servers, instead of your own local server, there's a bit of work to set it all up.
Before using the S3 driver, you will need to install the Flysystem S3 package via the Composer package manager:
composer require league/flysystem-aws-s3-v3 "^3.0"
The S3 driver configuration information is located in your
config/filesystems.php
configuration file. This file contains an example configuration array for an S3 driver and by default inherits values from the.env
file. Usually, you do not need to change anything there.
arn:aws:iam::*****:user/****
and will be required when setting permissions for the bucket.Secret access key is displayed only once in this view, so if you fail to save that key you need to create new keys.
Bucket names must be globally unique and must not contain spaces or uppercase letters. This means the bucket name should be unique in the region, not only for your account.
When choosing a region note region name, for example eu-central-1
, you will need to define that too in your Laravel app.
The rest can be left with the default settings, scroll to the bottom of the page and press Create bucket
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Statement1", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::****:user/****" }, "Action": [ "s3:DeleteObject", "s3:GetObject", "s3:PutObject", "s3:ReplicateObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::<YOUR-BUCKET_NAME>", "arn:aws:s3:::<YOUR-BUCKET_NAME>/*" ] } ]}
This configuration will allow to list/add/retrieve/delete objects in the bucket for the IAM User using access keys.
Update the Principal.AWS
value with your IAM User ARN. It should look like this arn:aws:iam::*****:user/****
.
Update the Resource
section with your bucket name. We apply these permissions for two resources. The first one is the bucket itself and the second one is for objects.
Update your .env
file in your Laravel project by providing AWS credentials, region, and bucket name.
.env
AWS_ACCESS_KEY_ID=********AWS_SECRET_ACCESS_KEY=********AWS_DEFAULT_REGION=<REGION-NAME>AWS_BUCKET=<BUCKET-NAME>AWS_USE_PATH_STYLE_ENDPOINT=false
Now we can perform all needed operations using S3 API on our new bucket.
To upload the locally stored files into S3 typically your code will look as follows:
use Illuminate\Support\Facades\Storage; $key = 'invoices/invoice.pdf';$contents = file_get_contents('storage/app/private/invoice.pdf'); Storage::disk('s3')->put($key, $contents);
Or if it is a controller with a form:
upload.blade.php
<form action="{{ route('invoice.store') }}" method="POST" enctype="multipart/form-data"> @csrf <input type="file" name="invoice"> <button type="submit"> Submit </button></form>
The file can be stored on S3 straight from the request and omitting the Storage
facade like in this example:
app/Http/Controllers/InvoiceController.php
public function store(Request $request){ if ($request->hasFile('invoice')) { $file = $request->file('invoice'); $file->storeAs('invoices', $file->getClientOriginalName(), 's3'); } // ...}
In Amazon S3 directories don't have a physical existence. Instead, each file is referred to as an "object" and is identified by its unique "object key" consisting of the file path and name.
The following example illustrates how to allow users to download files stored in a bucket from the invoices/
folder by visiting the /invoices/invoice.pdf
URL. Laravel now works as a proxy server between your S3 bucket and the user.
routes/web.php
Route::middleware('auth') ->get('/invoices/{name}', [InvoiceController::class, 'show']) ->name('invoice.show');
app/Http/Controllers/InvoiceController.php
public function show(string $name){ // ... if (! $canDownload) { abort(403); } $disk = Storage::disk('s3'); $key = 'invoices/' . $name; if (! $disk->fileExists($key)) { abort(404); } return $disk->download($key);}
This way all your files on Amazon S3 are private and are only accessible by logged-in users through your application.
Objects can be public. This bucket isn't public, but anyone with the appropriate permissions can grant public access to its objects.
And add the second statement:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Statement1", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::****:user/****" }, "Action": [ "s3:DeleteObject", "s3:GetObject", "s3:PutObject", "s3:ReplicateObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::<YOUR-BUCKET_NAME>", "arn:aws:s3:::<YOUR-BUCKET_NAME>/*" ] }, { "Sid": "Statement2", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::<YOUR-BUCKET_NAME>/images/*" } ]}
This time we have only one permission s3:GetObject
in the Actions
. Notice the Principal
value is *
which means Everyone
. The resource we are granting permission to get objects for everyone is all objects in the images
"folder".
We can upload the same cat.jpg
picture we used earlier in this tutorial with this snippet:
use Illuminate\Support\Facades\Storage; $key = 'images/cat.jpg';$contents = file_get_contents('public/images/invoice.pdf'); Storage::disk('s3')->put($key, $contents);
Image URL can be get using the url()
method:
$url = Storage::disk('s3')->url('images/cat.jpg');
URL will look as follows:
"https://<BUCKET-NAME>.s3.<REGION-NAME>.amazonaws.com/images/cat.jpg"
And when we visit that URL image appears. Now you can use it to display your assets or user content.
To verify that only image files are public and not the invoices we can list all the files in the bucket with the URLs.
$urls = collect(Storage::disk('s3')->allFiles())->map(function ($key) { return [ 'key' => $key, 'url' => Storage::disk('s3')->url($key), ];});
Which will give the following collection:
[ 0 => [ "key" => "images/cat.jpg" "url" => "https://<BUCKET-NAME>.s3.<REGION-NAME>.amazonaws.com/images/cat.jpg" ], 1 => [ "key" => "invoices/invoice.pdf" "url" => "https://<BUCKET-NAME>.s3.<REGION-NAME>.amazonaws.com/invoices/invoice.pdf" ]]
All objects in the bucket have the URL, but that doesn't mean they are accessible via public URL. If we try to access the invoice the following screen will show up: