Manipulating uploaded files in Laravel

We recently ran into a situation where the business logic of a project dictated that we can not have duplicate file contents. Regardless the naming convention, the contents of each file needed to remain unique. In this scenario, we were storing all types of uploaded files in a single directory.

To prevent content duplication, we needed to compare the newly uploaded file with files already in the destination directory.

Unfortunately, the Laravel documentation is not clear about the uploaded file class. Digging into the Laravel source code, you may find that instances returned by the file() method (from the request) are inherited from SplFileInfo. This means it is possible to manipulate the uploaded file with little hassle. Better yet, we can do this before storing the file.

You may find the evidence of the SplFileInfo class in File. It extends that class:

class File extends \SplFileInfo  
{


This reference to Symfony code may look odd if you are not familiar with the Laravel architecture, however Laravel uses lots of Symfony's components to form it's foundation.

Lets dive in and see how this class can be leveraged in our apps.


Handle upload

A basic file upload action may get the file instance from the request, check if an actual file was uploaded and finally store it. That may look something like this:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;  
use Symfony\Component\HttpFoundation\Response;

class FileController extends Controller  
{

    /**
     * Handle the file upload.
     *
     * @param  Request $request The request instance.
     * @return Response A response instance.
     */
    public function upload(Request $request)
    {

        /*
         * The form field was named "file".
         */
        $file = $request->file('file');

        if (empty($file)) {
            abort(400, 'No file was uploaded.');
        }

        $path = $file->store('uploads');

        // Do whatever you have to with the file.

    }
}


Read and compare file chunks

Using the aforementioned file instance, we may write a function to compare the file with another SpfFileInfo instance. To speed things up, we don't have to compare the entire file. Its possible to read a small chunk of same size from each file and check the differences between them. As soon as a difference is found, the method returns.

/**
 * Compare the contents of two files to check if they have some difference.
 *
 * It does not check what exactly is different between files. This way, as 
 * soon as a difference is found, the function stops reading the files.
 *
 * @param  SplFileInfo $a The first file to compare.
 * @param  SplFileInfo $b The second file to compare.
 * @return bool Indicates if the files have differences between them.
 */
private function fileDiff($a, $b)  
{
    $diff = false;
    $fa = $a->openFile();
    $fb = $b->openFile();

    /*
     * Read the same amount from each file. Breaks the loop as soon as a 
     * difference is found.
     */
    while (!$fa->eof() && !$fb->eof()) {
        if ($fa->fread(4096) !== $fb->fread(4096)) {
            $diff = true;
            break;
        }
    }

    /*
     * Just one of the files ended. This is unlikely to happen, though. 
     * Since we already checked before if the files have the same size.
     */
    if ($fa->eof() !== $fb->eof()) {
        $diff = true;
    }

    /*
     * Closing handlers.
     */
    $fa = null;
    $fb = null;

    return $diff;
}


What about performance?

Our function above is a good way of comparing two files contents. However, opening, reading and comparing the contents of every file in our uploads directory will be far too resource consuming (time, I/O, processing). We can significantly reduce the performance overhead by comparing the file sizes first, and only checking the content difference of matching size files. Here is what that would look like:

/**
 * Check if the given file was already uploaded.
 *
 * It loops through all files in the uploads directory, looking for anyone that 
 * could be equal to the currently uploaded file.
 *
 * @param  SplFileInfo $file The file to check.
 * @return bool Indicates if the file was already upload.
 */
private function isAlreadyUploaded($file)  
{
    $size = $file->getSize();

    /*
     * The directory where the files are stored.
     */
    $path = storage_path('app/uploads/');

    if (!is_dir($path)) {
        return false;
    }

    $files = scandir($path);
    foreach ($files as $f) {
        $filePath = $path . $f;
        if (!is_file($filePath)) {
            continue;
        }

        /*
         * If both files have the same size, check their contents.
         */
        if (filesize($filePath) === $size) {

            /*
             * Check if there are differences using the function we wrote above.
             */
            $diff = $this->fileDiff(new \SplFileInfo($filePath), $file);

            /*
             * Return the files are not different, meaning equal, that is 
             * already uploaded.
             */
            return !$diff;
        }
    }
    return false;
}


Handle and store file

Finally, we can call the last method from the upload action. Remember to only store the file after checking if it was previously uploaded.

/**
 * Handle the file upload.
 *
 * @param  Request $request The request instance.
 * @return Response A response instance.
 */
public function upload(Request $request)  
{

    /*
     * The form field was named "file".
     */
    $file = $request->file('file');

    if (empty($file)) {
        abort(400, 'No file was uploaded.');
    }

    /*
     * There is a file equal to the currently uploaded one in the uploads dir?
     */
    if ($this->isAlreadyUploaded($file)) {
        abort(400, 'This file was already imported.');
    }

    /*
     * Only stores the file after successfully checking if it is not at the 
     * destination already.
     */
    $path = $file->store('uploads');

    // Do whatever you have to with the file.

}



Conclusion

This is just one way to take advantage of the file roots to manipulate it. There are probably lots of other use cases where you may leverage this.

Also, its important to point out that this is just a helper given by the framework. You can certainly take other approaches to achieve the same result. For instance, its possible to store the file to a temporary path and then run all the required checks. Or use other file-related functions from PHP to manipulate the uploaded file.

Thanks for reading! If you have any comments or questions please leave them below.