1: <?php
2: error_reporting(0);
3: ##########################################################################
4: # ZipStream - Streamed, dynamically generated zip archives. #
5: # by Paul Duncan <pabs@pablotron.org> #
6: # #
7: # Copyright (C) 2007-2009 Paul Duncan <pabs@pablotron.org> #
8: # #
9: # Permission is hereby granted, free of charge, to any person obtaining #
10: # a copy of this software and associated documentation files (the #
11: # "Software"), to deal in the Software without restriction, including #
12: # without limitation the rights to use, copy, modify, merge, publish, #
13: # distribute, sublicense, and/or sell copies of the Software, and to #
14: # permit persons to whom the Software is furnished to do so, subject to #
15: # the following conditions: #
16: # #
17: # The above copyright notice and this permission notice shall be #
18: # included in all copies or substantial portions of the of the Software. #
19: # #
20: # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, #
21: # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF #
22: # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #
23: # IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR #
24: # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, #
25: # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR #
26: # OTHER DEALINGS IN THE SOFTWARE. #
27: ##########################################################################
28: #
29: # ZipStream - Streamed, dynamically generated zip archives.
30: # by Paul Duncan <pabs@pablotron.org>
31: #
32: # In 2012 altered and bugfixes including:
33: # - fix CRC calculation
34: # - Performance improvement for large files
35: # - fix Header for MacOSX
36: # - fix PHP Notices
37: # by Philipp Hammer-Pohlau
38: #
39: # Requirements:
40: #
41: # * PHP version 5.1.2 or newer.
42: #
43: # Usage:
44: #
45: # Streaming zip archives is a simple, three-step process:
46: #
47: # 1. Create the zip stream:
48: #
49: # $zip = new ZipStream('example.zip');
50: #
51: # 2. Add one or more files to the archive:
52: #
53: # # add first file
54: # $data = file_get_contents('some_file.gif');
55: # $zip->add_file('some_file.gif', $data);
56: #
57: # # add second file
58: # $data = file_get_contents('some_file.gif');
59: # $zip->add_file('another_file.png', $data);
60: #
61: # 3. Finish the zip stream:
62: #
63: # $zip->finish();
64: #
65: # You can also add an archive comment, add comments to individual files,
66: # and adjust the timestamp of files. See the API documentation for each
67: # method below for additional information.
68: #
69: # Example:
70: #
71: # # create a new zip stream object
72: # $zip = new ZipStream('some_files.zip');
73: #
74: # # list of local files
75: # $files = array('foo.txt', 'bar.jpg');
76: #
77: # # read and add each file to the archive
78: # foreach ($files as $path)
79: # $zip->add_file($path, file_get_contents($path));
80: #
81: # # write archive footer to stream
82: # $zip->finish();
83: #
84: class ZipStream {
85: const VERSION = '0.2.3';
86:
87: var $opt = array(),
88: $files = array(),
89: $cdr_ofs = 0,
90: $ofs = 0;
91:
92: #
93: # Create a new ZipStream object.
94: #
95: # Parameters:
96: #
97: # $name - Name of output file (optional).
98: # $opt - Hash of archive options (optional, see "Archive Options"
99: # below).
100: #
101: # Archive Options:
102: #
103: # comment - Comment for this archive.
104: # content_type - HTTP Content-Type. Defaults to 'application/x-zip'.
105: # content_disposition - HTTP Content-Disposition. Defaults to
106: # 'attachment; filename=\"FILENAME\"', where
107: # FILENAME is the specified filename.
108: # large_file_size - Size, in bytes, of the largest file to try
109: # and load into memory (used by
110: # add_file_from_path()). Large files may also
111: # be compressed differently; see the
112: # 'large_file_method' option.
113: # large_file_method - How to handle large files. Legal values are
114: # 'store' (the default), or 'deflate'. Store
115: # sends the file raw and is significantly
116: # faster, while 'deflate' compresses the file
117: # and is much, much slower. Note that deflate
118: # must compress the file twice and extremely
119: # slow.
120: # send_http_headers - Boolean indicating whether or not to send
121: # the HTTP headers for this file.
122: #
123: # Note that content_type and content_disposition do nothing if you are
124: # not sending HTTP headers.
125: #
126: # Large File Support:
127: #
128: # By default, the method add_file_from_path() will send send files
129: # larger than 20 megabytes along raw rather than attempting to
130: # compress them. You can change both the maximum size and the
131: # compression behavior using the large_file_* options above, with the
132: # following caveats:
133: #
134: # * For "small" files (e.g. files smaller than large_file_size), the
135: # memory use can be up to twice that of the actual file. In other
136: # words, adding a 10 megabyte file to the archive could potentially
137: # occupty 20 megabytes of memory.
138: #
139: # * Enabling compression on large files (e.g. files larger than
140: # large_file_size) is extremely slow, because ZipStream has to pass
141: # over the large file once to calculate header information, and then
142: # again to compress and send the actual data.
143: #
144: # Examples:
145: #
146: # # create a new zip file named 'foo.zip'
147: # $zip = new ZipStream('foo.zip');
148: #
149: # # create a new zip file named 'bar.zip' with a comment
150: # $zip = new ZipStream('bar.zip', array(
151: # 'comment' => 'this is a comment for the zip file.',
152: # ));
153: #
154: # Notes:
155: #
156: # If you do not set a filename, then this library _DOES NOT_ send HTTP
157: # headers by default. This behavior is to allow software to send its
158: # own headers (including the filename), and still use this library.
159: #
160: function __construct($name = null, $opt = array()) {
161: # save options
162: $this->opt = $opt;
163:
164: # set large file defaults: size = 20 megabytes, method = store
165: if (!isset($this->opt['large_file_size']))
166: $this->opt['large_file_size'] = 20 * 1024 * 1024;
167: if (!isset($this->opt['large_file_method']))
168: $this->opt['large_file_method'] = 'store';
169:
170: $this->output_name = $name;
171: if ($name || (isset($opt['send_http_headers']) && !empty($opt['send_http_headers'])))
172: $this->need_headers = true;
173: }
174:
175: #
176: # add_file - add a file to the archive
177: #
178: # Parameters:
179: #
180: # $name - path of file in archive (including directory).
181: # $data - contents of file
182: # $opt - Hash of options for file (optional, see "File Options"
183: # below).
184: #
185: # File Options:
186: # time - Last-modified timestamp (seconds since the epoch) of
187: # this file. Defaults to the current time.
188: # comment - Comment related to this file.
189: #
190: # Examples:
191: #
192: # # add a file named 'foo.txt'
193: # $data = file_get_contents('foo.txt');
194: # $zip->add_file('foo.txt', $data);
195: #
196: # # add a file named 'bar.jpg' with a comment and a last-modified
197: # # time of two hours ago
198: # $data = file_get_contents('bar.jpg');
199: # $zip->add_file('bar.jpg', $data, array(
200: # 'time' => time() - 2 * 3600,
201: # 'comment' => 'this is a comment about bar.jpg',
202: # ));
203: #
204: function add_file($name, $data, $opt = array()) {
205: # compress data
206: $zdata = gzdeflate($data);
207:
208: # calculate header attributes
209: $crc = crc32($data);
210: $zlen = strlen($zdata);
211: $len = strlen($data);
212: $meth = 0x08;
213:
214: # send file header
215: $this->add_file_header($name, $opt, $meth, $crc, $zlen, $len);
216:
217: # print data
218: $this->send($zdata);
219: }
220:
221: #
222: # add_file_from_path - add a file at path to the archive.
223: #
224: # Note that large files may be compresed differently than smaller
225: # files; see the "Large File Support" section above for more
226: # information.
227: #
228: # Parameters:
229: #
230: # $name - name of file in archive (including directory path).
231: # $path - path to file on disk (note: paths should be encoded using
232: # UNIX-style forward slashes -- e.g '/path/to/some/file').
233: # $opt - Hash of options for file (optional, see "File Options"
234: # below).
235: #
236: # File Options:
237: # time - Last-modified timestamp (seconds since the epoch) of
238: # this file. Defaults to the current time.
239: # comment - Comment related to this file.
240: #
241: # Examples:
242: #
243: # # add a file named 'foo.txt' from the local file '/tmp/foo.txt'
244: # $zip->add_file_from_path('foo.txt', '/tmp/foo.txt');
245: #
246: # # add a file named 'bigfile.rar' from the local file
247: # # '/usr/share/bigfile.rar' with a comment and a last-modified
248: # # time of two hours ago
249: # $path = '/usr/share/bigfile.rar';
250: # $zip->add_file_from_path('bigfile.rar', $path, array(
251: # 'time' => time() - 2 * 3600,
252: # 'comment' => 'this is a comment about bar.jpg',
253: # ));
254: #
255: function add_file_from_path($name, $path, $opt = array()) {
256: if ($this->is_large_file($path)) {
257: # file is too large to be read into memory; add progressively
258: $this->add_large_file($name, $path, $opt);
259: } else {
260: # file is small enough to read into memory; read file contents and
261: # handle with add_file()
262: $data = file_get_contents($path);
263: $this->add_file($name, $data, $opt);
264: }
265: }
266:
267: #
268: # finish - Write zip footer to stream.
269: #
270: # Example:
271: #
272: # # add a list of files to the archive
273: # $files = array('foo.txt', 'bar.jpg');
274: # foreach ($files as $path)
275: # $zip->add_file($path, file_get_contents($path));
276: #
277: # # write footer to stream
278: # $zip->finish();
279: #
280: function finish() {
281: # add trailing cdr record
282: $this->add_cdr($this->opt);
283: $this->clear();
284: }
285:
286: ###################
287: # PRIVATE METHODS #
288: ###################
289:
290: #
291: # Create and send zip header for this file.
292: #
293: private function add_file_header($name, $opt, $meth, $crc, $zlen, $len) {
294: # strip leading slashes from file name
295: # (fixes bug in windows archive viewer)
296: $name = preg_replace('/^\\/+/', '', $name);
297:
298: # calculate name length
299: $nlen = strlen($name);
300:
301: # create dos timestamp
302: if (!isset($opt['time'])) {
303: $opt['time'] = time();
304: }
305: $dts = $this->dostime($opt['time']);
306:
307: # build file header
308: $fields = array( # (from V.A of APPNOTE.TXT)
309: array('V', 0x04034b50), # local file header signature
310: array('v', 0x0014), # version needed to extract
311: array('v', 0x00), # general purpose bit flag
312: array('v', $meth), # compresion method (deflate or store)
313: array('V', $dts), # dos timestamp
314: array('V', $crc), # crc32 of data
315: array('V', $zlen), # compressed data length
316: array('V', $len), # uncompressed data length
317: array('v', $nlen), # filename length
318: array('v', 0), # extra data len
319: );
320:
321: # pack fields and calculate "total" length
322: $ret = $this->pack_fields($fields);
323: $cdr_len = strlen($ret) + $nlen + $zlen;
324:
325: # print header and filename
326: $this->send($ret . $name);
327:
328: # add to central directory record and increment offset
329: $this->add_to_cdr($name, $opt, $meth, $crc, $zlen, $len, $cdr_len);
330: }
331:
332: #
333: # Add a large file from the given path.
334: #
335: private function add_large_file($name, $path, $opt = array()) {
336: $st = stat($path);
337: $block_size = 1048576; # process in 1 megabyte chunks
338: $algo = 'crc32b';
339:
340: # calculate header attributes
341: $zlen = $len = $st['size'];
342:
343: $meth_str = $this->opt['large_file_method'];
344: if ($meth_str == 'store') {
345: # store method
346: $meth = 0x00;
347: $crc = unpack('N', hash_file($algo, $path, true));
348: $crc = $crc[1];
349: } elseif ($meth_str == 'deflate') {
350: # deflate method
351: $meth = 0x08;
352:
353: # open file, calculate crc and compressed file length
354: $fh = fopen($path, 'rb');
355: $hash_ctx = hash_init($algo);
356: $zlen = 0;
357:
358: # read each block, update crc and zlen
359: while ($data = fread($fh, $block_size)) {
360: hash_update($hash_ctx, $data);
361: $data = gzdeflate($data);
362: $zlen += strlen($data);
363: }
364:
365: # close file and finalize crc
366: fclose($fh);
367: $crc = unpack('N', hash_final($hash_ctx, true));
368: $crc = $crc[1];
369: } else {
370: die("unknown large_file_method: $meth_str");
371: }
372:
373: # send file header
374: $this->add_file_header($name, $opt, $meth, $crc, $zlen, $len);
375:
376: # open input file
377: $fh = fopen($path, 'rb');
378:
379: # send file blocks
380: while ($data = fread($fh, $block_size)) {
381: if ($meth_str == 'deflate')
382: $data = gzdeflate($data);
383:
384: # send data
385: $this->send($data);
386: }
387:
388: # close input file
389: fclose($fh);
390: }
391:
392: #
393: # Is this file larger than large_file_size?
394: #
395: function is_large_file($path) {
396: $st = stat($path);
397: return ($this->opt['large_file_size'] > 0) &&
398: ($st['size'] > $this->opt['large_file_size']);
399: }
400:
401: #
402: # Save file attributes for trailing CDR record.
403: #
404: private function add_to_cdr($name, $opt, $meth, $crc, $zlen, $len, $rec_len) {
405: $this->files[] = array($name, $opt, $meth, $crc, $zlen, $len, $this->ofs);
406: $this->ofs += $rec_len;
407: }
408:
409: #
410: # Send CDR record for specified file.
411: #
412: private function add_cdr_file($args) {
413: list ($name, $opt, $meth, $crc, $zlen, $len, $ofs) = $args;
414:
415: # get attributes
416: if (isset($opt['comment'])) {
417: $comment = $opt['comment'];
418: } else {
419: $comment = '';
420: }
421:
422: # get dos timestamp
423: $dts = $this->dostime($opt['time']);
424:
425: $fields = array( # (from V,F of APPNOTE.TXT)
426: array('V', 0x02014b50), # central file header signature
427: array('v', 0x0014), # version made by
428: array('v', 0x0014), # version needed to extract
429: array('v', 0x00), # general purpose bit flag
430: array('v', $meth), # compresion method (deflate or store)
431: array('V', $dts), # dos timestamp
432: array('V', $crc), # crc32 of data
433: array('V', $zlen), # compressed data length
434: array('V', $len), # uncompressed data length
435: array('v', strlen($name)), # filename length
436: array('v', 0), # extra data len
437: array('v', strlen($comment)), # file comment length
438: array('v', 0), # disk number start
439: array('v', 0), # internal file attributes
440: array('V', 32), # external file attributes
441: array('V', $ofs), # relative offset of local header
442: );
443:
444: # pack fields, then append name and comment
445: $ret = $this->pack_fields($fields) . $name . $comment;
446:
447: $this->send($ret);
448:
449: # increment cdr offset
450: $this->cdr_ofs += strlen($ret);
451: }
452:
453: #
454: # Send CDR EOF (Central Directory Record End-of-File) record.
455: #
456: private function add_cdr_eof($opt = null) {
457: $num = count($this->files);
458: $cdr_len = $this->cdr_ofs;
459: $cdr_ofs = $this->ofs;
460:
461: # grab comment (if specified)
462: $comment = '';
463: if (isset($opt) && isset($opt['comment'])) {
464: $comment = $opt['comment'];
465: }
466:
467: $fields = array( # (from V,F of APPNOTE.TXT)
468: array('V', 0x06054b50), # end of central file header signature
469: array('v', 0x00), # this disk number
470: array('v', 0x00), # number of disk with cdr
471: array('v', $num), # number of entries in the cdr on this disk
472: array('v', $num), # number of entries in the cdr
473: array('V', $cdr_len), # cdr size
474: array('V', $cdr_ofs), # cdr ofs
475: array('v', strlen($comment)), # zip file comment length
476: );
477:
478: $ret = $this->pack_fields($fields) . $comment;
479: $this->send($ret);
480: }
481:
482: #
483: # Add CDR (Central Directory Record) footer.
484: #
485: private function add_cdr($opt = null) {
486: foreach ($this->files as $file)
487: $this->add_cdr_file($file);
488: $this->add_cdr_eof($opt);
489: }
490:
491: #
492: # Clear all internal variables. Note that the stream object is not
493: # usable after this.
494: #
495: function clear() {
496: $this->files = array();
497: $this->ofs = 0;
498: $this->cdr_ofs = 0;
499: $this->opt = array();
500: }
501:
502: ###########################
503: # PRIVATE UTILITY METHODS #
504: ###########################
505:
506: #
507: # Send HTTP headers for this stream.
508: #
509: private function send_http_headers() {
510: # grab options
511: $opt = $this->opt;
512:
513: # grab content type from options
514: $content_type = 'application/zip';
515: if (isset($opt['content_type']))
516: $content_type = $this->opt['content_type'];
517:
518: # grab content disposition
519: $disposition = 'attachment';
520: if (isset($opt['content_disposition']))
521: $disposition = $opt['content_disposition'];
522:
523: if ($this->output_name)
524: $disposition .= "; filename=\"{$this->output_name}\"";
525:
526: $headers = array(
527: 'Content-Type' => $content_type,
528: 'Content-Disposition' => $disposition,
529: 'Pragma' => 'public',
530: 'Cache-Control' => 'public, must-revalidate',
531: 'Content-Transfer-Encoding' => 'binary',
532: );
533:
534: foreach ($headers as $key => $val)
535: header("$key: $val");
536: }
537:
538: #
539: # Send string, sending HTTP headers if necessary.
540: #
541: private function send($str) {
542: if ($this->need_headers)
543: $this->send_http_headers();
544: $this->need_headers = false;
545:
546: echo $str;
547: }
548:
549: #
550: # Convert a UNIX timestamp to a DOS timestamp.
551: #
552: function dostime($when = 0) {
553: # get date array for timestamp
554: $d = getdate($when);
555:
556: # set lower-bound on dates
557: if ($d['year'] < 1980) {
558: $d = array('year' => 1980, 'mon' => 1, 'mday' => 1,
559: 'hours' => 0, 'minutes' => 0, 'seconds' => 0);
560: }
561:
562: # remove extra years from 1980
563: $d['year'] -= 1980;
564:
565: # return date string
566: return ($d['year'] << 25) | ($d['mon'] << 21) | ($d['mday'] << 16) |
567: ($d['hours'] << 11) | ($d['minutes'] << 5) | ($d['seconds'] >> 1);
568: }
569:
570: #
571: # Create a format string and argument list for pack(), then call
572: # pack() and return the result.
573: #
574: function pack_fields($fields) {
575: list ($fmt, $args) = array('', array());
576:
577: # populate format string and argument list
578: foreach ($fields as $field) {
579: $fmt .= $field[0];
580: $args[] = $field[1];
581: }
582:
583: # prepend format string to argument list
584: array_unshift($args, $fmt);
585:
586: # build output string from header and compressed data
587: return call_user_func_array('pack', $args);
588: }
589: };
590:
591: ?>
592: