Send, validate, and store Base64 files with Laravel

Goran PopovićGoran Popović
5 min read

Laravel makes it easy to send and upload files, at least when it comes to handling binary files. You would typically send binary files using an HTML form with a multipart/form-data encoding type. Inside your controller, you can simply get the file like this: $request->file('image') and save it on the disk. The file you receive returns an instance of the Illuminate\Http\UploadedFile class, which provides you with a lot of handy methods to manipulate the received file further. Pretty easy and straightforward, right?

But what if you want to send and receive files with JSON requests? Things get a bit tricky there since JSON requests can't transport binary data. You would have to convert your file to a Base64 string and then process it within your application to extract the necessary data. Let's see how we can do that.

Converting the image to Base64

First, let's see how we can convert an image of our choosing to a Base64 string. This can be of course done programmatically, but for simplicity, we are just going to upload a simple PNG image on a Base64 Guru website (you can, of course, use any other), and save the output for later use.

Here is an example of what that might look like:

Converting an image

Postman example

Now, before we proceed, let's see a complete example of that kind of request in Postman. So, in short, we will be sending a raw JSON POST request with a single image param, and in return, we will just get the URL of the newly stored file.

That might look something like this:

Postman example

Initial setup

Ok, we know what to expect. Let's create a controller and a route to handle the request.

php artisan make:controller Base64Controller
// api.php

use App\Http\Controllers\Base64Controller;

Route::post('base64', [Base64Controller::class, 'index']);

So far, so good. Our base64 route will target the index method inside of the Base64Controller. Next, we will add some basic code to handle the JSON request in the index method.

// Base64Controller.php

namespace App\Http\Controllers;

use Illuminate\Http\File;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;

class Base64Controller extends Controller
{
    public function index()
    {
        if (request()->isJson() && !empty(request()->input('image'))) {
            $base64Image = request()->input('image');

            if (!$tmpFileObject = $this->validateBase64($base64Image, ['png', 'jpg', 'jpeg', 'gif'])) {
                return response()->json([
                    'error' => 'Invalid image format.'
                ], 415);
            }

            $storedFilePath = $this->storeFile($tmpFileObject);

            if(!$storedFilePath) {
                return response()->json([
                    'error' => 'Something went wrong, the file was not stored.'
                ], 500);
            }

            return response()->json([
                'image_url' => url(Storage::url($storedFilePath)),
            ]);
        }

        return response()->json([
            'error' => 'Invalid request.'
        ], 400);
    }

We are first checking if we are dealing with a JSON request and if the image parameter is not empty. We then validate and upload the image. In the end, we return the URL of the newly uploaded file. Simple enough, right? You may have noticed that we have defined two helper functions, validateBase64 and storeFile that are validating and storing the file for us. Time to define them.

Helper functions

The first function (validateBase64) will just check if the image param is a Base64 representation of an image. It will accept a Base64 string and an array of allowed mime types. If the validation doesn't fail, a temporary file object will be returned. The function is mostly based on this Stack Overflow answer.

// Base64Controller.php

/**
 * Validate base64 content.
 *
 * @see https://stackoverflow.com/a/52914093
 */
private function validateBase64(string $base64data, array $allowedMimeTypes)
{
    // strip out data URI scheme information (see RFC 2397)
    if (str_contains($base64data, ';base64')) {
        list(, $base64data) = explode(';', $base64data);
        list(, $base64data) = explode(',', $base64data);
    }

    // strict mode filters for non-base64 alphabet characters
    if (base64_decode($base64data, true) === false) {
        return false;
    }

    // decoding and then re-encoding should not change the data
    if (base64_encode(base64_decode($base64data)) !== $base64data) {
        return false;
    }

    $fileBinaryData = base64_decode($base64data);

    // temporarily store the decoded data on the filesystem to be able to use it later on
    $tmpFileName = tempnam(sys_get_temp_dir(), 'medialibrary');
    file_put_contents($tmpFileName, $fileBinaryData);

    $tmpFileObject = new File($tmpFileName);

    // guard against invalid mime types
    $allowedMimeTypes = Arr::flatten($allowedMimeTypes);

    // if there are no allowed mime types, then any type should be ok
    if (empty($allowedMimeTypes)) {
        return $tmpFileObject;
    }

    // Check the mime types
    $validation = Validator::make(
        ['file' => $tmpFileObject],
        ['file' => 'mimes:' . implode(',', $allowedMimeTypes)]
    );

    if($validation->fails()) {
        return false;
    }

    return $tmpFileObject;
}

Great, now let's define the second helper function that will process the temporary file object that was returned from the previous function and upload the file.

// Base64Controller.php

/**
 * Store the temporary file object
 */
private function storeFile(File $tmpFileObject)
{
    $tmpFileObjectPathName = $tmpFileObject->getPathname();

    $file = new UploadedFile(
        $tmpFileObjectPathName,
        $tmpFileObject->getFilename(),
        $tmpFileObject->getMimeType(),
        0,
        true
    );

    $storedFile = $file->store('images/base64', ['disk' => 'public']);

    unlink($tmpFileObjectPathName); // delete temp file

    return $storedFile;
}

And just like that, we are done. If you would like to copy the whole code from Base64Controller.php head out to Gist.

Conclusion

Today we made a few great functions that would allow us to accept, validate, and store Base64 encoded files. In our example, we were validating images, but you can use these functions for other types of files as well, like .txt, .pdf, and so on. Also, for simplicity, all of the code was added to the same controller, but you could also move these functions to your custom helper function file, or create a trait, and then easily reuse them whenever you need to process a Base64 file.


Enjoyed the article? Consider subscribing to my newsletter for future updates.


Originally published at geoligard.com.

0
Subscribe to my newsletter

Read articles from Goran Popović directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Goran Popović
Goran Popović

Software developer, enjoys working with Laravel, blogging at https://geoligard.com.