outputStream = fopen('php://output', 'w'); $this->outputName = $name; $this->needHeaders = true; } /** * addFileFromPath * add a file at path to the archive. */ public function addFile($name, $path){ $name = $this->filterFilename($name); $zipMethod = static::METHOD_STORE; $headerLength = $this->addFileHeader($name,$zipMethod); $zipLength = $fileLength = filesize_64($path); $fh = fopen($path, 'rb'); while (!feof($fh)) { $data = fread($fh, 1048576); $this->send($data); } fclose($fh); $crc = hexdec(hash_file('crc32b', $path)); $this->addFileFooter($name,$zipMethod, $crc, $zipLength, $fileLength, $headerLength); } /** * addFile_from_stream */ public function addFileFromStream($name, $stream){ $name = $this->filterFilename($name); $zipMethod = static::METHOD_STORE; $headerLength = $this->addFileHeader($name,$zipMethod); fseek($stream, 0, SEEK_END); $zipLength = $fileLength = ftell($stream); rewind($stream); $hashCtx = hash_init('crc32b'); while (!feof($stream)) { $data = fread($stream, 1048576); hash_update($hashCtx, $data); $this->send($data); } $crc = hexdec(hash_final($hashCtx)); $this->addFileFooter($name,$zipMethod, $crc, $zipLength, $fileLength, $headerLength); } public function finish(){ foreach ($this->files as $file){ $this->addCdrFile($file); } $this->addCdr64Eof(); $this->addCdr64Locator(); $this->addCdrEof(); $this->clear(); } /** * Create and send zip header for this file. * * @param String $name * @param Integer $zipMethod * @return void */ protected function addFileHeader($name,$zipMethod){ $name = preg_replace('/^\\/+/', '', $name); $nlen = strlen($name); $time = $this->dosTime(time()); $fields = array(// v=2byte,V=4byte,P=8byte array('V', static::FILE_HEADER_SIGNATURE), array('v', static::ZIP_VERSION_64), // 压缩版本 array('v', 0b00001000), // General purpose bit flags - data descriptor flag set array('v', $zipMethod), // Compression method array('V', $time), // Timestamp (DOS Format) time=2byte/date/2byte array('V', 0x00000000), // CRC32 of data (0 -> moved to data descriptor footer) array('V', 0xFFFFFFFF), // zip64时全1 array('V', 0xFFFFFFFF), // Length of original data (Forced to 0xFFFFFFFF for 64bit extension) array('v', $nlen), // Length of filename array('v', 32), // Extra data (32 bytes) ); $fields64 = array(// 32Byte; array('v', 0x0001), // 64Bit Extension array('v', 28), // 28bytes of data follows array('P', 0x0000000000000000), // Length of original data (0 -> moved to data descriptor footer) array('P', 0x0000000000000000), // Length of compressed data (0 -> moved to data descriptor footer) array('P', 0x0000000000000000), // Relative Header Offset array('V', 0x00000000) // Disk number ); $header = $this->packFields($fields); $header64 = $this->packFields($fields64); $this->send($header . $name . $header64); return strlen($header) + $nlen + strlen($header64); } /** * Create and send data descriptor footer for this file. */ protected function addFileFooter($name,$zipMethod, $crc, $zipLength, $fileLength, $headerLength){ $fields = array( array('V', static::DATA_DESCRIPTOR_SIGNATURE), array('V', $crc), // CRC32 array('P', $zipLength), // 压缩后大小 array('P', $fileLength), // 原始大小 ); $footer = $this->packFields($fields); $this->send($footer); $totalLength = $headerLength + $zipLength + $flen; $this->addToCdr($name,$zipMethod, $crc, $zipLength, $fileLength, $totalLength); } /** * Save file attributes for trailing CDR record. * * @param String $name * @param Integer $zipMethod * @param string $crc * @param Integer $zipLength * @param Integer $len * @param Integer $rec_len * @return void * @return void */ private function addToCdr($name,$zipMethod, $crc, $zipLength, $len, $rec_len) { $this->files[] = array( $name, $zipMethod, $crc, $zipLength, $len, $this->ofs ); $this->ofs += $rec_len; } /** * Send CDR record for specified file. */ protected function addCdrFile($args){ list($name,$zipMethod, $crc, $zipLength, $len, $offset) = $args; $comment = ''; $time = $this->dosTime(time()); $fields = array( array('V', static::CDR_FILE_SIGNATURE), // Central file header signature array('v', static::ZIP_VERSION_64), // Made by version array('v', static::ZIP_VERSION_64), // Extract by version array('v', 0b00001000), // General purpose bit flags - data descriptor flag set array('v', $zipMethod), // Compression method array('V', $time), // Timestamp (DOS Format) array('V', $crc), // CRC32 array('V', 0xFFFFFFFF), // Compressed Data Length (Forced to 0xFFFFFFFF for 64bit Extension) array('V', 0xFFFFFFFF), // Original Data Length (Forced to 0xFFFFFFFF for 64bit Extension) array('v', strlen($name)), // Length of filename array('v', 32), // Extra data len (32bytes of 64bit Extension) array('v', strlen($comment)), // Length of comment array('v', 0), // Disk number array('v', 0), // Internal File Attributes array('V', 32), // External File Attributes array('V', 0xFFFFFFFF) // Relative offset of local header (Forced to 0xFFFFFFFF for 64bit Extension) ); $fields64 = array( array('v', 0x0001), // 64Bit Extension array('v', 28), // 28bytes of data follows array('P', $len), // Length of original data (0 -> moved to data descriptor footer) array('P', $zipLength), // Length of compressed data (0 -> moved to data descriptor footer) array('P', $offset), // Relative Header Offset array('V', 0) // Disk number ); $header = $this->packFields($fields); $footer = $this->packFields($fields64); $ret = $header . $name . $comment . $footer; $this->send($ret); $this->cdr_ofs += strlen($ret); } /** * Send ZIP64 CDR EOF (Central Directory Record End-of-File) record. */ protected function addCdr64Eof(){ $num = count($this->files); $cdrLength = $this->cdr_ofs; $cdrOffset = $this->ofs; $fields = array( array('V', static::ZIP64_CDR_EOF_SIGNATURE), // ZIP64 end of central file header signature array('P', 44), // Length of data below this header (length of block - 12) = 44 array('v', static::ZIP_VERSION_64), // Made by version array('v', static::ZIP_VERSION_64), // Extract by version array('V', 0x00), // disk number array('V', 0x00), // no of disks array('P', $num), // no of entries on disk array('P', $num), // no of entries in cdr array('P', $cdrLength), // CDR size array('P', $cdrOffset), // CDR offset ); $ret = $this->packFields($fields); $this->send($ret); } /** * Send ZIP64 CDR Locator (Central Directory Record Locator) record. */ protected function addCdr64Locator(){ $num = count($this->files); $cdrLength = $this->cdr_ofs; $cdrOffset = $this->ofs; $fields = array( array('V', static::ZIP64_CDR_LOCATOR_SIGNATURE), // ZIP64 end of central file header signature array('V', 0x00), // Disc number containing CDR64EOF array('P', $cdrOffset + $cdrLength), // CDR offset array('V', 1), // Total number of disks ); $ret = $this->packFields($fields); $this->send($ret); } /** * Send CDR EOF (Central Directory Record End-of-File) record. */ protected function addCdrEof(){ $num = count($this->files); $cdrLength = $this->cdr_ofs; $cdrOffset = $this->ofs; $comment = ''; $fields = array( array('V', static::CDR_EOF_SIGNATURE), // end of central file header signature array('v', 0x00), // disk number array('v', 0x00), // no of disks array('v', $num), // no of entries on disk array('v', $num), // no of entries in cdr array('V', 0xFFFFFFFF), // CDR size (Force to 0xFFFFFFFF for Zip64) array('V', 0xFFFFFFFF), // CDR offset (Force to 0xFFFFFFFF for Zip64) array('v', strlen($comment)), // Zip Comment size ); $ret = $this->packFields($fields) . $comment; $this->send($ret); } /** * Add CDR (Central Directory Record) footer. */ protected function addCdr(){ foreach ($this->files as $file){ $this->addCdrFile($file); } $this->addCdrEof(); } protected function clear(){ $this->files = array(); $this->ofs = 0; $this->cdr_ofs = 0; } protected function sendHttpHeaders(){ $disposition = 'attachment'; if ($this->outputName) { $safeOutput = trim(str_replace(['"', "'", '\\', ';', "\n", "\r"], '', $this->outputName)); $urlencoded = rawurlencode($safeOutput); $disposition .= "; filename*=utf-8''{$urlencoded}"; } $headers = array( 'Content-Type' => 'application/x-zip', 'Content-Disposition' => $disposition, 'Pragma' => 'public', 'Cache-Control' => 'public, must-revalidate', 'Content-Transfer-Encoding' => 'binary' ); foreach ($headers as $key => $value) { header($key.': '.$value); } } /** * Send string, sending HTTP headers if necessary. */ protected function send($str){ if ($this->needHeaders) { $this->sendHttpHeaders(); } $this->needHeaders = false; fwrite($this->outputStream, $str); } /** * 转换时间戳为dos时间 */ protected final function dosTime($when){ $d = getdate($when); if ($d['year'] < 1980) { $d = array( 'year' => 1980, 'mon' => 1, 'mday' => 1, 'hours' => 0, 'minutes' => 0, 'seconds' => 0 ); } $d['year'] -= 1980; return ($d['year'] << 25) | ($d['mon'] << 21) | ($d['mday'] << 16) | ($d['hours'] << 11) | ($d['minutes'] << 5) | ($d['seconds'] >> 1); } protected function packFields($fields){ $fmt = ''; $args = array(); foreach ($fields as $field) { $fmt .= $field[0]; $args[] = $field[1]; } array_unshift($args, $fmt); return call_user_func_array('pack', $args); } protected function filterFilename($filename){ return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $filename); } }