This blob has been accessed 391 times via Git panel.
- <?php
- ##########################################################################
- # ZipStream - Streamed, dynamically generated zip archives. #
- # by Paul Duncan <pabs@pablotron.org> #
- # #
- # Copyright (C) 2007 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 of the Software, its documentation and #
- # marketing & publicity materials, and acknowledgment shall be given in #
- # the documentation, materials and software packages that this Software #
- # was used. #
- # #
- # 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>
- #
- # 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 {
- 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 ZipStream($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 = mb_strlen($zdata);
- $len = mb_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.
- # $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.
- #
- function add_file_header($name, $opt, $meth, $crc, $zlen, $len) {
- # calculate name length
- $nlen = mb_strlen($name);
- # create dos timestamp
- $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', 0x14), # 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 = mb_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.
- #
- 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
- $hash_ctx = hash_init($algo);
- $zlen = 0;
- # read each block, update crc and zlen
- hash_update($hash_ctx, $data);
- $data = gzdeflate($data);
- $zlen += mb_strlen($data);
- }
- # close file and finalize crc
- $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
- # send file blocks
- if ($meth_str == 'deflate')
- $data = gzdeflate($data);
- # send data
- $this->send($data);
- }
- # close input file
- }
- #
- # 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.
- #
- 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.
- #
- 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', 0x00), # version made by
- array('v', 0x14), # 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', mb_strlen($name)), # filename length
- array('v', 0), # extra data len
- array('v', mb_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 += mb_strlen($ret);
- }
- #
- # Send CDR EOF (Central Directory Record End-of-File) record.
- #
- 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', mb_strlen($comment)), # zip file comment length
- );
- $ret = $this->pack_fields($fields) . $comment;
- $this->send($ret);
- }
- #
- # Add CDR (Central Directory Record) footer.
- #
- 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.
- #
- 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.
- #
- 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);
- }
- };
- ?>