| <?php | |
| ########################################################################## | |
| # ZipStream - Streamed, dynamically generated zip archives. # | |
| # by Paul Duncan <pabs@pablotron.org> # | |
| # # | |
| # Copyright (C) 2007-2009 Paul Duncan <pabs@pablotron.org> # | |
| # # | |
| # Permission is hereby granted, free of charge, to any person obtaining # | |
| # a copy of this software and associated documentation files (the # | |
| # "Software"), to deal in the Software without restriction, including # | |
| # without limitation the rights to use, copy, modify, merge, publish, # | |
| # distribute, sublicense, and/or sell copies of the Software, and to # | |
| # permit persons to whom the Software is furnished to do so, subject to # | |
| # the following conditions: # | |
| # # | |
| # The above copyright notice and this permission notice shall be # | |
| # included in all copies or substantial portions of the of the Software. # | |
| # # | |
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # | |
| # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # | |
| # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # | |
| # IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR # | |
| # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, # | |
| # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # | |
| # OTHER DEALINGS IN THE SOFTWARE. # | |
| ########################################################################## | |
| # | |
| # ZipStream - Streamed, dynamically generated zip archives. | |
| # by Paul Duncan <pabs@pablotron.org> | |
| # | |
| # Requirements: | |
| # | |
| # * PHP version 5.1.2 or newer. | |
| # | |
| # Usage: | |
| # | |
| # Streaming zip archives is a simple, three-step process: | |
| # | |
| # 1. Create the zip stream: | |
| # | |
| # $zip = new ZipStream('example.zip'); | |
| # | |
| # 2. Add one or more files to the archive: | |
| # | |
| # # add first file | |
| # $data = file_get_contents('some_file.gif'); | |
| # $zip->add_file('some_file.gif', $data); | |
| # | |
| # # add second file | |
| # $data = file_get_contents('some_file.gif'); | |
| # $zip->add_file('another_file.png', $data); | |
| # | |
| # 3. Finish the zip stream: | |
| # | |
| # $zip->finish(); | |
| # | |
| # You can also add an archive comment, add comments to individual files, | |
| # and adjust the timestamp of files. See the API documentation for each | |
| # method below for additional information. | |
| # | |
| # Example: | |
| # | |
| # # create a new zip stream object | |
| # $zip = new ZipStream('some_files.zip'); | |
| # | |
| # # list of local files | |
| # $files = array('foo.txt', 'bar.jpg'); | |
| # | |
| # # read and add each file to the archive | |
| # foreach ($files as $path) | |
| # $zip->add_file($path, file_get_contents($path)); | |
| # | |
| # # write archive footer to stream | |
| # $zip->finish(); | |
| # | |
| class ZipStream { | |
| const VERSION = '0.2.2'; | |
| var $opt = array(), | |
| $files = array(), | |
| $cdr_ofs = 0, | |
| $ofs = 0; | |
| # | |
| # Create a new ZipStream object. | |
| # | |
| # Parameters: | |
| # | |
| # $name - Name of output file (optional). | |
| # $opt - Hash of archive options (optional, see "Archive Options" | |
| # below). | |
| # | |
| # Archive Options: | |
| # | |
| # comment - Comment for this archive. | |
| # content_type - HTTP Content-Type. Defaults to 'application/x-zip'. | |
| # content_disposition - HTTP Content-Disposition. Defaults to | |
| # 'attachment; filename=\"FILENAME\"', where | |
| # FILENAME is the specified filename. | |
| # large_file_size - Size, in bytes, of the largest file to try | |
| # and load into memory (used by | |
| # add_file_from_path()). Large files may also | |
| # be compressed differently; see the | |
| # 'large_file_method' option. | |
| # large_file_method - How to handle large files. Legal values are | |
| # 'store' (the default), or 'deflate'. Store | |
| # sends the file raw and is significantly | |
| # faster, while 'deflate' compresses the file | |
| # and is much, much slower. Note that deflate | |
| # must compress the file twice and extremely | |
| # slow. | |
| # send_http_headers - Boolean indicating whether or not to send | |
| # the HTTP headers for this file. | |
| # | |
| # Note that content_type and content_disposition do nothing if you are | |
| # not sending HTTP headers. | |
| # | |
| # Large File Support: | |
| # | |
| # By default, the method add_file_from_path() will send send files | |
| # larger than 20 megabytes along raw rather than attempting to | |
| # compress them. You can change both the maximum size and the | |
| # compression behavior using the large_file_* options above, with the | |
| # following caveats: | |
| # | |
| # * For "small" files (e.g. files smaller than large_file_size), the | |
| # memory use can be up to twice that of the actual file. In other | |
| # words, adding a 10 megabyte file to the archive could potentially | |
| # occupty 20 megabytes of memory. | |
| # | |
| # * Enabling compression on large files (e.g. files larger than | |
| # large_file_size) is extremely slow, because ZipStream has to pass | |
| # over the large file once to calculate header information, and then | |
| # again to compress and send the actual data. | |
| # | |
| # Examples: | |
| # | |
| # # create a new zip file named 'foo.zip' | |
| # $zip = new ZipStream('foo.zip'); | |
| # | |
| # # create a new zip file named 'bar.zip' with a comment | |
| # $zip = new ZipStream('bar.zip', array( | |
| # 'comment' => 'this is a comment for the zip file.', | |
| # )); | |
| # | |
| # Notes: | |
| # | |
| # If you do not set a filename, then this library _DOES NOT_ send HTTP | |
| # headers by default. This behavior is to allow software to send its | |
| # own headers (including the filename), and still use this library. | |
| # | |
| function __construct($name = null, $opt = array()) { | |
| # save options | |
| $this->opt = $opt; | |
| # set large file defaults: size = 20 megabytes, method = store | |
| if (!$this->opt['large_file_size']) | |
| $this->opt['large_file_size'] = 20 * 1024 * 1024; | |
| if (!$this->opt['large_file_method']) | |
| $this->opt['large_file_method'] = 'store'; | |
| $this->output_name = $name; | |
| if ($name || $opt['send_http_headers']) | |
| $this->need_headers = true; | |
| } | |
| # | |
| # add_file - add a file to the archive | |
| # | |
| # Parameters: | |
| # | |
| # $name - path of file in archive (including directory). | |
| # $data - contents of file | |
| # $opt - Hash of options for file (optional, see "File Options" | |
| # below). | |
| # | |
| # File Options: | |
| # time - Last-modified timestamp (seconds since the epoch) of | |
| # this file. Defaults to the current time. | |
| # comment - Comment related to this file. | |
| # | |
| # Examples: | |
| # | |
| # # add a file named 'foo.txt' | |
| # $data = file_get_contents('foo.txt'); | |
| # $zip->add_file('foo.txt', $data); | |
| # | |
| # # add a file named 'bar.jpg' with a comment and a last-modified | |
| # # time of two hours ago | |
| # $data = file_get_contents('bar.jpg'); | |
| # $zip->add_file('bar.jpg', $data, array( | |
| # 'time' => time() - 2 * 3600, | |
| # 'comment' => 'this is a comment about bar.jpg', | |
| # )); | |
| # | |
| function add_file($name, $data, $opt = array()) { | |
| # compress data | |
| $zdata = gzdeflate($data); | |
| # calculate header attributes | |
| $crc = crc32($data); | |
| $zlen = strlen($zdata); | |
| $len = strlen($data); | |
| $meth = 0x08; | |
| # send file header | |
| $this->add_file_header($name, $opt, $meth, $crc, $zlen, $len); | |
| # print data | |
| $this->send($zdata); | |
| } | |
| # | |
| # add_file_from_path - add a file at path to the archive. | |
| # | |
| # Note that large files may be compresed differently than smaller | |
| # files; see the "Large File Support" section above for more | |
| # information. | |
| # | |
| # Parameters: | |
| # | |
| # $name - name of file in archive (including directory path). | |
| # $path - path to file on disk (note: paths should be encoded using | |
| # UNIX-style forward slashes -- e.g '/path/to/some/file'). | |
| # $opt - Hash of options for file (optional, see "File Options" | |
| # below). | |
| # | |
| # File Options: | |
| # time - Last-modified timestamp (seconds since the epoch) of | |
| # this file. Defaults to the current time. | |
| # comment - Comment related to this file. | |
| # | |
| # Examples: | |
| # | |
| # # add a file named 'foo.txt' from the local file '/tmp/foo.txt' | |
| # $zip->add_file_from_path('foo.txt', '/tmp/foo.txt'); | |
| # | |
| # # add a file named 'bigfile.rar' from the local file | |
| # # '/usr/share/bigfile.rar' with a comment and a last-modified | |
| # # time of two hours ago | |
| # $path = '/usr/share/bigfile.rar'; | |
| # $zip->add_file_from_path('bigfile.rar', $path, array( | |
| # 'time' => time() - 2 * 3600, | |
| # 'comment' => 'this is a comment about bar.jpg', | |
| # )); | |
| # | |
| function add_file_from_path($name, $path, $opt = array()) { | |
| if ($this->is_large_file($path)) { | |
| # file is too large to be read into memory; add progressively | |
| $this->add_large_file($name, $path, $opt); | |
| } else { | |
| # file is small enough to read into memory; read file contents and | |
| # handle with add_file() | |
| $data = file_get_contents($path); | |
| $this->add_file($name, $data, $opt); | |
| } | |
| } | |
| # | |
| # finish - Write zip footer to stream. | |
| # | |
| # Example: | |
| # | |
| # # add a list of files to the archive | |
| # $files = array('foo.txt', 'bar.jpg'); | |
| # foreach ($files as $path) | |
| # $zip->add_file($path, file_get_contents($path)); | |
| # | |
| # # write footer to stream | |
| # $zip->finish(); | |
| # | |
| function finish() { | |
| # add trailing cdr record | |
| $this->add_cdr($this->opt); | |
| $this->clear(); | |
| } | |
| ################### | |
| # PRIVATE METHODS # | |
| ################### | |
| # | |
| # Create and send zip header for this file. | |
| # | |
| private function add_file_header($name, $opt, $meth, $crc, $zlen, $len) { | |
| # strip leading slashes from file name | |
| # (fixes bug in windows archive viewer) | |
| $name = preg_replace('/^\\/+/', '', $name); | |
| # calculate name length | |
| $nlen = strlen($name); | |
| # create dos timestamp | |
| $opt['time'] = $opt['time'] ? $opt['time'] : time(); | |
| $dts = $this->dostime($opt['time']); | |
| # build file header | |
| $fields = array( # (from V.A of APPNOTE.TXT) | |
| array('V', 0x04034b50), # local file header signature | |
| array('v', (6 << 8) + 3), # version needed to extract | |
| array('v', 0x00), # general purpose bit flag | |
| array('v', $meth), # compresion method (deflate or store) | |
| array('V', $dts), # dos timestamp | |
| array('V', $crc), # crc32 of data | |
| array('V', $zlen), # compressed data length | |
| array('V', $len), # uncompressed data length | |
| array('v', $nlen), # filename length | |
| array('v', 0), # extra data len | |
| ); | |
| # pack fields and calculate "total" length | |
| $ret = $this->pack_fields($fields); | |
| $cdr_len = strlen($ret) + $nlen + $zlen; | |
| # print header and filename | |
| $this->send($ret . $name); | |
| # add to central directory record and increment offset | |
| $this->add_to_cdr($name, $opt, $meth, $crc, $zlen, $len, $cdr_len); | |
| } | |
| # | |
| # Add a large file from the given path. | |
| # | |
| private function add_large_file($name, $path, $opt = array()) { | |
| $st = stat($path); | |
| $block_size = 1048576; # process in 1 megabyte chunks | |
| $algo = 'crc32b'; | |
| # calculate header attributes | |
| $zlen = $len = $st['size']; | |
| $meth_str = $this->opt['large_file_method']; | |
| if ($meth_str == 'store') { | |
| # store method | |
| $meth = 0x00; | |
| $crc = unpack('V', hash_file($algo, $path, true)); | |
| $crc = $crc[1]; | |
| } elseif ($meth_str == 'deflate') { | |
| # deflate method | |
| $meth = 0x08; | |
| # open file, calculate crc and compressed file length | |
| $fh = fopen($path, 'rb'); | |
| $hash_ctx = hash_init($algo); | |
| $zlen = 0; | |
| # read each block, update crc and zlen | |
| while ($data = fgets($fh, $block_size)) { | |
| hash_update($hash_ctx, $data); | |
| $data = gzdeflate($data); | |
| $zlen += strlen($data); | |
| } | |
| # close file and finalize crc | |
| fclose($fh); | |
| $crc = unpack('V', hash_final($hash_ctx, true)); | |
| $crc = $crc[1]; | |
| } else { | |
| die("unknown large_file_method: $meth_str"); | |
| } | |
| # send file header | |
| $this->add_file_header($name, $opt, $meth, $crc, $zlen, $len); | |
| # open input file | |
| $fh = fopen($path, 'rb'); | |
| # send file blocks | |
| while ($data = fgets($fh, $block_size)) { | |
| if ($meth_str == 'deflate') | |
| $data = gzdeflate($data); | |
| # send data | |
| $this->send($data); | |
| } | |
| # close input file | |
| fclose($fh); | |
| } | |
| # | |
| # Is this file larger than large_file_size? | |
| # | |
| function is_large_file($path) { | |
| $st = stat($path); | |
| return ($this->opt['large_file_size'] > 0) && | |
| ($st['size'] > $this->opt['large_file_size']); | |
| } | |
| # | |
| # Save file attributes for trailing CDR record. | |
| # | |
| private function add_to_cdr($name, $opt, $meth, $crc, $zlen, $len, $rec_len) { | |
| $this->files[] = array($name, $opt, $meth, $crc, $zlen, $len, $this->ofs); | |
| $this->ofs += $rec_len; | |
| } | |
| # | |
| # Send CDR record for specified file. | |
| # | |
| private function add_cdr_file($args) { | |
| list ($name, $opt, $meth, $crc, $zlen, $len, $ofs) = $args; | |
| # get attributes | |
| $comment = $opt['comment'] ? $opt['comment'] : ''; | |
| # get dos timestamp | |
| $dts = $this->dostime($opt['time']); | |
| $fields = array( # (from V,F of APPNOTE.TXT) | |
| array('V', 0x02014b50), # central file header signature | |
| array('v', (6 << 8) + 3), # version made by | |
| array('v', (6 << 8) + 3), # version needed to extract | |
| array('v', 0x00), # general purpose bit flag | |
| array('v', $meth), # compresion method (deflate or store) | |
| array('V', $dts), # dos timestamp | |
| array('V', $crc), # crc32 of data | |
| array('V', $zlen), # compressed data length | |
| array('V', $len), # uncompressed data length | |
| array('v', strlen($name)), # filename length | |
| array('v', 0), # extra data len | |
| array('v', strlen($comment)), # file comment length | |
| array('v', 0), # disk number start | |
| array('v', 0), # internal file attributes | |
| array('V', 32), # external file attributes | |
| array('V', $ofs), # relative offset of local header | |
| ); | |
| # pack fields, then append name and comment | |
| $ret = $this->pack_fields($fields) . $name . $comment; | |
| $this->send($ret); | |
| # increment cdr offset | |
| $this->cdr_ofs += strlen($ret); | |
| } | |
| # | |
| # Send CDR EOF (Central Directory Record End-of-File) record. | |
| # | |
| private function add_cdr_eof($opt = null) { | |
| $num = count($this->files); | |
| $cdr_len = $this->cdr_ofs; | |
| $cdr_ofs = $this->ofs; | |
| # grab comment (if specified) | |
| $comment = ''; | |
| if ($opt && $opt['comment']) | |
| $comment = $opt['comment']; | |
| $fields = array( # (from V,F of APPNOTE.TXT) | |
| array('V', 0x06054b50), # end of central file header signature | |
| array('v', 0x00), # this disk number | |
| array('v', 0x00), # number of disk with cdr | |
| array('v', $num), # number of entries in the cdr on this disk | |
| array('v', $num), # number of entries in the cdr | |
| array('V', $cdr_len), # cdr size | |
| array('V', $cdr_ofs), # cdr ofs | |
| array('v', strlen($comment)), # zip file comment length | |
| ); | |
| $ret = $this->pack_fields($fields) . $comment; | |
| $this->send($ret); | |
| } | |
| # | |
| # Add CDR (Central Directory Record) footer. | |
| # | |
| private function add_cdr($opt = null) { | |
| foreach ($this->files as $file) | |
| $this->add_cdr_file($file); | |
| $this->add_cdr_eof($opt); | |
| } | |
| # | |
| # Clear all internal variables. Note that the stream object is not | |
| # usable after this. | |
| # | |
| function clear() { | |
| $this->files = array(); | |
| $this->ofs = 0; | |
| $this->cdr_ofs = 0; | |
| $this->opt = array(); | |
| } | |
| ########################### | |
| # PRIVATE UTILITY METHODS # | |
| ########################### | |
| # | |
| # Send HTTP headers for this stream. | |
| # | |
| private function send_http_headers() { | |
| # grab options | |
| $opt = $this->opt; | |
| # grab content type from options | |
| $content_type = 'application/x-zip'; | |
| if ($opt['content_type']) | |
| $content_type = $this->opt['content_type']; | |
| # grab content disposition | |
| $disposition = 'attachment'; | |
| if ($opt['content_disposition']) | |
| $disposition = $opt['content_disposition']; | |
| if ($this->output_name) | |
| $disposition .= "; filename=\"{$this->output_name}\""; | |
| $headers = array( | |
| 'Content-Type' => $content_type, | |
| 'Content-Disposition' => $disposition, | |
| 'Pragma' => 'public', | |
| 'Cache-Control' => 'public, must-revalidate', | |
| 'Content-Transfer-Encoding' => 'binary', | |
| ); | |
| foreach ($headers as $key => $val) | |
| header("$key: $val"); | |
| } | |
| # | |
| # Send string, sending HTTP headers if necessary. | |
| # | |
| private function send($str) { | |
| if ($this->need_headers) | |
| $this->send_http_headers(); | |
| $this->need_headers = false; | |
| echo $str; | |
| } | |
| # | |
| # Convert a UNIX timestamp to a DOS timestamp. | |
| # | |
| function dostime($when = 0) { | |
| # get date array for timestamp | |
| $d = getdate($when); | |
| # set lower-bound on dates | |
| if ($d['year'] < 1980) { | |
| $d = array('year' => 1980, 'mon' => 1, 'mday' => 1, | |
| 'hours' => 0, 'minutes' => 0, 'seconds' => 0); | |
| } | |
| # remove extra years from 1980 | |
| $d['year'] -= 1980; | |
| # return date string | |
| return ($d['year'] << 25) | ($d['mon'] << 21) | ($d['mday'] << 16) | | |
| ($d['hours'] << 11) | ($d['minutes'] << 5) | ($d['seconds'] >> 1); | |
| } | |
| # | |
| # Create a format string and argument list for pack(), then call | |
| # pack() and return the result. | |
| # | |
| function pack_fields($fields) { | |
| list ($fmt, $args) = array('', array()); | |
| # populate format string and argument list | |
| foreach ($fields as $field) { | |
| $fmt .= $field[0]; | |
| $args[] = $field[1]; | |
| } | |
| # prepend format string to argument list | |
| array_unshift($args, $fmt); | |
| # build output string from header and compressed data | |
| return call_user_func_array('pack', $args); | |
| } | |
| }; | |
| ?> |