Storing images in public folder and serving them from controller

While working ony my first Laravel application imageshare.code4fun.io which was inspired from imgur.com I had to figure out how to best store the images in the public folder. Looking at imgur and the links it servers which look like this

http://i.imgur.com/MklswxJ.jpg
http://i.imgur.com/3ZwfxBa.jpg
http://i.imgur.com/LSC4baf.jpg

I asumed that imgur was storing all those millions of images in a same folder so I proceded to do the same storing all uploaded images directly inside public folder. Later I did some research how many files you can have in a folder and if it would have any performance penalties the more files you had. I found out that you should definitly not store lots of images in the same folder.

So I was puzzled how imgur was then storing it's images since they all appeared to not have any folders added to the url. Following is the solution I implemented for my application that generated the links same as imgur does for it's images. When uploading an image I do this:

    $current = Carbon::now();
    $year = $current->year;
    $month = $current->month;
    $day = $current->day;
    $hour = $current->hour;
    $folder = 'uploads/'.$year.'/'.$month.'/'.$day.'/'.$hour;

    if( ! file_exists($folder)) {
        $oldmask = umask(0);
        mkdir($folder, 0775, true);
        mkdir($folder . '/thumb/', 0775, true);
        umask($oldmask);
    }

First I create a upload folder inside public folder, this folder is called uploads. Inside this folder I create a folder for image to be uploaded based on this format public/uploads/year/month/day/hour If your website grows in popularity and lots of images are saved each hour, you could then also add minutes and even seconds to the upload folder.

Before I create the folder I check if it already exists and if not I create the folder and then save the images inside that folder. So now it's possible to access images using for example www.mywebsite.com/uploads/year/month/day/hour/image_name.jpg However that is not a short and nice looking url I was looking for. I really wanted to be able to server the images without specifing the folder path in the url, I just wanted to use image name.

To do that I added these fields to my images datatable:

    $column = $table->string('hash')->unique();
    $table->index('hash');
    $column->collation = 'utf8_bin';
    $table->string('slug')->unique()->nullable();
    $table->string('extension');
    $table->string('bucket')->nullable();
    $table->string('path')->nullable();

Hash is a generated at upload and set as image name that must be unique for each image and it's used to look up the specific image. As you can see I specify it's collation to be utf8_bin since I want the hash to be case sensitive so that HASH.jpg and hash.jpg be treated as unique values and not the same value. Since image hash is generated to be a-z,A-Z,0-9 and not having it case sensitive would be problematic if you are trying to look up images that have same name but different case.

Path of the image is stored in the path column and extension in the extension column. So when I user does something like this www.myimagewebsite.com/LSC4baf.jpg I look inside the images table for hash of LSC4baf and if I find one I can render the image directly from the controller. Here is how route and controller code looks:

Route::get('{hash}.{extension}', 'ImageController@image')->where('hash', '[a-zA-Z0-9-]+')->where('extension', '[jpg, jpeg, gif, png, mp4, bmp]+');

 

public function image($hash, $extension)
{
    $image = null;

    $image = Cache::rememberForever('image_'.$hash, function() use($hash){
        return Image::where('hash', $hash)->first();
    });
    // return ImgResizer::make($image->hash . '.jpg')->response($image->extension);

    if($image == null)
        abort(404, 'The image you are looking for could not be found.');

    $imagefile = file_get_contents($image->path.'/'.$image->hash . '.' . $image->extension);

    header('Content-type: image/'.$image->extension.';');
    header("Content-Length: " . strlen($imagefile));
    echo $imagefile;
}

So as you can see route looks for {hash}.{extension} and I limit extension only to these values jpg, jpeg, gif, png, mp4, bmp and hash only to a-zA-Z0-9 characters. So when user looks up image like this www.mywebsite.com/LSC4baf.jpg to user it looks like all images are stored in the same folder. User never knows where the images are really stored. This way you could build file hosting website and store all uploads inside public folder using method above and only restrict file access to users that are registered members. Noone can access files that are stored in the public folder since they do not know where uploaded files are stored and can't access the files without having membership on your website.

As you can see query to database is cached so it's only going to run first time image is accessed and be remembered forever. If you delete the image you then need to invalidate cache for this image so that cache does not return an image that does not exist.

Hope you enjoyed the short tutorial and if you have questions ask them below.