1
0
Fork 0
mirror of https://github.com/Yetangitu/ampache synced 2025-10-05 02:39:47 +02:00
ampache/modules/ZipStream/ZipStream.php
2015-02-06 20:56:47 +01:00

860 lines
21 KiB
PHP

<?php
namespace ZipStream;
use ZipStream\Exception\InvalidOptionException;
use ZipStream\Exception\FileNotFoundException;
use ZipStream\Exception\FileNotReadableException;
/**
* ZipStream
*
* Streamed, dynamically generated zip archives.
*
* @author Paul Duncan <pabs@pablotron.org>
* @copyright Copyright (C) 2007-2009 Paul Duncan <pabs@pablotron.org>
*
* @author Jonatan Männchen <jonatan@maennchen.ch>
* @copyright Copyright (C) 2014 Jonatan Männchen <jonatan@maennchen.ch>
*
* @author Jesse Donat <donatj@gmail.com>
* @copyright Copyright (C) 2014 Jesse Donat <donatj@gmail.com>
*
* @license https://raw.githubusercontent.com/maennchen/ZipStream-PHP/master/LICENCE
*
*
* 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->addFile('some_file.gif', $data);
*
* * add second file
* $data = file_get_contents('some_file.gif');
* $zip->addFile('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->addFile($path, file_get_contents($path));
*
* // write archive footer to stream
* $zip->finish();
*/
class ZipStream {
const VERSION = '0.3.0';
const METHOD_STORE = 'store';
const METHOD_DEFLATE = 'deflate';
const OPTION_LARGE_FILE_SIZE = 'large_file_size';
const OPTION_LARGE_FILE_METHOD = 'large_file_method';
const OPTION_SEND_HTTP_HEADERS = 'send_http_headers';
const OPTION_HTTP_HEADER_CALLBACK = 'http_header_callback';
const OPTION_OUTPUT_STREAM = 'output_stream';
const OPTION_CONTENT_TYPE = 'content_type';
const OPTION_CONTENT_DISPOSITION = 'content_disposition';
/**
* Global Options
*
* @var array
*/
public $opt = array();
/**
* @var array
*/
public $files = array();
/**
* @var integer
*/
public $cdr_ofs = 0;
/**
* @var integer
*/
public $ofs = 0;
/**
* @var bool
*/
protected $need_headers;
/**
* @var null|String
*/
protected $output_name;
/**
* Create a new ZipStream object.
*
* Parameters:
*
* @param String $name - Name of output file (optional).
* @param array $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
* addFileFromPath()). 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.
* sendHttpHeaders - 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 addFileFromPath() 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.
*/
public function __construct($name = null, $opt = array()) {
$defaults = array(
// set large file defaults: size = 20 megabytes
self::OPTION_LARGE_FILE_SIZE => 20 * 1024 * 1024,
self::OPTION_LARGE_FILE_METHOD => self::METHOD_STORE,
self::OPTION_SEND_HTTP_HEADERS => false,
self::OPTION_HTTP_HEADER_CALLBACK => 'header',
);
// merge and save options
$this->opt = array_merge($defaults, $opt);
if (!isset($this->opt[self::OPTION_OUTPUT_STREAM])) {
$this->opt[self::OPTION_OUTPUT_STREAM] = fopen('php://output', 'w');
}
$this->output_name = $name;
$this->need_headers = $name || $this->opt[self::OPTION_SEND_HTTP_HEADERS];
}
/**
* addFile
*
* add a file to the archive
*
* @param String $name - path of file in archive (including directory).
* @param String $data - contents of file
* @param array $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->addFile('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->addFile('bar.jpg', $data, array(
* 'time' => time() - 2 * 3600,
* 'comment' => 'this is a comment about bar.jpg',
* ));
*/
public function addFile($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->addFileHeader($name, $opt, $meth, $crc, $zlen, $len);
// print data
$this->send($zdata);
}
/**
* addFileFromPath
*
* 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.
*
* @param String $name - name of file in archive (including directory path).
* @param String $path - path to file on disk (note: paths should be encoded using
* UNIX-style forward slashes -- e.g '/path/to/some/file').
* @param array $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->addFileFromPath('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->addFileFromPath('bigfile.rar', $path, array(
* 'time' => time() - 2 * 3600,
* 'comment' => 'this is a comment about bar.jpg',
* ));
*
* @return void
* @throws \ZipStream\Exception\FileNotFoundException
* @throws \ZipStream\Exception\FileNotReadableException
*/
public function addFileFromPath($name, $path, $opt = array()) {
if(!is_readable($path)) {
if(!file_exists($path)) {
throw new FileNotFoundException($path);
}
throw new FileNotReadableException($path);
}
if ($this->isLargeFile($path)) {
// file is too large to be read into memory; add progressively
$this->addLargeFile($name, $path, $opt);
} else {
// file is small enough to read into memory; read file contents and
// handle with addFile()
$data = file_get_contents($path);
$this->addFile($name, $data, $opt);
}
}
/**
* addFile_from_stream
*
* dds an open stream to the archive uncompressed
*
* @param String $name - path of file in archive (including directory).
* @param Resource $stream - contents of file as a stream resource
* @param array $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:
*
* // create a temporary file stream and write text to it
* $fp = tmpfile();
* fwrite($fp, 'The quick brown fox jumped over the lazy dog.');
*
* // add a file named 'streamfile.txt' from the content of the stream
* $x->addFile_from_stream('streamfile.txt', $fp);
*
* @return void
*/
public function addFileFromStream($name, $stream, $opt = array()) {
$block_size = 1048576; // process in 1 megabyte chunks
$algo = 'crc32b';
$meth = 0x00;
// calculate header attributes
fseek($stream, 0, SEEK_END);
$zlen = $len = ftell($stream);
rewind($stream);
$hash_ctx = hash_init($algo);
hash_update_stream($hash_ctx, $stream);
$crc = hexdec(hash_final($hash_ctx));
// send file header
$this->addFileHeader($name, $opt, $meth, $crc, $zlen, $len);
rewind($stream);
while (!feof($stream)) {
$data = fread($stream, $block_size);
// send data
$this->send($data);
}
}
/**
* 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->addFile($path, file_get_contents($path));
*
* // write footer to stream
* $zip->finish();
*
* @return void
*/
public function finish() {
// add trailing cdr record
$this->addCdr($this->opt);
$this->clear();
}
/**
* Create and send zip header for this file.
*
* @param String $name
* @param Array $opt
* @param Integer $meth
* @param string $crc
* @param Integer $zlen
* @param Integer $len
* @return void
*/
protected function addFileHeader($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'] = isset($opt['time']) && !empty($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',
0x000A
), // version needed to extract
//FIXED as mentioned in http://linlog.skepticats.com/entries/2012/02/Streaming_ZIP_files_in_PHP.php
//and http://stackoverflow.com/questions/5573211/dynamically-created-zip-files-by-zipstream-in-php-wont-open-in-osx
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->packFields($fields);
$cdr_len = strlen($ret) + $nlen + $zlen;
// print header and filename
$this->send($ret . $name);
// add to central directory record and increment offset
$this->addToCdr($name, $opt, $meth, $crc, $zlen, $len, $cdr_len);
}
/**
* Add a large file from the given path.
*
* @param String $name
* @param String $path
* @param array $opt
* @return void
* @throws \ZipStream\Exception\InvalidOptionException
*/
protected function addLargeFile($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[self::OPTION_LARGE_FILE_METHOD];
if ($meth_str == self::METHOD_STORE) {
// store method
$meth = 0x00;
$crc = hexdec(hash_file($algo, $path));
} elseif ($meth_str == self::METHOD_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 (!feof($fh)) {
$data = fread($fh, $block_size);
hash_update($hash_ctx, $data);
}
rewind($fh);
$filter = stream_filter_append($fh, 'zlib.deflate', STREAM_FILTER_READ, 6);
while (!feof($fh)) {
$data = fread($fh, $block_size);
$zlen += strlen($data);
}
stream_filter_remove($filter);
// close file and finalize crc
fclose($fh);
$crc = hexdec(hash_final($hash_ctx));
} else {
throw new InvalidOptionException('large_file_method', array(self::METHOD_STORE, self::METHOD_DEFLATE), $meth_str);
}
// send file header
$this->addFileHeader($name, $opt, $meth, $crc, $zlen, $len);
// open input file
$fh = fopen($path, 'rb');
if ($meth_str == self::METHOD_DEFLATE) {
$filter = stream_filter_append($fh, 'zlib.deflate', STREAM_FILTER_READ, 6);
}
// send file blocks
while (!feof($fh)) {
$data = fread($fh, $block_size);
// send data
$this->send($data);
}
if (isset($filter) && is_resource($filter)) {
stream_filter_remove($filter);
}
// close input file
fclose($fh);
}
/**
* Is this file larger than large_file_size?
*
* @param string $path
* @return Boolean
*/
protected function isLargeFile($path) {
$st = stat($path);
return ($this->opt[self::OPTION_LARGE_FILE_SIZE] > 0) && ($st['size'] > $this->opt[self::OPTION_LARGE_FILE_SIZE]);
}
/**
* Save file attributes for trailing CDR record.
*
* @param String $name
* @param Array $opt
* @param Integer $meth
* @param string $crc
* @param Integer $zlen
* @param Integer $len
* @param Integer $rec_len
* @return void
* @return void
*/
private function addToCdr($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.
*
* @param array $args
* @return void
*/
protected function addCdrFile($args) {
list($name, $opt, $meth, $crc, $zlen, $len, $ofs) = $args;
// get attributes
$comment = isset($opt['comment']) && !empty($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->packFields($fields) . $name . $comment;
$this->send($ret);
// increment cdr offset
$this->cdr_ofs += strlen($ret);
}
/**
* Send CDR EOF (Central Directory Record End-of-File) record.
*
* @param array $opt
* @return void
*/
protected function addCdrEof($opt = null) {
$num = count($this->files);
$cdr_len = $this->cdr_ofs;
$cdr_ofs = $this->ofs;
// grab comment (if specified)
$comment = '';
if ($opt && isset($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->packFields($fields) . $comment;
$this->send($ret);
}
/**
* Add CDR (Central Directory Record) footer.
*
* @param array $opt
* @return void
*/
protected function addCdr($opt = null) {
foreach ($this->files as $file)
$this->addCdrFile($file);
$this->addCdrEof($opt);
}
/**
* Clear all internal variables. Note that the stream object is not
* usable after this.
*
* @return void
*/
protected function clear() {
$this->files = array();
$this->ofs = 0;
$this->cdr_ofs = 0;
$this->opt = array();
}
/**
* Send HTTP headers for this stream.
*
* @return void
*/
protected function sendHttpHeaders() {
// grab options
$opt = $this->opt;
// grab content type from options
$content_type = 'application/x-zip';
if (isset($opt[self::OPTION_CONTENT_TYPE])) {
$content_type = $this->opt[self::OPTION_CONTENT_TYPE];
}
// grab content disposition
$disposition = 'attachment';
if (isset($opt[self::OPTION_CONTENT_DISPOSITION])) {
$disposition = $opt[self::OPTION_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'
);
$call = $this->opt[self::OPTION_HTTP_HEADER_CALLBACK];
foreach ($headers as $key => $val)
$call("$key: $val");
}
/**
* Send string, sending HTTP headers if necessary.
*
* @param String $str
* @return void
*/
protected function send($str) {
if ($this->need_headers) {
$this->sendHttpHeaders();
}
$this->need_headers = false;
fwrite($this->opt[self::OPTION_OUTPUT_STREAM], $str);
}
/**
* Convert a UNIX timestamp to a DOS timestamp.
*
* @param Integer $when
* @return Integer DOS Timestamp
*/
protected final function dostime($when) {
// 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.
*
* @param array $fields
* @return string
*/
protected function packFields($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);
}
}