DEV Community

fractalbit
fractalbit

Posted 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 files while building applications 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 cannot 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 (authorize via link sharing). But in our case lets say we only want the owner to be able to download the file. So what can you do?

First, you 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 you 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)
    {
        // You should do your 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) {
            $filepath = storage_path($file->filename);
            // filename should be a relative path to your file like 'app/userfiles/report1253.pdf'
            return response()->download($filepath);
        }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 ex. a private image and you want to display it (instead of downloading) only to the user it belongs, you 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) {
            $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="{{ route('/file/serve/' . $fileId) }}">
Enter fullscreen mode Exit fullscreen mode

The above techniques are also useful when working with files through ajax requests. Let's say you create a large pdf file that takes 10 seconds to create. You would want to show the user a loader and after the file is created, hide the loader and prompt him 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, you 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
        event.preventDefault();

        $('#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.

Top comments (0)