Jun 17, 2022 by Thibault Debatty | 13557 views
https://cylab.be/blog/223/file-upload-validation-and-storage-with-laravel
Laravel is a powerful framework, that offers all functionalities required to implement file upload, validation and storage. Here is a complete and concrete example.
This example is taken from our cyrange Cyber Range project. For this project we have a list Virtual Machine (VM) images that can be deployed by the user. VM images are modelized using a class AppImage
. For this example, we want to add a screenshot for each VM image, so the user has an idea of what to expect when the VM is deployed.
For this we will:
The upload form is actually the easiest part. You can put it in a dedicated view, or add it to an existing view:
<form method="POST"
action="{{ action("ImageController@uploadScreenshot", ["image" => $image]) }}"
enctype="multipart/form-data">
{{ csrf_field() }}
<input class="form-control mb-2" type="file"
accept="image/png"
id="screenshot" name="screenshot">
<button type="submit" class="btn btn-primary">
<i class="fas fa-upload"></i> Upload
</button>
</form>
There are 4 attention points here:
ImageController@uploadScreenshot
);enctype="multipart/form-data"
form parameter;{{ csrf_field() }}
;<input accept=''>
parameter, but the actual file type will be validated in the controller.In routes/web.php, we must add a route for our new controller method:
Route::post(
'app/images/{image}/screenshot',
'ImageController@uploadScreenshot');
And in app/Http/Controllers/ImageController.php we can add the controller method.
use IlluminateSupportFacadesStorage;
/**
* Upload screenshot
* https://cylab.be/blog/223/file-upload-validation-and-storage-with-laravel
* @param Image $image
*/
public function uploadScreenshot(Image $image, Request $request)
{
$request->validate([
"screenshot" => [
'required', 'file',
'mimes:png',
// max file size in kB
'max:1024',
// for images, specify dimension constraints
// https://laravel.com/docs/9.x/validation#rule-dimensions
'dimensions:min_width=500,max_width=1500'
]
]);
// store screenshot
$filename = Storage::putFile(
'public/images/screenshots',
$request->file('screenshot'));
$image->screenshot = $filename;
$image->save();
return redirect(action('ImageController@show', ['image' => $image]));
}
In this method, we
$request->validate()
to validate the type and size of the uploaded file;Storage
facade to save the uploaded file. There are different possibilities here that will be explained below…Laravel provides a Storage
facade to handle file storage. As the name states, this is a facade that provides a uniform interface for different implementations. The most commonly used implementation drivers are local
and s3
.
local
driver will store files in (and fetch files from) the local directory storage/app
;s3
driver will store files in a cloud storage compatible with Amazon S3 API (so it can be AWS S3 itself, or any other like MinIO, wasabi, ceph etc.)The implementation used by your application can be configured in config/filesystems.php and in your .env file.
Moreover, there are different ways to use the Storage facade:
So it makes a total of 8 (2 x 2 x 2) combinations, each with pros and cons.
For example, if you are using the s3 driver, with auto-generated names and public files:
However:
u3f8eu6s1sRefSsDiGBAdF2zW.png
, for an image integrated in a view this is not an issue, but for a PDF that can be downloaded this is not an option…In this example, we are be using a local driver, with auto-generated names and public files. This also has some pros and cons:
But:
storage/app/public
directory;public/storage
to storage/app/public
.So first we will need a database migration to add a new column screenshot
where we can save the auto-generated filename:
php artisan make:migration --table images images_add_screenshot
with the following content:
public function up()
{
Schema::table('images', function (Blueprint $table) {
$table->string('screenshot')->nullable();
});
}
and run the migration:
php artisan migrate
The code to store file is already present in the example controller above:
$filename = Storage::putFile(
'public/images/screenshots',
$request->file('screenshot'));
$image->screenshot = $filename;
$image->save();
In this example we use the Storage::putFile()
method to save the uploaded file in the public/images/screenshots
directory. We choose to make the file public, so it must be somewhere in the ‘public’ directory. Indeed, with the ‘local’ driver, this corresponds to the actual directory storage/app/public/images/screenshots
, which will be publicly accessible thanks to the symlink that we will create…
The Storage::putFile()
also return the unique filename generated, that we must save in the database.
NB: if you want to choose the filename yourself, you can use the method Storage::putFileAs($directory, $file, $filename)
When using the ‘local’ driver with public files, we must create a symlink from from public/storage
to storage/app/public
to make public files accessible. You can create this symlink with artisan:
php artisan storage:link
Storage offers multiple methods to retrieve and use files:
$filename = $image->screenshot;
$contents = Storage::get($filename);
$size = Storage::size($filename);
$unix_timestamp = Storage::lastModified($filename);
// check if the file exists:
Storage::exists($filename);
// download the file in the browser of the user
// useful for files that are *not* public, combined with access control
if (is_allowed()) {
return Storage::download($filename);
}
We chose that these files are public, so we can simply use the public URL of the file in our views:
<img src="{{ Storage::url($image->screenshot) }}">
This blog post is licensed under CC BY-SA 4.0