Recursively ZIP entire directory using PHP

December 27th 2012

I often need to ZIP up an entire directory on a server. Whether it's sending out documents to a customer, or just taking a quick backup to send up to Rackspace for safe keeping.

When I do this, it's probably no surprise that I use my own PHP Class to manage the process. This is a little more involved than previous posts, but still relatively straightforward.

First, you'll need an Exception file. Make a new file with this as the content:

PHP Code:
<?php
class App_File_Zip_Exception extends Exception {}

Now, the brains of this operation is this class:

PHP Code:
<?php
class App_File_Zip {

    
/**
     * Zip a file, or entire directory recursively.
     *
     * @param string $source directory or file name
     * @param string $destinationPathAndFilename full path to output
     * @throws App_File_Zip_Exception
     * @return boolean whether zip was a success
     */
    
public static function CreateFromFilesystem($source$destinationPathAndFilename)
    {
        
$base realpath(dirname($destinationPathAndFilename));

        if (!
is_writable($base))
        {
            throw new 
App_File_Zip_Exception('Destination must be writable directory.');
        }

        if (!
is_dir($base))
        {
            throw new 
App_File_Zip_Exception('Destination must be a writable directory.');
        }

        if (!
file_exists($source))
        {
            throw new 
App_File_Zip_Exception('Source doesnt exist in location: ' $source);
        }

        
$source realpath($source);

        if (!
extension_loaded('zip') || !file_exists($source))
        {
            return 
false;
        }

        
$zip = new ZipArchive();
        
$zip->open($destinationPathAndFilenameZipArchive::CREATE);
        if (
is_dir($source) === true)
        {
            
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source), RecursiveIteratorIterator::SELF_FIRST);
            
$baseName realpath($source);
            foreach (
$files as $file)
            {
                if( 
in_array(substr($filestrrpos($fileDIRECTORY_SEPARATOR)+1), array('.''..')) )
                {
                    continue;
                }

                
$relative substr($filestrlen($baseName));
                if (
is_dir($file) === true)
                {
                    
// Add directory
                    
$added $zip->addEmptyDir(trim($relative"\\/"));
                    if (!
$added)
                    {
                        throw new 
App_File_Zip_Exception('Unable to add directory named: ' trim($relative"\\/"));
                    }
                }
                else if (
is_file($file) === true)
                {
                    
// Add file
                    
$added $zip->addFromString(trim($relative"\\/"), file_get_contents($file));
                    if (!
$added)
                    {
                        throw new 
App_File_Zip_Exception('Unable to add file named: ' trim($relative"\\/"));
                    }
                }
            }
        }
        else if (
is_file($source) === true)
        {
            
// Add file
            
$added $zip->addFromString(trim(basename($source), "\\/"), file_get_contents($source));
            if (!
$added)
            {
                throw new 
App_File_Zip_Exception('Unable to add file named: ' trim($relative"\\/"));
            }
        }
        else
        {
            throw new 
App_File_Zip_Exception('Source must be a directory or a file.');
        }

        
$zip->setArchiveComment('Created with App_Framework');

        return 
$zip->close();
    }

}

Using the class is relatively simple. Here's how I generally use it:

PHP Code:
<?php
try {
    
$source '/my/path';
    
$destination '/my/writable/path/tmp.zip';
    
App_File_Zip::CreateFromFilesystem($source$destination);
} catch (
App_File_Zip_Exception $e) {
    
// Zip file was not created.
}

Did this help you out? Did you improve it? Let me know in the comments.

Giorgio commented on Dec 27th 2013

Thanks for sharing! I am using your script in a project and it works very well. The only thing I have changed in code is the creation of the base folder.

Suppose that you are calling your script this way:
App_File_Zip::CreateFromFilesystem("firstFolder/", "backup.zip");
App_File_Zip::CreateFromFilesystem("secondFolder/", "backup.zip");
If "firstFolder" and "secondFolder" contain the same file, the second call will overwrite this file. This is not correct, since when I am invoking the method this way, my intention is clearly to add two different folders to zip file.

So, I would change the snippet keeping a copy of my original folder and use it when I add files and directories. This way:
$path = realpath($source); // after this I will always use $path instead of $source
$added = $zip->addEmptyDir($source.trim($relative, "\/")); // $source is now used to create a base path inside zip
$added = $zip->addFromString($source.trim($relative, "\/"), file_get_contents($file)); // the same as above

This way, I will create "firstFolder" and "secondFolder" inside zip file: this keeps stuff ordered and avoids the issue I've explained before. Maybe it's not the most elegant solution, but I think it may do the trick.. :-)

Let me know!
Thanks!