DEV Community

fractalbit
fractalbit

Posted on • Edited on

Tips for working with private files in laravel

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';
Enter fullscreen mode Exit fullscreen mode

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 -->
Enter fullscreen mode Exit fullscreen mode

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');
        }
    }
Enter fullscreen mode Exit fullscreen mode
    <!-- In a blade view -->
    <a href="/file/download/1253">Download</a>
Enter fullscreen mode Exit fullscreen mode

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');
        }
    }
Enter fullscreen mode Exit fullscreen mode
    <!-- In a blade view -->
    <img src="/file/serve/1253">
Enter fullscreen mode Exit fullscreen mode

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;
        });
    }
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
johninkesta profile image
John

Instead of

return response()->file($filepath);
Enter fullscreen mode Exit fullscreen mode

you can also

return Storage::response($file->filename);
Enter fullscreen mode Exit fullscreen mode

Also there are some nice helper functions:

abort_if(!auth()->user(), 403);
$this->authorize('view', $someModelAssociatedWithFile);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
Sloan, the sloth mascot
Comment deleted
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
mansourm profile image
Seyed Mansour Mirbehbahani • Edited

hey, good article. can you to elaborate on FileModel ?