By Michael M on
Because ZIP creation and manipulation functionality is not implemented into PHP for most people, I found a method of creating zips with a simple zip class that should work on any platform.
Here is the 'zip.inc' file (taken from the phpMyadmin project) that you should place in the includes directory (optionally, if you don't want users to add files into the includes directory, you can save it in your module, save it in the same folder as your module, etc.):
/* $Id: zip.lib.php,v 2.4 2004/11/03 13:56:52 garvinhicking Exp $ */
// vim: expandtab sw=4 ts=4 sts=4:
/**
* Zip file creation class.
* Makes zip files.
*
* Based on :
*
* http://www.zend.com/codex.php?id=535&single=1
* By Eric Mueller <eric@themepark.com>
*
* http://www.zend.com/codex.php?id=470&single=1
* by Denis125 <webmaster@atlant.ru>
*
* a patch from Peter Listiak <mlady@users.sourceforge.net> for last modified
* date and time of the compressed file
*
* Official ZIP file format: http://www.pkware.com/appnote.txt
*
* @access public
*/
class zipfile
{
/**
* Array to store compressed data
*
* @var array $datasec
*/
var $datasec = array();
/**
* Central directory
*
* @var array $ctrl_dir
*/
var $ctrl_dir = array();
/**
* End of central directory record
*
* @var string $eof_ctrl_dir
*/
var $eof_ctrl_dir = "\x50\x4b\x05\x06\x00\x00\x00\x00";
/**
* Last offset position
*
* @var integer $old_offset
*/
var $old_offset = 0;
/**
* Converts an Unix timestamp to a four byte DOS date and time format (date
* in high two bytes, time in low two bytes allowing magnitude comparison).
*
* @param integer the current Unix timestamp
*
* @return integer the current date in a four byte DOS format
*
* @access private
*/
function unix2DosTime($unixtime = 0) {
$timearray = ($unixtime == 0) ? getdate() : getdate($unixtime);
if ($timearray['year'] < 1980) {
$timearray['year'] = 1980;
$timearray['mon'] = 1;
$timearray['mday'] = 1;
$timearray['hours'] = 0;
$timearray['minutes'] = 0;
$timearray['seconds'] = 0;
} // end if
return (($timearray['year'] - 1980) << 25) | ($timearray['mon'] << 21) | ($timearray['mday'] << 16) |
($timearray['hours'] << 11) | ($timearray['minutes'] << 5) | ($timearray['seconds'] >> 1);
} // end of the 'unix2DosTime()' method
/**
* Adds "file" to archive
*
* @param string file contents
* @param string name of the file in the archive (may contains the path)
* @param integer the current timestamp
*
* @access public
*/
function addFile($data, $name, $time = 0)
{
$name = str_replace('\\', '/', $name);
$dtime = dechex($this->unix2DosTime($time));
$hexdtime = '\x' . $dtime[6] . $dtime[7]
. '\x' . $dtime[4] . $dtime[5]
. '\x' . $dtime[2] . $dtime[3]
. '\x' . $dtime[0] . $dtime[1];
eval('$hexdtime = "' . $hexdtime . '";');
$fr = "\x50\x4b\x03\x04";
$fr .= "\x14\x00"; // ver needed to extract
$fr .= "\x00\x00"; // gen purpose bit flag
$fr .= "\x08\x00"; // compression method
$fr .= $hexdtime; // last mod time and date
// "local file header" segment
$unc_len = strlen($data);
$crc = crc32($data);
$zdata = gzcompress($data);
$zdata = substr(substr($zdata, 0, strlen($zdata) - 4), 2); // fix crc bug
$c_len = strlen($zdata);
$fr .= pack('V', $crc); // crc32
$fr .= pack('V', $c_len); // compressed filesize
$fr .= pack('V', $unc_len); // uncompressed filesize
$fr .= pack('v', strlen($name)); // length of filename
$fr .= pack('v', 0); // extra field length
$fr .= $name;
// "file data" segment
$fr .= $zdata;
// "data descriptor" segment (optional but necessary if archive is not
// served as file)
// nijel(2004-10-19): this seems not to be needed at all and causes
// problems in some cases (bug #1037737)
//$fr .= pack('V', $crc); // crc32
//$fr .= pack('V', $c_len); // compressed filesize
//$fr .= pack('V', $unc_len); // uncompressed filesize
// add this entry to array
$this -> datasec[] = $fr;
// now add to central directory record
$cdrec = "\x50\x4b\x01\x02";
$cdrec .= "\x00\x00"; // version made by
$cdrec .= "\x14\x00"; // version needed to extract
$cdrec .= "\x00\x00"; // gen purpose bit flag
$cdrec .= "\x08\x00"; // compression method
$cdrec .= $hexdtime; // last mod time & date
$cdrec .= pack('V', $crc); // crc32
$cdrec .= pack('V', $c_len); // compressed filesize
$cdrec .= pack('V', $unc_len); // uncompressed filesize
$cdrec .= pack('v', strlen($name) ); // length of filename
$cdrec .= pack('v', 0 ); // extra field length
$cdrec .= pack('v', 0 ); // file comment length
$cdrec .= pack('v', 0 ); // disk number start
$cdrec .= pack('v', 0 ); // internal file attributes
$cdrec .= pack('V', 32 ); // external file attributes - 'archive' bit set
$cdrec .= pack('V', $this -> old_offset ); // relative offset of local header
$this -> old_offset += strlen($fr);
$cdrec .= $name;
// optional extra field, file comment goes here
// save to central directory
$this -> ctrl_dir[] = $cdrec;
} // end of the 'addFile()' method
/**
* Dumps out file
*
* @return string the zipped file
*
* @access public
*/
function file()
{
$data = implode('', $this -> datasec);
$ctrldir = implode('', $this -> ctrl_dir);
return
$data .
$ctrldir .
$this -> eof_ctrl_dir .
pack('v', sizeof($this -> ctrl_dir)) . // total # of entries "on this disk"
pack('v', sizeof($this -> ctrl_dir)) . // total # of entries overall
pack('V', strlen($ctrldir)) . // size of central dir
pack('V', strlen($data)) . // offset to start of central dir
"\x00\x00"; // .zip file comment length
} // end of the 'file()' method
} // end of the 'zipfile' class
Then, in your module, you can do the following:
require_once("./includes/zip.inc");
$zip = new zipfile();
$files_to_zip = array('mypic.jpg', 'mydoc.doc', 'readme.txt');
foreach ($files_to_zip as $filename) {
$zip->addFile(file_get_contents(file_create_path($filename)), $filename); // the second parameter specifies the filename in the zip
}
if (!file_save_data($zip->file(), file_create_path('test.zip'))) {
drupal_set_message(t('The zip could not be created.'), 'error');
}
You can also write the file_save_data function into the class or add more functions that can improve the class or offer more standardized functions.
Michael
Comments
Almost forgot...
Before using the zipping functionality, check to see if the gzcompress function exists:
----
http://eUploads.com
Help getting it to stream to disk for large directories?
Thanks for posting this - I've implemented it into my fileshare module where it allows the user to compress and download a directory. It works fine for smaller files, but for larger ones it becomes a memory problem. I'm considering trying to re-write the phpmyadmin class to support streaming to disk but am not sure if it's really possible... See below for how I'm using the class. I'm allocating a ridiculous amount of memory to php for now and I'm not happy about it. :)
PLEASE HELP!
Thanks - Jamie.
Jamie, When exactly does
Jamie,
When exactly does there seem to be a memory problem: when the ZIP file in memory becomes too large OR when you add files that are too large?
I will have to test this with large files or a large number of files.
What kind of memory issues are you experiencing? How do you know that there are memory problems (your development PC is hanging, the memory used by PHP sky-rockets, etc.)? Is it a problem when while the file is transfered to the users OR is the memory problem with the creation of the ZIP file? It looks like you are creating a ZIP in memory and then outputting it for download. Maybe you can try to save the zip as a file and forward the user to the file location?
What exactly do you mean "streaming to disk"?
Just brain storming...
----
http://eUploads.com
Hi thanks for the
Hi thanks for the reply!
It's php's memory limit that I'm hitting. I'm setting php's memory temporarily to 500 megs:
ini_set('memory_limit', '500M');And it's working, but it's got our IT guy freaked out. And since I want to include the functionality in the standard release of the fileshare module I would like it to work on most webservers... and I doubt a shared webserver would allow that type of memory allocation.
So to be able to keep php's memory limit set at something like 16M we'll have to read the file in and stream it to disk... or read it in in small bits and use the disk as a cache... then have the user download the temp file and remove the file after the download...
I hope that helps clear things up! :)
Let me know what you think. Thanks - Jamie.
Avoiding the memory limit
The following changes allow using zip.lib.php (renamed zipstream...) to write the .zip to stdout on the fly so the data sections of the .zip are not saved in memory, which should greatly reduce the memory requirement of this code for large .zip files.
The only thing you need to add is a call to the ->setDoWrite() method of the zipfile class *immediately* after the creation of the class and before adding any files e.g.
Here are the changes:
Test
Hi drbitboy,
I tried using your solution but I got no luck. I got the file printed on the screen instead of creating a compressed file. Can you provide a sample of its implementation/source code or similar?
I'm having the very same struggle to not blow up the memory during zip file creation.
Thanks,
Neill
Thanks Michael!
Thanks Michael! I'll be trying this out, really really useful!!! thanks a lot
This is no longer needed in Drupal 7
It is now in core. See http://api.drupal.org/api/drupal/includes--archiver.inc/interface/Archiv...