From 34aeb3ff15237bb152410bba5d9d25da0ac20568 Mon Sep 17 00:00:00 2001 From: Victor Dubiniuk Date: Tue, 22 Sep 2015 23:26:59 +0300 Subject: [PATCH 1/3] Initial implementaton --- composer.json | 29 +- src/TarStreamer.php | 306 ++++++++++++++++ stream.php | 414 ---------------------- tarstream.php | 520 ---------------------------- test/testdata/lorem.txt | 5 + test/testdata/lorem2 | 3 + test/testdata/more ipsum/lorem2.txt | 24 ++ test/usage.php | 72 ++++ 8 files changed, 425 insertions(+), 948 deletions(-) create mode 100644 src/TarStreamer.php delete mode 100644 stream.php delete mode 100644 tarstream.php create mode 100644 test/testdata/lorem.txt create mode 100644 test/testdata/lorem2 create mode 100644 test/testdata/more ipsum/lorem2.txt create mode 100644 test/usage.php diff --git a/composer.json b/composer.json index 2269c1d..59dcfe1 100644 --- a/composer.json +++ b/composer.json @@ -1,15 +1,16 @@ { - "name": "barracudanetworks/archivestream-php", - "type": "library", - "description": "A library for dynamically streaming dynamic tar or zip files without the need to have the complete file stored on the server.", - "keywords": ["zip", "tar", "archive", "stream", "php"], - "homepage": "https://github.com/barracudanetworks/ArchiveStream-php", - "license": "MIT", - "require": { - "php": ">=5.1.2", - "ext-gmp": "*" - }, - "autoload": { - "files": [ "stream.php", "tarstream.php", "zipstream.php" ] - } -} \ No newline at end of file + "name": "deepdiver1975/tarstreamer", + "type": "library", + "description": "A library for dynamically streaming dynamic tar files without the need to have the complete file stored on the server.", + "keywords": ["tar", "archive", "stream", "php"], + "homepage": "https://github.com/DeepDiver1975/TarStreamer", + "license": "MIT", + "require": { + "php": ">=5.3.8" + }, + "autoload": { + "psr-4": { + "DeepDiver1975\\TarStreamer\\": "src/" + } + } +} diff --git a/src/TarStreamer.php b/src/TarStreamer.php new file mode 100644 index 0000000..f32731f --- /dev/null +++ b/src/TarStreamer.php @@ -0,0 +1,306 @@ +outstream = $options['outstream']; + } else { + $this->outstream = fopen('php://output', 'w'); + // turn off output buffering + while (ob_get_level() > 0){ + ob_end_flush(); + } + } + } + + /** + * Send appropriate http headers before streaming the tar file and disable output buffering. + * This method, if used, has to be called before adding anything to the tar file. + * + * @param string $archiveName Filename of archive to be created (optional, default 'archive.tar') + * @param string $contentType Content mime type to be set (optional, default 'application/x-tar') + */ + public function sendHeaders($archiveName = 'archive.tar', $contentType = 'application/x-tar'){ + if (headers_sent($headerFile, $headerLine)){ + die( + "

Error: Unable to send file " . + "$archiveName. HTML Headers have already been sent from " . + "$headerFile in line $headerLine" . + "

" + ); + } + $buffer = ob_get_contents(); + if (!empty($buffer)){ + die( + "\n

Error: Unable to send file " . + "$archiveName.epub. Output buffer " . + "already contains text (typically warnings or errors).

" + ); + } + + $headers = [ + 'Pragma' => 'public', + 'Last-Modified' => gmdate('D, d M Y H:i:s T'), + 'Expires' => '0', + 'Accept-Ranges' => 'bytes', + 'Connection' => 'Keep-Alive', + 'Content-Type' => $contentType, + 'Content-Disposition' => 'attachment; filename="' . $archiveName . '";', + 'Cache-Control' => 'public, must-revalidate', + 'Content-Transfer-Encoding' => 'binary', + ]; + + foreach ($headers as $key => $val){ + header("$key: $val"); + } + } + + /** + * Add a file to the archive at the specified location and file name. + * + * @param string $stream Stream to read data from + * @param string $filePath Filepath and name to be used in the archive. + * @param array $options Optional, additional options + * Valid options are: + * * int timestamp: timestamp for the file (default: current time) + * @return bool $success + */ + public function addFileFromStream($stream, $filePath, $size, $options = []){ + if (!is_resource($stream) || get_resource_type($stream) != 'stream'){ + return false; + } + + $this->initFileStreamTransfer($filePath, self::REGTYPE, $size, $options); + + // send file blocks + while ($data = fread($stream, $this->blockSize)){ + // send data + $this->streamFilePart($data); + } + + // complete the file stream + $this->completeFileStream($size); + + return true; + } + + /** + * Explicitly adds a directory to the tar (necessary for empty directories) + * + * @param string $name Name (path) of the directory + * @param array $opt Additional options to set + * Valid options are: + * * int timestamp: timestamp for the file (default: current time) + * @return void + */ + public function addEmptyDir($name, $opt = []){ + $opt['type'] = self::DIRTYPE; + + // send header + $this->initFileStreamTransfer($name, self::DIRTYPE, 0, $opt); + + // complete the file stream + $this->completeFileStream(0); + } + + /** + * Close the archive. + * A closed archive can no longer have new files added to it. After + * closing, the file is completely written to the output stream. + * @return bool $success */ + public function finalize(){ + // tar requires the end of the file have two 512 byte null blocks + $this->send(pack('a1024', '')); + + // flush the data to the output + fflush($this->outstream); + return true; + } + + /** + * Initialize a file stream + * + * @param string $name file path or just name + * @param int $type type of the item + * @param int $size size in bytes of the file + * @param array $opt array (optional) + * Valid options are: + * * int timestamp: timestamp for the file (default: current time) + */ + protected function initFileStreamTransfer($name, $type, $size, $opt = []){ + $dirName = (dirname($name) == '.') ? '' : dirname($name); + $fileName = ($type == self::DIRTYPE) ? basename($name) . '/' : basename($name); + + + // handle long file names via PAX + if (strlen($fileName) > 99 || strlen($dirName) > 154){ + $pax = $this->paxGenerate([ 'path' => $dirName . '/' . $fileName]); + $paxSize = strlen($pax); + + $this->initFileStreamTransfer('', self::XHDTYPE, $paxSize); + + $this->streamFilePart($pax); + $this->completeFileStream($paxSize); + } + + // process optional arguments + $time = isset($opt['timestamp']) ? $opt['timestamp'] : time(); + + // build data descriptor + $fields = [ + ['a100', substr($fileName, 0, 100)], + ['a8', str_pad('777', 7, '0', STR_PAD_LEFT)], + ['a8', decoct(str_pad('0', 7, '0', STR_PAD_LEFT))], + ['a8', decoct(str_pad('0', 7, '0', STR_PAD_LEFT))], + ['a12', str_pad(decoct($size), 11, '0', STR_PAD_LEFT)], + ['a12', str_pad(decoct($time), 11, '0', STR_PAD_LEFT)], + ['a8', ''], + ['a1', $type], + ['a100', ''], + ['a6', 'ustar'], + ['a2', '00'], + ['a32', ''], + ['a32', ''], + ['a8', ''], + ['a8', ''], + ['a155', substr($dirName, 0, 155)], + ['a12', ''], + ]; + + // pack fields and calculate "total" length + $header = $this->packFields($fields); + + // Compute header checksum + $checksum = str_pad(decoct($this->computeUnsignedChecksum($header)), 6, "0", STR_PAD_LEFT); + for ($i = 0; $i < 6; $i++){ + $header[(148 + $i)] = substr($checksum, $i, 1); + } + $header[154] = chr(0); + $header[155] = chr(32); + + // print header + $this->send($header); + } + + /** + * Stream the next part of the current file stream. + * + * @param $data raw data to send + */ + protected function streamFilePart($data){ + // send data + $this->send($data); + + // flush the data to the output + fflush($this->outstream); + } + + /** + * Complete the current file stream + * + */ + protected function completeFileStream($size){ + // ensure we pad the last block so that it is 512 bytes + if (($mod = ($size % 512)) > 0){ + $this->send(pack('a' . (512 - $mod), '')); + } + + // flush the data to the output + fflush($this->outstream); + } + + /** + * Send string, sending HTTP headers if necessary. + * + * @param string $data data to send + */ + protected function send($data){ + if ($this->needHeaders){ + $this->sendHeaders(); + } + $this->needHeaders = false; + + fwrite($this->outstream, $data); + } + + /** + * Create a format string and argument list for pack(), then call pack() and return the result. + * + * @param array key being the format string and value being the data to pack + * @return string binary packed data returned from pack() + */ + protected function packFields($fields){ + list ($fmt, $args) = ['', []]; + + // 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); + } + + /** + * Generate unsigned checksum of header + * + * @param string $header + * @return string unsigned checksum + */ + protected function computeUnsignedChecksum($header){ + $unsignedChecksum = 0; + for ($i = 0; $i < 512; $i++){ + $unsignedChecksum += ord($header[$i]); + } + for ($i = 0; $i < 8; $i++){ + $unsignedChecksum -= ord($header[148 + $i]); + } + $unsignedChecksum += ord(" ") * 8; + + return $unsignedChecksum; + } + + /** + * Generate a PAX string + * + * @param array $fields key value mapping + * @return string PAX formated string + * @link http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&manpath=FreeBSD+8-current tar / PAX spec + */ + protected function paxGenerate($fields){ + $lines = ''; + foreach ($fields as $name => $value){ + // build the line and the size + $line = ' ' . $name . '=' . $value . "\n"; + $size = strlen(strlen($line)) + strlen($line); + + // add the line + $lines .= $size . $line; + } + + return $lines; + } +} diff --git a/stream.php b/stream.php deleted file mode 100644 index b4a3ac8..0000000 --- a/stream.php +++ /dev/null @@ -1,414 +0,0 @@ -opt = $opt; - - // if a $base_path was passed set the protected property with that value, otherwise leave it empty - $this->container_dir_name = isset($base_path) ? $base_path . '/' : ''; - - // 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_files_only'])) - { - $this->opt['large_files_only'] = false; - } - - $this->output_name = $name; - if ($name || isset($opt['send_http_headers'])) - { - $this->need_headers = true; - } - - // turn off output buffering - while (ob_get_level() > 0) - { - ob_end_flush(); - } - } - - /** - * Create instance based on useragent string - * - * @param string $base_filename the base of the filename that will be appended with the correct extention - * @param array $opt hash of archive options (see above for list) - * @return ArchiveStream for either zip or tar - * @access public - */ - public static function instance_by_useragent($base_filename = null, $opt = array()) - { - $user_agent = (isset($_SERVER['HTTP_USER_AGENT']) ? strtolower($_SERVER['HTTP_USER_AGENT']) : ''); - - // detect windows and use zip - if (strpos($user_agent, 'windows') !== false) - { - require_once(__DIR__ . '/zipstream.php'); - $filename = (($base_filename === null) ? null : $base_filename . '.zip'); - return new ArchiveStream_Zip($filename, $opt, $base_filename); - } - // fallback to tar - else - { - require_once(__DIR__ . '/tarstream.php'); - $filename = (($base_filename === null) ? null : $base_filename . '.tar'); - return new ArchiveStream_Tar($filename, $opt, $base_filename); - } - } - - /** - * Add file to the archive - * - * Parameters: - * - * @param string $name path of file in archive (including directory). - * @param string $data contents of file - * @param array $opt hash of file options (see above for list) - * @access public - */ - public function add_file($name, $data, $opt = array()) - { - // calculate header attributes - $this->meth_str = 'deflate'; - $meth = 0x08; - - // send file header - $this->init_file_stream_transfer($name, strlen($data), $opt, $meth); - - // send data - $this->stream_file_part($data, $single_part = true); - - // complete the file stream - $this->complete_file_stream(); - } - - /** - * Add file by path - * - * @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 file options (see above for list) - * @access public - */ - public function add_file_from_path($name, $path, $opt = array()) - { - if ($this->opt['large_files_only'] || $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); - } - } - - /** - * Log an error to be output at the end of the archive - * - * @param string $message error text to display in log file - */ - public function push_error($message) - { - $this->errors[] = (string) $message; - } - - /** - * Set whether or not all elements in the archive will be placed within one container directory - * - * @param bool $bool true to use contaner directory, false to prevent using one. Defaults to false - */ - public function set_use_container_dir($bool = false) - { - $this->use_container_dir = (bool) $bool; - } - - /** - * Set the name filename for the error log file when it's added to the archive - * - * @param string $name the filename for the error log - */ - public function set_error_log_filename($name) - { - if (isset($name)) - { - $this->error_log_filename = (string) $name; - } - } - - /** - * Set the first line of text in the error log file - * - * @param string $msg the text to display on the first line of the error log file - */ - public function set_error_header_text($msg) - { - if (isset($msg)) - { - $this->error_header_text = (string) $msg; - } - } - - /*************************** - * PRIVATE UTILITY METHODS * - ***************************/ - - /** - * Add a large file from the given path - * - * @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 file options (see above for list) - * @access protected - */ - protected function add_large_file($name, $path, $opt = array()) - { - // send file header - $this->init_file_stream_transfer($name, filesize($path), $opt); - - // open input file - $fh = fopen($path, 'rb'); - - // send file blocks - while ($data = fread($fh, $this->block_size)) - { - // send data - $this->stream_file_part($data); - } - - // close input file - fclose($fh); - - // complete the file stream - $this->complete_file_stream(); - } - - /** - * Is this file larger than large_file_size? - * - * @param string $path path to file on disk - * @return bool true if large, false if small - * @access protected - */ - protected function is_large_file($path) - { - $st = stat($path); - return ($this->opt['large_file_size'] > 0) && ($st['size'] > $this->opt['large_file_size']); - } - - /** - * Send HTTP headers for this stream. - * - * @access private - */ - private function send_http_headers() - { - // grab options - $opt = $this->opt; - - // grab content type from options - if ( isset($opt['content_type']) ) - $content_type = $opt['content_type']; - else - $content_type = 'application/x-zip'; - - // grab content type encoding from options and append to the content type option - if ( isset($opt['content_type_encoding']) ) - $content_type .= '; charset=' . $opt['content_type_encoding']; - - // 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. - * - * @param string $data data to send - * @access protected - */ - protected function send( $data ) - { - if ($this->need_headers) - $this->send_http_headers(); - $this->need_headers = false; - - echo $data; - } - - /** - * If errors were encountered, add an error log file to the end of the archive - */ - public function add_error_log() - { - if (!empty($this->errors)) - { - $msg = $this->error_header_text; - foreach ($this->errors as $err) - { - $msg .= "\r\n\r\n" . $err; - } - - // stash current value so it can be reset later - $temp = $this->use_container_dir; - - // set to false to put the error log file in the root instead of the container directory, if we're using one - $this->use_container_dir = false; - - $this->add_file($this->error_log_filename, $msg); - - // reset to original value and dump the temp variable - $this->use_container_dir = $temp; - unset($temp); - } - } - - /** - * Convert a UNIX timestamp to a DOS timestamp. - * - * @param int $when unix timestamp - * @return string DOS timestamp - * @access protected - */ - protected 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); - } - - /** - * Split a 64bit integer to two 32bit integers - * - * @param mixed $value integer or gmp resource - * @return array containing high and low 32bit integers - * @access protected - */ - protected function int64_split($value) - { - // gmp - if (is_resource($value)) - { - $hex = str_pad(gmp_strval($value, 16), 16, '0', STR_PAD_LEFT); - - $high = $this->gmp_convert(substr($hex, 0, 8), 16, 10); - $low = $this->gmp_convert(substr($hex, 8, 8), 16, 10); - } - // int - else - { - $left = 0xffffffff00000000; - $right = 0x00000000ffffffff; - - $high = ($value & $left) >>32; - $low = $value & $right; - } - - return array($low, $high); - } - - /** - * Create a format string and argument list for pack(), then call pack() and return the result. - * - * @param array key being the format string and value being the data to pack - * @return string binary packed data returned from pack() - * @access protected - */ - protected 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); - } - - /** - * Convert a number between bases via gmp - * - * @param int $num number to convert - * @param int $base_a base to convert from - * @param int $base_b base to convert to - * @return string number in string format - * @access private - */ - private function gmp_convert($num, $base_a, $base_b) - { - $gmp_num = gmp_init($num, $base_a); - - if (!$gmp_num) - { - die("gmp_convert could not convert [$num] from base [$base_a] to base [$base_b]"); - } - - return gmp_strval ($gmp_num, $base_b); - } -} diff --git a/tarstream.php b/tarstream.php deleted file mode 100644 index 12ca89a..0000000 --- a/tarstream.php +++ /dev/null @@ -1,520 +0,0 @@ -opt = $opt; - - // if a $base_path was passed set the protected property with that value, otherwise leave it empty - $this->container_dir_name = isset($base_path) ? $base_path . '/' : ''; - - // 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_files_only'])) - { - $this->opt['large_files_only'] = false; - } - - $this->output_name = $name; - if ($name || isset($opt['send_http_headers'])) - { - $this->need_headers = true; - } - - // turn off output buffering - while (ob_get_level() > 0) - { - ob_end_flush(); - } - - $this->opt['content_type'] = 'application/x-tar'; - } - - /** - * Explicitly adds a directory to the tar (necessary for empty directories) - * - * @param string $name Name (path) of the directory - * @param array $opt Additional options to set ("type" will be overridden) - * @return void - */ - function add_directory($name, $opt = array()) - { - // calculate header attributes - $opt['type'] = self::DIRTYPE; - - // send header - $this->init_file_stream_transfer($name, $size = 0, $opt); - - // complete the file stream - $this->complete_file_stream(); - } - - /** - * Initialize a file stream - * - * @param string $name file path or just name - * @param int $size size in bytes of the file - * @param array $opt array containing time / type (optional) - * @access public - */ - public function init_file_stream_transfer($name, $size, $opt = array()) - { - // try to detect the type if not provided - $type = self::REGTYPE; - if (isset($opt['type'])) - { - $type = $opt['type']; - } - elseif (substr($name, -1) == '/') - { - $type = self::DIRTYPE; - } - - $dirName = dirname($name); - $name = basename($name); - - $dirName = ($dirName == '.') ? '' : $dirName; - $name = ($type == self::DIRTYPE) ? $name . '/' : $name; - - // if we're using a container directory, prepend it to the filename - if ($this->use_container_dir) - { - // the container directory will end with a '/' so ensure the filename doesn't start with one - $dirName = $this->container_dir_name . preg_replace('/^\\/+/', '', $dirName); - } - - // handle long file names via PAX - if (strlen($name) > 99 || strlen($dirName) > 154) - { - $pax = $this->__pax_generate(array( - 'path' => $dirName . '/' . $name - )); - - $this->init_file_stream_transfer('', strlen($pax), array( - 'type' => self::XHDTYPE - )); - - $this->stream_file_part($pax); - $this->complete_file_stream(); - } - - // stash the file size for later use - $this->file_size = $size; - - // process optional arguments - $time = isset($opt['time']) ? $opt['time'] : time(); - - // build data descriptor - $fields = array( - array('a100', substr($name, 0, 100)), - array('a8', str_pad('777', 7, '0', STR_PAD_LEFT)), - array('a8', decoct(str_pad('0', 7, '0', STR_PAD_LEFT))), - array('a8', decoct(str_pad('0', 7, '0', STR_PAD_LEFT))), - array('a12', decoct(str_pad($size, 11, '0', STR_PAD_LEFT))), - array('a12', decoct(str_pad($time, 11, '0', STR_PAD_LEFT))), - array('a8', ''), - array('a1', $type), - array('a100', ''), - array('a6', 'ustar'), - array('a2', '00'), - array('a32', ''), - array('a32', ''), - array('a8', ''), - array('a8', ''), - array('a155', substr($dirName, 0, 155)), - array('a12', ''), - ); - - // pack fields and calculate "total" length - $header = $this->pack_fields($fields); - - // Compute header checksum - $checksum = str_pad(decoct($this->__computeUnsignedChecksum($header)),6,"0",STR_PAD_LEFT); - for($i=0; $i<6; $i++) - { - $header[(148 + $i)] = substr($checksum,$i,1); - } - $header[154] = chr(0); - $header[155] = chr(32); - - // print header - $this->send($header); - } - - /** - * Create a format string and argument list for pack(), then call pack() and return the result. - * - * @param array $fields key being the format string and value being the data to pack - * @return string binary packed data returned from pack() - * @access protected - */ - protected 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); - } - - - /** - * Stream the next part of the current file stream. - * - * @param mixed $data raw data to send - * @access public - */ - function stream_file_part( $data ) - { - // send data - $this->send($data); - - // flush the data to the output - flush(); - } - - /** - * If errors were encountered, add an error log file to the end of the archive - */ - public function add_error_log() - { - if (!empty($this->errors)) - { - $msg = $this->error_header_text; - foreach ($this->errors as $err) - { - $msg .= "\r\n\r\n" . $err; - } - - // stash current value so it can be reset later - $temp = $this->use_container_dir; - - // set to false to put the error log file in the root instead of the container directory, if we're using one - $this->use_container_dir = false; - - $this->add_file($this->error_log_filename, $msg); - - // reset to original value and dump the temp variable - $this->use_container_dir = $temp; - unset($temp); - } - } - - - /** - * Complete the current file stream - * - * @access private - */ - public function complete_file_stream() - { - // ensure we pad the last block so that it is 512 bytes - if (($mod = ($this->file_size % 512)) > 0) - $this->send( pack('a' . (512 - $mod) , '') ); - - // flush the data to the output - flush(); - } - - /** - * Finish an archive - * - * @access public - */ - public function finish() - { - // adds an error log file if we've been tracking errors - $this->add_error_log(); - - // tar requires the end of the file have two 512 byte null blocks - $this->send( pack('a1024', '') ); - - // flush the data to the output - flush(); - } - - /** - * Add file to the archive - * - * Parameters: - * - * @param string $name path of file in archive (including directory). - * @param string $data contents of file - * @param array $opt hash of file options (see above for list) - * @access public - */ - public function add_file($name, $data, $opt = array()) - { - // calculate header attributes - // send file header - $this->init_file_stream_transfer($name, strlen($data), $opt); - - // send data - $this->stream_file_part($data); - - // complete the file stream - $this->complete_file_stream(); - } - - /** - * Is this file larger than large_file_size? - * - * @param string $path path to file on disk - * @return bool true if large, false if small - * @access protected - */ - protected function is_large_file($path) - { - $st = stat($path); - return ($this->opt['large_file_size'] > 0) && ($st['size'] > $this->opt['large_file_size']); - } - - /** - * Add file by path - * - * @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 file options (see above for list) - * @access public - */ - public function add_file_from_path($name, $path, $opt = array()) - { - if ($this->opt['large_files_only'] || $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); - } - } - - /** - * Log an error to be output at the end of the archive - * - * @param string $message error text to display in log file - */ - public function push_error($message) - { - $this->errors[] = (string) $message; - } - - /** - * Set whether or not all elements in the archive will be placed within one container directory - * - * @param bool $bool true to use container directory, false to prevent using one. Defaults to false - */ - public function set_use_container_dir($bool = false) - { - $this->use_container_dir = (bool) $bool; - } - - /** - * Set the name filename for the error log file when it's added to the archive - * - * @param string $name the filename for the error log - */ - public function set_error_log_filename($name) - { - if (isset($name)) - { - $this->error_log_filename = (string) $name; - } - } - - /** - * Set the first line of text in the error log file - * - * @param string $msg the text to display on the first line of the error log file - */ - public function set_error_header_text($msg) - { - if (isset($msg)) - { - $this->error_header_text = (string) $msg; - } - } - - /** - * Send HTTP headers for this stream. - * - * @access private - */ - private function send_http_headers() - { - // grab options - $opt = $this->opt; - - // grab content type from options - if ( isset($opt['content_type']) ) - $content_type = $opt['content_type']; - else - $content_type = 'application/x-zip'; - - // grab content type encoding from options and append to the content type option - if ( isset($opt['content_type_encoding']) ) - $content_type .= '; charset=' . $opt['content_type_encoding']; - - // 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. - * - * @param string $data data to send - * @access protected - */ - protected function send( $data ) - { - if ($this->need_headers) - $this->send_http_headers(); - $this->need_headers = false; - - echo $data; - } - - /** - * Add a large file from the given path - * - * @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 file options (see above for list) - * @access protected - */ - protected function add_large_file($name, $path, $opt = array()) - { - // send file header - $this->init_file_stream_transfer($name, filesize($path), $opt); - - // open input file - $fh = fopen($path, 'rb'); - - // send file blocks - while ($data = fread($fh, $this->block_size)) - { - // send data - $this->stream_file_part($data); - } - - // close input file - fclose($fh); - - // complete the file stream - $this->complete_file_stream(); - } - - /** - * Generate unsigned checksum of header - * - * @param string $header - * @return string unsigned checksum - * @access private - */ - private function __computeUnsignedChecksum($header) - { - $unsigned_checksum = 0; - for($i=0; $i<512; $i++) - $unsigned_checksum += ord($header[$i]); - for($i=0; $i<8; $i++) - $unsigned_checksum -= ord($header[148 + $i]); - $unsigned_checksum += ord(" ") * 8; - - return $unsigned_checksum; - } - - /** - * Generate a PAX string - * - * @param array $fields key value mapping - * @return string PAX formated string - * @link http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&manpath=FreeBSD+8-current tar / PAX spec - * @access private - */ - private function __pax_generate($fields) - { - $lines = ''; - foreach ($fields as $name => $value) - { - // build the line and the size - $line = ' ' . $name . '=' . $value . "\n"; - $size = strlen(strlen($line)) + strlen($line); - - // add the line - $lines .= $size . $line; - } - - return $lines; - } -} diff --git a/test/testdata/lorem.txt b/test/testdata/lorem.txt new file mode 100644 index 0000000..ccd0041 --- /dev/null +++ b/test/testdata/lorem.txt @@ -0,0 +1,5 @@ +Duis a blandit nulla. Proin velit est, consectetur vel sapien non, elementum semper odio. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc id eros a tellus posuere laoreet eget at mauris. Nullam tincidunt bibendum metus, a porttitor lorem elementum in. Curabitur sit amet ultricies enim. Phasellus ullamcorper nibh vel odio luctus dignissim.Nam imperdiet leo eros. Sed et est ut justo accumsan egestas nec id leo. + +Quisque bibendum ultrices orci vitae rhoncus. Etiam nec quam posuere, malesuada lorem eget, convallis arcu. Integer lacinia consectetur dui ac dictum. Sed fermentum nulla vitae dui consequat, vitae lobortis neque euismod. Nullam commodo imperdiet fringilla. Nunc ultricies nisl neque, nec ullamcorper turpis feugiat id. Nulla ut rutrum felis, ut tincidunt diam. In interdum erat tristique, mattis velit nec, lacinia enim. Fusce vehicula vitae mi id maximus. Aliquam suscipit mi a felis dignissim ultricies. + +Suspendisse eu nisi eu ipsum vulputate ultricies. Donec feugiat varius eros, vitae semper lorem blandit ac. Suspendisse id gravida odio. Vestibulum eu leo quis velit ultrices molestie. Vestibulum eget eros quis erat auctor dapibus ut sit amet lectus. Donec eu efficitur ex. Fusce ultricies diam a ex venenatis fringilla. Cras interdum purus a accumsan lobortis. Praesent consectetur felis eget tellus tempus, eu posuere metus pulvinar. Morbi odio purus, finibus quis tincidunt sit amet, consectetur quis metus. \ No newline at end of file diff --git a/test/testdata/lorem2 b/test/testdata/lorem2 new file mode 100644 index 0000000..3b24fe6 --- /dev/null +++ b/test/testdata/lorem2 @@ -0,0 +1,3 @@ +Maecenas eu quam vel diam rutrum rhoncus vel placerat dui. In luctus, diam at malesuada tristique, turpis enim consequat sem, vel posuere dolor ligula et erat. Maecenas vulputate turpis lorem, accumsan tristique dolor tristique a. Morbi nec fringilla dolor. Sed placerat nulla sapien, nec vulputate turpis tincidunt id. Vivamus sit amet diam neque. Duis neque felis, mollis eget nisi a, placerat dapibus lectus. Sed vestibulum, ex id viverra convallis, ligula libero aliquam nibh, eu fermentum mi ipsum eu est. Donec ullamcorper maximus purus in faucibus. Donec a vehicula erat. Cras ac gravida turpis. Aliquam erat volutpat. Quisque egestas risus id mi ullamcorper vulputate. Maecenas in auctor ipsum, eget iaculis velit. + +Vivamus non tortor eget erat pulvinar ornare nec a elit. Ut tincidunt malesuada mi, eu efficitur diam posuere sed. Sed scelerisque a sem egestas semper. Donec pretium, ante quis luctus lobortis, dolor nibh suscipit nisl, sit amet lobortis ante nunc a elit. Nullam pulvinar lacus eu ultricies dapibus. Aenean bibendum, quam eu gravida lobortis, eros est feugiat neque, sit amet convallis diam purus nec massa. Quisque viverra, dui eget interdum scelerisque, velit mi fermentum turpis, maximus porta est libero id dolor. \ No newline at end of file diff --git a/test/testdata/more ipsum/lorem2.txt b/test/testdata/more ipsum/lorem2.txt new file mode 100644 index 0000000..f0e218f --- /dev/null +++ b/test/testdata/more ipsum/lorem2.txt @@ -0,0 +1,24 @@ +Як умру, то поховайте +Мене на могилі +Серед степу широкого +На Вкраїні милій, +Щоб лани широкополі, +І Дніпро, і кручі +Було видно, було чути, +Як реве ревучий. +Як понесе з України +У синєє море +Кров ворожу... отойді я +І лани і гори — +Все покину, і полину +До самого Бога +Молитися... а до того +Я не знаю Бога. +Поховайте та вставайте, +Кайдани порвіте +І вражою злою кров’ю +Волю окропіте. +І мене в сем’ї великій, +В сем’ї вольній, новій, +Не забудьте пом’янути +Незлим тихим словом. diff --git a/test/usage.php b/test/usage.php new file mode 100644 index 0000000..3c59da9 --- /dev/null +++ b/test/usage.php @@ -0,0 +1,72 @@ +sendHeaders('testdir.tar'); + +// Iterate though the directory tree +foreach($objects as $path => $object){ + + // Find a relative path inside the package + $internalPath = substr($path, strlen($basePath)); + + if (is_file($path)) { + // Path a file descriptor, relative path and file size + $fh = fopen($path, 'r'); + $tarStdoutStreamer->addFileFromStream($fh, $internalPath, filesize($path)); + fclose($fh); + } elseif(is_dir($path)) { + // Just a path + $tarStdoutStreamer->addEmptyDir($internalPath); + } +} + +// Send the end marker +$tarStdoutStreamer->finalize(); +// And that's it + + +// Another Usecase: Stream into file +$newTarFileDescriptor = fopen('test.tar', 'w+'); + +// Passing a descriptor to a brand new object +$tarFileStreamer = new TarStreamer(['outstream' => $newTarFileDescriptor]); + +// Iterate though the directory tree +foreach($objects as $path => $object){ + // Just for the sake of debugging + echo "adding $path\n"; + + // Find a relative path inside the package + $internalPath = substr($path, strlen($basePath)); + + if (is_file($path)) { + $fh = fopen($path, 'r'); + $tarFileStreamer->addFileFromStream($fh, $internalPath, filesize($path)); + fclose($fh); + } elseif(is_dir($path)) { + $tarFileStreamer->addEmptyDir($internalPath); + } +} + +// Send the end marker +$tarFileStreamer->finalize(); + +// Close output file +fclose($newTarFileDescriptor); From bcdf95111ddbd542b41b8381f254b0f94f129fbd Mon Sep 17 00:00:00 2001 From: Victor Dubiniuk Date: Thu, 24 Sep 2015 20:05:22 +0300 Subject: [PATCH 2/3] Escape header values --- src/TarStreamer.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/TarStreamer.php b/src/TarStreamer.php index f32731f..596550b 100644 --- a/src/TarStreamer.php +++ b/src/TarStreamer.php @@ -40,10 +40,11 @@ public function __construct($options = []){ * @param string $contentType Content mime type to be set (optional, default 'application/x-tar') */ public function sendHeaders($archiveName = 'archive.tar', $contentType = 'application/x-tar'){ + $encodedArchiveName = rawurlencode($archiveName); if (headers_sent($headerFile, $headerLine)){ die( "

Error: Unable to send file " . - "$archiveName. HTML Headers have already been sent from " . + "$encodedArchiveName. HTML Headers have already been sent from " . "$headerFile in line $headerLine" . "

" ); @@ -52,7 +53,7 @@ public function sendHeaders($archiveName = 'archive.tar', $contentType = 'applic if (!empty($buffer)){ die( "\n

Error: Unable to send file " . - "$archiveName.epub. Output buffer " . + "$encodedArchiveName. Output buffer " . "already contains text (typically warnings or errors).

" ); } @@ -69,8 +70,9 @@ public function sendHeaders($archiveName = 'archive.tar', $contentType = 'applic 'Content-Transfer-Encoding' => 'binary', ]; - foreach ($headers as $key => $val){ - header("$key: $val"); + foreach ($headers as $key => $value){ + $encodedValue = rawurlencode($value); + header("$key: $encodedValue"); } } From 0311a060f34bea3ff1f27ada4f34791885678c94 Mon Sep 17 00:00:00 2001 From: Victor Dubiniuk Date: Thu, 24 Sep 2015 21:15:54 +0300 Subject: [PATCH 3/3] Escape name only --- src/TarStreamer.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/TarStreamer.php b/src/TarStreamer.php index 596550b..c90c12e 100644 --- a/src/TarStreamer.php +++ b/src/TarStreamer.php @@ -65,14 +65,13 @@ public function sendHeaders($archiveName = 'archive.tar', $contentType = 'applic 'Accept-Ranges' => 'bytes', 'Connection' => 'Keep-Alive', 'Content-Type' => $contentType, - 'Content-Disposition' => 'attachment; filename="' . $archiveName . '";', + 'Content-Disposition' => 'attachment; filename="' . $encodedArchiveName . '";', 'Cache-Control' => 'public, must-revalidate', 'Content-Transfer-Encoding' => 'binary', ]; foreach ($headers as $key => $value){ - $encodedValue = rawurlencode($value); - header("$key: $encodedValue"); + header("$key: $value"); } }