<?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,
$need_headers
= false,
$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
(!isset(
$this
->opt[
'large_file_size'
]))
$this
->opt[
'large_file_size'
] = 20 * 1024 * 1024;
if
(!isset(
$this
->opt[
'large_file_method'
]))
$this
->opt[
'large_file_method'
] =
'store'
;
$this
->output_name =
$name
;
if
(
$name
|| (isset(
$opt
[
'send_http_headers'
])
&&
$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'
] = isset(
$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
= isset(
$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
&& 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
->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
(isset(
$opt
[
'content_type'
]))
$content_type
=
$this
->opt[
'content_type'
];
# grab content disposition
$disposition
=
'attachment'
;
if
(isset(
$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
);
}
};
?>