Let's start by saying this is not a full guide about working with files in laravel. It will not cover how to upload files for example or provide a full working example that you can test-drive yourself. But I will highlight some things I learned working with non-public files as my journey to learn laravel continues. I encourage you to chime in the comments if you have any suggestions/improvements.
When building applications that users and authentication/authorization is involved you should not store files that belong to users in the public folder. That would mean anyone with the url could download the file and you definetely don't want this. One thought might be to have non-standard filenames stored in the database. For example you could append a long and random string of characters at the end of the filename.
$filename = 'report_' . \Str::random(20) . '.pdf';
That way, you cannot easily guess the filename and by default you cannot browse the public folder for a list of available files. But this is not a bulletproof solution. If the filename leaks, one way or another, anyone could download the file without even logging in. In some cases you may actually want this, but in our case lets say we only want the owner to be able to download the file. So what can we do?
First, we should store the file in a folder like storage\app\userfiles
and not in storage\app\public
. The files there are secure, but how do you provide a link to the user to download the file if they are not accessible?
<!-- Some view.blade.php file -->
<!-- url and asset helpers will result in a 404, file not found error -->
<a href="{{ url('app/userfiles/report1253.pdf') }}">Download</a>
<!-- Because the file is simply not accessible outside our app code while not in the public folder -->
What we can do, is create a route that points to a Controller method and that method is responsible to do all the authentication/authorization needed before returning a download response, so the owner can get the file. Let's see an example...
// In web.php
Route::get('/file/download/{file}', [FileAccessController::class, 'download']);
// In FileAccessController.php
public function download(FileModel $file)
{
// We should do our authentication/authorization checks here
// We assume you have a FileModel with a defined belongs to User relationship.
if(Auth::user() && Auth::id() === $file->user->id) {
// filename should be a relative path inside storage/app to your file like 'userfiles/report1253.pdf'
return Storage::download($file->filename);
}else{
return abort('403');
}
}
<!-- In a blade view -->
<a href="/file/download/1253">Download</a>
If the file is, for example, a private image and we want to display it (instead of downloading) only to the user it belongs, we could return a file response instead.
// In web.php
Route::get('/file/serve/{file}', [FileAccessController::class, 'serve']);
// In FileAccessController.php
public function serve(FileModel $file)
{
if(Auth::user() && Auth::id() === $file->user->id) {
// Here we don't use the Storage facade that assumes the storage/app folder
// So filename should be a relative path inside storage to your file like 'app/userfiles/report1253.pdf'
$filepath = storage_path($file->filename);
return response()->file($filepath);
}else{
return abort('404');
}
}
<!-- In a blade view -->
<img src="/file/serve/1253">
The above techniques are also useful when working with files through ajax requests. Let's say we generate a large pdf file that takes 10 seconds to create. We want to show the user a loader and after the file is created, hide the loader and prompt the user to download the file. I found some solutions on how to accept a blob response in javascript/jquery but i didn't like it much. It felt too "hacky". So instead, we could just return an id of the file, that could be used to redirect the user to the correct route after the file is created...
// Jquery example, sorry! :P
function ajaxExport(){ // ex. invoked from a button click
$('#loader').show();
$.get('/generate-time-consuming-pdf', function( fileId ) {
$('#loader').hide();
if(fileId) window.location = '/file/download/' + fileId;
});
}
That's it for now. I hope you found the article useful and i would love to hear your feedback.
If you like this article, you may also like my tweets. Have a look at my Twitter profile.
Top comments (6)
Instead of
you can also
Also there are some nice helper functions:
hey, good article. can you to elaborate on
FileModel
?