laravel-medialibrary is a powerhouse package that can help handle media in a Laravel application. It can organise your files across multiple filesystems, generate thumbnails, optimize images and much much more.

My team and I are hard at work creating a new major version, v7, that adds a lot of awesome features such as responsive images, vue components to assist with uploading, single file collections, ... In this post I'd like to highlight one cool handy feature that's coming to v7: multi file downloads.

In our package every file is associated with a Media model. To download a single file you can do this in a controller:

public function download(Media $media) 
{
   return response()->download($media->getPath());
}

Because Media implements Laravel's Responsable interface you can also write that a bit shorter:

public function download(Media $media) 
{
   return $media;
}

Pretty sweet! But what if you want to download multiple files at once? Currently the medialibrary won't help you with this and you need to take care of this yourself. You could generate a zip file with all media you want to download on your server and then do

return response()->download($pathToZippedMediaFiles);

But maybe you want to download big files, in that case creating the zip takes a while. Or maybe the files are stored on a remote service like S3, so now it takes even longer because the files need to be copied over first.

I'm happy to share that medialibrary v7 will solve this for you. It includes a ZipStreamResponse class that allows you to respond with a stream. Files will be zipped on the fly and you can even include files from multiple filesystems.

Let's take a look at an example on how to use ZipStreamResponse. We're going to create two routes. Visiting add-files will add some files to our medialibrary, visiting download-files will download them. I'm using routes here for demonstration purposes, in a real world app you'd probably use ZipStreamResponse in a controller.

Route::get('add-files', function() {
    //create a regular model
    $article = Article::create();

    // add a file to the downloads collection on the local disk
    $article
        ->addMedia($pathToAFile)
        ->toMediaCollection('downloads');

    // add a file to the downloads collection on the s3 disk
    $article
    ->addMedia($pathToABigFile)
    ->toMediaCollection('downloads', 's3');

    return 'files added!';
});

Route::get('download-files', function() {
    //get all files in the download collection
    $allMedia = Article::first()->getMedia('downloads');

    // download them in a streamed way, so no prob if your files are very large
    return ZipStreamResponse::create('my-files.zip')->addMedia($allMedia);
});

That last line is the most important one. It's kinda cool that the zip is created on the fly and that it pulls data from both the local disk and s3.

Coding ZipStreamResponse up was easier than I thought it would be. The maennchen/zipstream-php does the hard work of creating a zip stream. All I need to do was to integrate the provided ZipStream class in our medialibrary. Here's the entire source code of Spatie\MediaLibrary\ZipStreamResponse:

namespace Spatie\MediaLibrary;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Contracts\Support\Responsable;
use Spatie\MediaLibrary\Media;
use Symfony\Component\HttpFoundation\StreamedResponse;
use ZipStream\ZipStream;

class ZipStreamResponse implements Responsable
{
    /** string */
    protected $zipName;

    /** Illuminate\Support\Collection */
    protected $mediaItems;

    public static function create(string $zipName)
    {
        return new static($zipName);
    }

    public function __construct(string $zipName)
    {
        $this->zipName = $zipName;
    }

    public function addMedia($mediaItems)
    {
        $this->mediaItems = $mediaItems;

        return $this;
    }

    public function toResponse($request)
    {
        return new StreamedZipResponse(function () {
            $zip = new ZipStream($this->zipName);
            
            $this->mediaItems->each(function (Media $media) use ($zip) {
                $zip->addFileFromStream($media->file_name, $media->stream());
            });
            
            $zip->finish();
        });
    }
}

Of course there are situations (eg. when the same assets get downloaded over and over again, or when download speed is very important) where you still want to create a zip file locally and store it for later use. But I believe StreamedZipResponse does provide a good solution for the proverbial 80%

Like mentioned above, multi file downloads are coming to v7 of laravel-medialibrary which will be released around February - March 2018. Of course you can start using v6 of laravel-medialibrary right now.