Search code examples
phplaravelphp-ziparchive

Laravel with PHP ZipArchive - adding file twice


I'm trying to create a KMZ file with Laravel. That's just a ZIP file with a doc.kml at the root, and any referenced files included in the archive.

Here is my relatively simple code:

        // Defined previously in the file:
        // $location -- a model containing a geographic location
        // $kml -- an XML document for the location
        // $kml is a valid KML file (tested and working)
        $kmz = new ZipArchive;
        $kmz->open("/tmp/{$location->name}.kmz", ZipArchive::CREATE);
        $kmz->addFromString("doc.kml", $kml);
        $icons = $location->markers->pluck("icon")->unique();
        // dd( $icons );
        foreach( $icons as $icon )
        {
            $icon = trim($icon, "/");
            $kmz->addFile(public_path($icon), $icon);
        }
        $kmz->close();
        return response()->download("/tmp/{$location->name}.kmz");

This downloads the KMZ file and it works as expected (I can open the KMZ in Google Earth and view the file, the markers are displayed, etc)

However each icon is added twice. This is an example of what I see when I include a single icon: /img/maps/pins/fa-home.png

enter image description here

As you can see, there's a blank-named folder. When I dive into this folder, it reveals the following:

enter image description here

The image fa-home.png exists both in the /img/maps/pins/ directory and in this strange blank directory. Worse yet, the blank directory is dumping my server's directory structure (a minor security hole)

What could be causing this?


Solution

  • After a couple days I figured out the issue here, and it's a bit embarrassing.

    There's a subtle hint at the problem on this line:

    $kmz->open("/tmp/{$location->name}.kmz", ZipArchive::CREATE);
    

    When using ZipArchive::CREATE, if the file already exists, it will open it for modification. This is not well-documented. At best we have the following line:

    ZipArchive::CREATE (integer)

    Create the archive if it does not exist.

    Source: http://php.net/manual/en/zip.constants.php#ziparchive.constants.create

    Nothing here specifies the behavior if it does exist. Apparently the behavior is to modify the existing file directly.

    It turns out the //home/ubuntu/workspace/public//img/maps/pins directory was added to the zip in a previous test that I had written which has since been corrected and removed. However since I was constantly updating the existing file instead of creating a new one, this file never got removed from my ZIP file.

    As a quick fix, the following can be used in place of this line:

    $kmz->open("/tmp/{$location->name}.kmz", ZipArchive::CREATE | ZipArchive::OVERWRITE);
    

    However if two people try to access the same file then the second one may receive a server error (as the first user has an open handle to the ZIP file and it cannot be destroyed for overwriting). A better solution I found was to append a unique string to the filename. I went with the following:

    $utime = utime();
    $rand = rand();
    $kmz->open("/tmp/{$location->name}-{$utime}-{$rand}.kmz", ZipArchive::CREATE | ZipArchive::OVERWRITE);
    

    I'm not 100% satisfied with my solution since it's non-deterministic. It's possible, albeit very, very, very unlikely, for two people to simultaneously (same microsecond) access the same file and for the rand() function to happen to return the same value for each of them. The odds of this happening even once in the next 100,000 years are probably 1 in a trillion, but if there's a better solution I'd be interested to figure it out (utilizing a lock file, hashing some properties of the request, etc).