File "stream_full.php"

Full Path: /home/pylifeesyu/www/wp-content/plugins/wponlinebackup/include/stream_full.php
File size: 31 KB
MIME-type: text/x-php
Charset: utf-8

<?php

/*
WPOnlineBackup_Stream_Full - Archives data to a zip file archive for full backup
Can compress data, or just store it
Has functions to add files from path, and also to stream data to it
A writer is given to write the archive to -
	which means we can place an archive anywhere - even output directly to the browser
*/

/*
Contains derivations from ZipStream by Paul Duncan with minor improvements and additions

In general this allows us to write a ZIP file within any stream that we can fwrite to, and it means we can stream data
to the zip file as well, as opposed to having to write to a temporary file and then compress and then encrypt

Issue with ZipStream however is that for large file compression it loops and gzdeflates in blocks. This however does not
work as they are separate streams, and to join two streams together is very difficult due to incorrect alignment of bits.
For simplicity and 

----------------------------------------------------------------------------------------------------
Licence for ZipStream which we derived from
----------------------------------------------------------------------------------------------------

##########################################################################
# 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.                                        #
##########################################################################

*/

class WPOnlineBackup_Stream_Full
{
	/*private*/ var $WPOnlineBackup;

	/*private*/ var $config;

	/*private*/ var $status;

	/*private*/ var $compressor;
	/*private*/ var $compressor_name;
	/*private*/ var $writer;
	/*private*/ var $writer_name;
	/*private*/ var $disk;

	/*private*/ var $file_count;
	/*private*/ var $cdr_ofs;
	/*private*/ var $last_cdr;
	/*private*/ var $cdr;
	/*private*/ var $ofs;

	/*private*/ var $rolling_name;
	/*private*/ var $rolling_writing;

	/*private*/ var $encrypted;
	/*private*/ var $compressed;

	/*private*/ var $files;

	/*private*/ var $code;

	/*private*/ var $datasize_limit;
	/*private*/ var $filesize_limit;

	/*private*/ var $bin_database;
	/*private*/ var $bin_filesystem;

	/*public*/ function WPOnlineBackup_Stream_Full( & $WPOnlineBackup )
	{
		// Store the main object
		$this->WPOnlineBackup = & $WPOnlineBackup;

		// Check we have CRC32 stuff available
		require_once WPONLINEBACKUP_PATH . '/include/functions.php';

		// Disk object
		require_once WPONLINEBACKUP_PATH . '/include/disk.php';
	}

	/*public*/ function Save()
	{
		// Return the state
		$state = array(
			'status'		=> $this->status,
			'config'		=> $this->config,
			'encrypted'		=> $this->encrypted,
			'compressed'		=> $this->compressed,
			'files'			=> $this->files,
			'disk'			=> $this->disk->Save(),
			'datasize_limit'	=> $this->datasize_limit,
			'filesize_limit'	=> $this->filesize_limit,
			'bin_database'		=> $this->bin_database,
			'bin_filesystem'	=> $this->bin_filesystem,
		);

		if ( $this->status == 0 ) {

			$state['compressor']		= $this->compressor->Save();
			$state['compressor_name']	= $this->compressor_name;
			$state['writer']		= $this->writer !== false ? $this->writer->Save() : false;

			if ( $this->writer !== false ) {
				$state['writer_name']	= $this->writer_name;
			}

			$state['file_count']		= $this->file_count;
			$state['cdr_ofs']		= $this->cdr_ofs;
			$state['ofs']			= $this->ofs;
			$state['cdr']			= $this->cdr === false ? false : $this->cdr->Save();
			$state['last_cdr']		= $this->last_cdr;

			$state['rolling_name']		= $this->rolling_name;
			$state['rolling_writing']	= $this->rolling_writing;

		}

		return $state;
	}

	/*public*/ function Load( $state, $rotation )
	{
		// Store the config
		$this->config = $state['config'];

		$this->status = $state['status'];

		$this->encrypted = $state['encrypted'];
		$this->compressed = $state['compressed'];

		$this->files = $state['files'];

		$this->datasize_limit = $state['datasize_limit'];
		$this->filesize_limit = $state['filesize_limit'];

		$this->bin_database = $state['bin_database'];
		$this->bin_filesystem = $state['bin_filesystem'];

		// Reopen the file
		$this->disk = new WPOnlineBackup_Disk( $this->WPOnlineBackup );

		if ( ( $ret = $this->disk->Load( $state['disk'], $rotation ) ) !== true ) return $ret;

		if ( $this->status == 0 ) {

			if ( $state['writer'] === false ) {

				$this->writer = false;

			} else {

				$this->writer_name = $state['writer_name'];
				$this->compressor_name = $state['compressor_name'];

				// Load the writer
				require_once WPONLINEBACKUP_PATH . '/include/' . strtolower( $this->writer_name ) . '.php';
				$name = 'WPOnlineBackup_' . $this->writer_name;

				$this->writer = new $name( $this->WPOnlineBackup );

				if ( ( $ret = $this->writer->Load( $state['writer'], $this->disk, $rotation ) ) !== true ) {
					$this->disk->CleanUp();
					return $ret;
				}

			}

			// Load the compressor
			require_once WPONLINEBACKUP_PATH . '/include/' . strtolower( $this->compressor_name ) . '.php';
			$name = 'WPOnlineBackup_' . $this->compressor_name;

			$this->compressor = new $name( $this->WPOnlineBackup );

			if ( ( $ret = $this->compressor->Load( $state['compressor'], $this->writer, $rotation ) ) !== true ) {
				$this->writer->CleanUp();
				$this->disk->CleanUp();
				return $ret;
			}

			$this->code = $this->compressor->Get_ZIP_Code();

			// Repopulate the saved state
			$this->file_count = $state['file_count'];
			$this->cdr_ofs = $state['cdr_ofs'];
			$this->ofs = $state['ofs'];
			if ( $state['cdr'] !== false ) {
				if ( ( $ret = $this->writer->Branch( $this->cdr ) ) !== true || ( $ret = $this->cdr->Load( $state['cdr'], $rotation ) ) !== true ) {
					$this->compressor->CleanUp();
					$this->writer->CleanUp();
					$this->disk->CleanUp();
					return $ret;
				}
			} else {
				$this->cdr = false;
			}
			$this->last_cdr = $state['last_cdr'];

			$this->rolling_name = $state['rolling_name'];
			$this->rolling_writing = $state['rolling_writing'];

		}

		return true;
	}

	/*public*/ function Open( $config, $title, $description )
	{
		// ASSERTION - The file is closed
		$this->config = $config;

		$this->config['designated_ext'] = array();

		$this->config['backwards_compat'] = $this->WPOnlineBackup->Get_Setting( 'zip_backwards_compat', false );

		// Prepare the writer
		switch ( $this->config[ 'encryption' ] ) {

			// We may implement ZIP AE-1 and AE-2 encryption in the future - but this doesn't encrypt metadata (filenames etc.)
			case 'DES':
			case 'AES128':
			case 'AES192':
			case 'AES256':
				require_once WPONLINEBACKUP_PATH . '/include/writer_encrypt.php';

				$this->writer_name = 'Writer_Encrypt';
				$this->writer = new WPOnlineBackup_Writer_Encrypt( $this->WPOnlineBackup );

				$this->config['designated_ext'][] = '.enc';

				$this->encrypted = true;
				break;

			// No encryption
			default:
				require_once WPONLINEBACKUP_PATH . '/include/writer_direct.php';

				$this->writer_name = 'Writer_Direct';
				$this->writer = new WPOnlineBackup_Writer_Direct( $this->WPOnlineBackup );

				$this->encrypted = false;
				break;

		}

		// Prepare the compressor
		switch ( $this->config['compression'] ) {

			// We only know DEFLATE at the moment
			case 'DEFLATE':
				require_once WPONLINEBACKUP_PATH . '/include/compressor_deflate.php';

				$this->compressor_name = 'Compressor_Deflate';
				$this->compressor = new WPOnlineBackup_Compressor_Deflate( $this->WPOnlineBackup );

				$this->compressed = true;
				break;

			// No compression
			default:
				require_once WPONLINEBACKUP_PATH . '/include/compressor_none.php';

				$this->compressor_name = 'Compressor_None';
				$this->compressor = new WPOnlineBackup_Compressor_None( $this->WPOnlineBackup );

				$this->compressed = false;
				break;

		}

		$this->code = $this->compressor->Get_ZIP_Code();

		$this->config['designated_ext'][] = '.zip';

		$this->config['designated_ext'] = implode( array_reverse( $this->config['designated_ext'] ) );

		$this->disk = new WPOnlineBackup_Disk( $this->WPOnlineBackup );

		$this->disk->Initialise( $this->config['designated_path'] );

		// Open a disk but request no rejection header on the resulting file - we will write our own and embed it in ZIP format
		if ( ( $ret = $this->disk->Open( 'backup' . $this->config['designated_ext'], false ) ) !== true )
			return $ret;

		// Pass the file handle to the writer, and open the writer - tell the writer that we will use it to write a rejection header
		// This means writers such as the encryption writer will write it's own rejection header since it knows it will mangle the one we give it
		if ( ( $ret = $this->writer->Open( $this->disk, $this->config['encryption'], $this->config['encryption_key'], 0, true ) ) !== true ) {
			$this->disk->CleanUp();
			return $ret;
		}

		// Pass the writer to the compressor, and open it
		if ( ( $ret = $this->compressor->Open( $this->writer ) ) !== true ) {
			$this->writer->CleanUp();
			$this->disk->CleanUp();
			return $ret;
		}

		$this->status = 0;

		$this->files = 0;

		// Initialize parameters we need for ZIP archiving
		$this->file_count = 0;
		$this->cdr_ofs = 0;
		$this->ofs = 0;
		$this->cdr = false;
		$this->last_cdr = false;

		// Default to no limits
		$this->datasize_limit = null;
		$this->filesize_limit = null;

		// No databases
		$this->bin_database = false;
		$this->bin_filesystem = false;

		// Add our Readme.txt with it's hidden extra field containing our rejection header
		if ( true !== ( $ret = $this->Add_Readme() ) )
			return $ret;

		return true;
	}

	/*public*/ function Flush()
	{
		// ASSERTION - The file is open
		// Write the CDR
		if ( ( $ret = $this->Add_CDR() ) !== true ) return $ret;

		return true;
	}

	/*public*/ function Close()
	{
		// Close the writer
		if ( !is_array( $ret = $this->writer->Close() ) ) return $ret;

		$this->writer = false;

		// Close the disk
		if ( ( $ret = $this->disk->Close() ) !== true ) return $ret;

		if ( $this->cdr !== false ) {
			$this->cdr->Delete( true );
			$this->cdr = false;
		}

		$this->status = 1;

		return true;
	}

	/*public*/ function CleanUp( $wipe = true )
	{
		$this->compressor->CleanUp( $wipe );
		if ( $this->writer !== false )
			$this->writer->CleanUp();

		$this->disk->CleanUp( $wipe );
		if ( $this->cdr !== false )
			$this->cdr->CleanUp( $wipe );
	}

	/*public*/ function Start_Reconstruct()
	{
		// ASSERTION - Status is 1 - Close() has been called
		return $this->disk->Start_Reconstruct();
	}

	/*public*/ function Do_Reconstruct()
	{
		// ASSERTION - Status is 1 - Close() has been called
		// ASSERTION - Start_Reconstruct has been called
		return $this->disk->Do_Reconstruct();
	}

	/*public*/ function End_Reconstruct()
	{
		// ASSERTION - Status is 1 - Close() has been called
		// ASSERTION - Start_Reconstruct has been called and Do_Reconstruct has returned success
		return $this->disk->End_Reconstruct();
	}

	/*public*/ function Is_Encrypted()
	{
		return $this->encrypted;
	}

	/*public*/ function Is_Compressed()
	{
		return $this->compressed;
	}

	/*public*/ function Files()
	{
		return $this->files;
	}

	/*public*/ function Impose_DataSize_Limit( $size, $message, $ret )
	{
		$this->datasize_limit = array( $size, $message, $ret );
	}

	/*public*/ function Impose_FileSize_Limit( $size, $message, $ret )
	{
		$this->filesize_limit = array( $size, $message, $ret );
	}

	/*public*/ function Add_Folder_From_Path( $bin, $folder, $path, & $success, $status )
	{
		$success = true;

		// No mod time specified? Find it now
		if ( !isset( $status['mod_time'] ) ) {

			if ( false === ( $status['mod_time'] = @filemtime( $path ) ) ) {

				$success = OBFW_Tidy_Exception();
				$success = OBFW_FOpen_Exception( $path, $size );

				// We need to add the folder even if we failed to get it's status - just use current time as mod_time
				$status['mod_time'] = time();

			}

		}

		if ( true !== ( $ret = $this->Add_File_Header( $bin, $folder . '/', $status, 0, false, 0, 0, 0 ) ) )
			return $ret;

		return true;
	}

	// Code derived from:
	// ZipStream - Streamed, dynamically generated zip archives.
	// by Paul Duncan <pabs@pablotron.org>
	/*public*/ function Add_File_From_Path( $bin, $file, $path, & $size, $status )
	{
		// ASSERTION - The file is open and we are not in the middle of a rolling deflation with Open_File

		// Grab the filesize so we can work out how to process it
		if ( is_null( $size ) && false === ( $size = @filesize( $path ) ) ) {

			$size = OBFW_Tidy_Exception();
			$size = OBFW_FOpen_Exception( $path, $size );
			return true;

		}

		// No mod time specified? Find it now
		if ( !isset( $status['mod_time'] ) ) {

			if ( false === ( $status['mod_time'] = @filemtime( $path ) ) ) {

				$size = OBFW_Tidy_Exception();
				$size = OBFW_FOpen_Exception( $path, $size );
				return true;

			}

		}

		// Compress in one go treating as a string, or piece by piece?
		if ( $size > $this->WPOnlineBackup->Get_Setting( 'max_block_size' ) ) {

			if ( true !== ( $ret = $this->Add_Large_File( $bin, $file, $path, $size, $status ) ) )
				return $ret;

			return true;

		} else {

			if ( false === ( $data = @file_get_contents( $path ) ) ) {

				$size = OBFW_Tidy_Exception();
				return true;

			}

			$size = strlen( $data );

			return $this->Add_File_From_String( $bin, $file, $size, $data, $status );

		}
	}

	/*public*/ function Add_File_From_String( $bin, $file, & $size, $data, $status )
	{
		// Calculate header attributes and compress data
		if ( !is_array( $result = $this->compressor->Atomic( $data ) ) )
			return $result;

		$size = array(
			'file_size'	=> $result['size'],
			'stored_size'	=> $result['zlen'],
		);

		// Write file header
		if ( true !== ( $ret = $this->Add_File_Header( $bin, $file, $status, $this->code, false, $result['crc'], $result['zlen'], $result['size'] ) ) )
			return $ret;

		// Write data
		if ( true !== ( $ret = $this->writer->Write( $result['data'] ) ) )
			return $ret;

		$this->files++;

		return true;
	}

	/*public*/ function Start_Stream( $bin, $name, & $size, $status )
	{
		// ASSERTION - The file is open and we are not in the middle of a stream started with Start_Stream
		// Store file name
		$this->rolling_name = array( $bin, $name, & $size, $status );
		$this->rolling_writing = false;

		// Start the compressor
		if ( true !== ( $ret = $this->compressor->Start_Stream() ) )
			return $ret;

		return true;
	}

	/*public*/ function Write_Stream( $data, $len = false )
	{
		// ASSERTION - The file is open and we are currently in the middle of a stream started with Start_Stream
		// If len is false, use strlen()
		if ( $len === false )
			$len = strlen( $data );

		if ( !$this->rolling_writing ) {

			// Call compressor but tell not to write
			if ( !is_bool( $ret = $this->compressor->Write_Stream( $data, $len, true ) ) )
				return $ret;

			if ( $ret === false ) {

				// Compressor wants to write, write file header now but use data descriptor instead of storing crc/zlen/size in the local header
				if ( true !== ( $ret = $this->Add_File_Header( $this->rolling_name[0], $this->rolling_name[1], $this->rolling_name[3], $this->code, true ) ) )
					return $ret;

				$this->rolling_writing = true;

			} else {

				// Doesn't want to write, just return
				return true;

			}

		}

		// Call compressor and allow it to write normally
		if ( true !== ( $ret = $this->compressor->Write_Stream( $data, $len ) ) )
			return $ret;

		return true;
	}

	/*public*/ function End_Stream()
	{
		// ASSERTION - The file is open and we are currently in the middle of a stream started with Start_Stream()
		// If we never started writing, the following will perform all in buffer
		if ( true !== ( $ret = $this->compressor->End_Stream() ) )
			return $ret;

		return true;
	}

	/*public*/ function Commit_Stream()
	{
		if ( !$this->rolling_writing ) {

			// Not been writing so add the file header now - when we commit it will add the data
			if ( true !== ( $ret = $this->Add_File_Header( $this->rolling_name[0], $this->rolling_name[1], $this->rolling_name[3], $this->code, false, $result['crc'], $result['zlen'], $result['size'] ) ) )
				return $ret;

		}

		// If we never started writing, the following will perform all in buffer and write it out
		if ( !is_array( $result = $this->compressor->Commit_Stream() ) )
			return $result;

		$this->rolling_name[2] = array(
			'file_size'	=> $result['size'],
			'stored_size'	=> $result['zlen'],
		);

		if ( $this->rolling_writing ) {

			// Been writing so update the file header and solidify
			if ( true !== ( $ret = $this->Update_File_Header( $result['crc'], $result['zlen'], $result['size'] ) ) )
				return $ret;

			if ( true !== ( $ret = $this->writer->Solidify() ) )
				return $ret;

		}

		return true;
	}

	/*public*/ function CleanUp_Stream()
	{
		// Cleanup the compressor
		$this->compressor->CleanUp_Stream();

		// If we've been writing, commit the writer
		if ( $this->rolling_writing && true !== ( $ret = $this->writer->Commit() ) )
			return $ret;

		$this->rolling_name = false;

		$this->files++;

		return true;
	}

	/*private*/ function Add_Large_File( $bin, $file, $path, & $size, $status )
	{
		// Open file
		if ( !( $fh = @fopen( $path, 'rb' ) ) ) {
			$size = OBFW_Tidy_Exception();
			return true;
		}

		// Force compressor and writer to register temporary files
		$this->compressor->Register_Temps( true );
		$this->writer->Register_Temps( true );

		$size = 0;

		// Start the stream
		if ( ( $ret = $this->Start_Stream( $bin, $file, $size, $status ) ) !== true ) {
			@fclose( $fh );
			return $ret;
		}

		$writing = false;

		// Read each block, update size and write data
		while ( true ) {

			// Read a block
			if ( ( $data = @fread( $fh, $this->WPOnlineBackup->Get_Setting( 'max_block_size' ) ) ) === false ) {

				$size = OBFW_Tidy_Exception();

				@fclose( $fh );

				// Restore compressor and writer
				$this->writer->Register_Temps( false );
				$this->compressor->Register_Temps( false );

				return true;

			}

			// Get length of data read
			$len = strlen( $data );

			// Update size
			$size += $len;

			// Write to the stream
			if ( ( $ret = $this->Write_Stream( $data, $len ) ) !== true ) {
				@fclose( $fh );
				return $ret;
			}

			// End of file? End loop
			if ( @feof( $fh ) ) break;

		}

		// Close the file
		@fclose( $fh );

		// End the stream
		if ( ( $ret = $this->End_Stream() ) !== true ) return $ret;

		// Commit the stream
		if ( ( $ret = $this->Commit_Stream() ) !== true ) return $ret;

		// Clean up the stream
		if ( ( $ret = $this->CleanUp_Stream() ) !== true ) return $ret;

		// Restore compressor and writer
		$this->writer->Register_Temps( false );
		$this->compressor->Register_Temps( false );

		return true;
	}

	/*private*/ function Add_Readme()
	{
		// Set the filename and calculate name length
		$file = _x( 'Readme.txt', 'Readme.txt filename in downloadable ZIP file', 'wponlinebackup' );
		$nlen = strlen( $file );

		// Create dos timestamp
		$status = array(
			'mod_time'	=> time(),
		);

// translators: date() function string when displaying creation time in downloadable backup file
		$date_format = __( 'd-m-Y H.i.s T' , 'wponlinebackup' );

		$data =
			__( 'Online Backup for WordPress', 'wponlinebackup' ) . WPONLINEBACKUP_EOL .
			sprintf( __( 'Version %s', 'wponlinebackup' ), WPONLINEBACKUP_VERSION ) . WPONLINEBACKUP_EOL .
			__( 'http://www.backup-technology.com/free-wordpress-backup/', 'wponlinebackup' ) . WPONLINEBACKUP_EOL .
			WPONLINEBACKUP_EOL .
			sprintf( __( 'Blog: %s', 'wponlinebackup' ), get_bloginfo( 'wpurl' ) ) . WPONLINEBACKUP_EOL .
			sprintf( __( 'Creation Time: %s', 'wponlinebackup' ), date( $date_format, $status['mod_time'] ) ) . WPONLINEBACKUP_EOL;

		// Calculate header attributes and compress data
		if ( !is_array( $result = $this->compressor->Atomic( $data ) ) )
			return $result;

		// Add the file header
		if ( true !== ( $ret = $this->Add_File_Header( false, $file, $status, $this->code, false, $result['crc'], $result['zlen'], $result['size'], array( 'rjct' ) ) ) )
			return $ret;

		// Write the file data
		if ( true !== ( $ret = $this->writer->Write( $result['data'] ) ) )
			return $ret;

		return true;
	}

	/*private*/ function Add_File_Header( $bin, $file, $status, $meth, $volatile = true, $crc = 0, $zlen = 0, $len = 0, $extra = array() )
	{
		// Strip leading slashes from file name (fixes bug in windows archive viewer)
		$file = preg_replace( '/^[\\/\\\\]+/', '', $file );

		if ( $bin === false ) {

			$bin = '';

		} else if ( $bin === WPONLINEBACKUP_BIN_DATABASE ) {

			$bin = _x( 'Database', 'Full backup folder name in downloadable ZIP file' , 'wponlinebackup' ) . '/';

			if ( !$this->bin_database ) {

				$binstatus = array(
					'mod_time'	=> time(),
				);

				// Add the Database folder
				if ( true !== ( $this->Add_File_Header( $bin, '', $binstatus, 0x00, false ) ) )
					return $ret;

				$this->bin_database = true;

			}

		} else if ( $bin === WPONLINEBACKUP_BIN_FILESYSTEM ) {

			$bin = _x( 'FileSystem', 'Full backup folder name in downloadable ZIP file' , 'wponlinebackup' ) . '/';

			if ( !$this->bin_filesystem ) {

				$binstatus = array(
					'mod_time'	=> time(),
				);

				// Add the Database folder
				if ( true !== ( $this->Add_File_Header( $bin, '', $binstatus, 0x00, false ) ) )
					return $ret;

				$this->bin_filesystem = true;

			}

		}

		$file = $bin . $file;

		// Create dos timestamp
		$dts = WPOnlineBackup_Functions::DOS_Time( $status['mod_time'] );

		// Why don't we use general purpose bit flag bit 3 to make a streamable ZIP file?
		// So that we can write CRC/ZLEN/LEN after the file data and not need to seek back and rewrite?
		// Because Mac OS X Archive Utility doesn't work with them :-(
		// Run this and try it: zip -X - smallfile.txt - | cat > test.zip
		// Give it lots of data
		// Then CTRL+D to end STIN and try to extract the test.zip - you'll get an error
		// Using Windows or unzip command line will work
		$genpurp = 0x0000;

		// Calculate extra data
		$extraloc = '';
		$extracdr = '';

		if ( in_array( 'rjct', $extra ) ) {

			// The rejection header we will hide inside an extra field
			$rjct = $this->disk->Get_Header();

			// The extra field
			$fields = array(
				array( 'v',	0x4400		),	// We made this up... Shouldn't cause an issue
				array( 'v',	strlen( $rjct ) ),	// Size of the rejection header we're embedding in the ZIP file
			);

			// Pack extra field and rejection header
			$extraloc .= WPOnlineBackup_Functions::Pack_Fields( $fields ) . $rjct;

		}

		if ( $this->config['backwards_compat'] ) {

			// Store the UTF-8 filename in an extra field
			$fields = array(
				array( 'v',	0x7075			),	// Info-ZIP Unicode Path Extra Field
				array( 'v',	5 + strlen( $file )	),	// Size of the block
				array( 'C',	1			),	// Version
				array( 'V',	crc32( $file )		),	// Unicode name CRC32 checksum
			);

			$fields = WPOnlineBackup_Functions::Pack_Fields( $fields ) . $file;
			$extraloc .= $fields;
			$extracdr .= $fields;

			// ASCII filename
			// We will assume valid UTF-8 was passed in, which currently it should be
			// It would be a bug if we didn't - UTF8_To_ASCII only works properly with valid UTF-8 since it only modifyes UTF-8 chars
			$fileloc = WPOnlineBackup_Functions::UTF8_To_ASCII( $file );

		} else {

			// Just store UTF-8 in the local header, but user gen purpose bit flag 11
			$fileloc = $file;

			$genpurp |= 0x0800;

		}

		$elenloc = strlen( $extraloc );
		$elencdr = strlen( $extracdr );

		$nlen = strlen( $fileloc );

		// Build file header
		$fields = array(	// (from V.A of APPNOTE.TXT)
			array( 'V',	0x04034b50	),	// local file header signature
			array( 'C',	20		),	// version needed to extract (lower byte, appnote version)
			array( 'C',	0		),	// version needed to extract (upper byte, compatibility code)
			array( 'v',	$genpurp	),	// general purpose bit flag
			array( 'v',	$meth		),	// compression method (deflate or store)
			array( 'V',	$dts		),	// dos timestamp
			// 14 bytes in - this is where Update_File_Header would update
			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',	$elenloc	),	// extra data len
		);

		// Pack fields and calculate "total" length
		$fields = WPOnlineBackup_Functions::Pack_Fields( $fields ) . $fileloc . $extraloc;
		$record_len = strlen( $fields ) + $zlen;

		// Write the header and filename
		if ( $volatile ) {

			// Write volatile so we can seek back and modify the CRC/ZLEN/LEN
			if ( true !== ( $ret = $this->writer->Start_Volatile() ) )
				return $ret;

			if ( !is_int( $pos = $this->writer->Volatile_Write( $fields ) ) )
				return $pos;

		} else {

			// No need for pos, we don't need to seek back
			$pos = false;

			if ( true !== ( $ret = $this->writer->Write( $fields ) ) )
				return $ret;

		}

		// Add to central directory record and increment offset
		$record = compact( 'pos', 'fileloc', 'nlen', 'genpurp', 'meth', 'dts', 'crc', 'zlen', 'len', 'elencdr', 'extracdr' );

		// Store offset of this entry
		$record['ofs'] = $this->ofs;

		$this->file_count++;

		// If we have a CDR already stored, write it to the CDR buffer, creating one if necessary
		if ( $this->last_cdr !== false ) {

			if ( $this->cdr === false ) {

				if ( ( $ret = $this->writer->Branch( $this->cdr ) ) !== true ) return $ret;

				if ( ( $ret = $this->cdr->Open( 'cdrbuffer' ) ) !== true ) return $ret;

			}

			if ( ( $ret = $this->Add_CDR_File( $this->cdr, $this->last_cdr ) ) !== true ) return $ret;

		}

		// Store this CDR - if we only end up with a single CDR, we don't bother creating the CDR buffer then
		$this->last_cdr = $record;

		// Increase offset to be used for next entry
		$this->ofs += $record_len;
		return true;
	}

	/*private*/ function Update_File_Header( $crc, $zlen, $len )
	{
		// Fetch the actual offset from the CDR
		$pos = $this->last_cdr['pos'];

		// Build new values
		$fields = array(	// (from V.A of APPNOTE.TXT)
			array( 'V',	$crc		),	// crc32 of data
			array( 'V',	$zlen		),	// compressed data length
			array( 'V',	$len		),	// uncompressed data length
		);

		// Fill in the blanks
		if ( true !== ( $ret = $this->writer->Volatile_Rewrite( $pos + 14, WPOnlineBackup_Functions::Pack_Fields( $fields ) ) ) )
			return $ret;

		// Update the internal offset pointer
		$this->ofs -= $this->last_cdr['zlen'];
		$this->ofs += $zlen;

		// Update the CDR
		$this->last_cdr['crc'] = $crc;
		$this->last_cdr['zlen'] = $zlen;
		$this->last_cdr['len'] = $len;

		return true;
	}

	/*private*/ function Add_CDR_File( & $file, $args )
	{
		// External attributes
		$attr = 0;

		// If file ends with forward slash, we're a directory
		if ( substr( $args['fileloc'], -1 ) == '/' ) {
			$attr |= 0040755 << 16; // Use 0755 permissions for directories
			$attr |= 0x10; // MS-DOS directory attribute bit
		} else {
			$attr |= 0100644 << 16; // Use 0644 permissions for files
		}

		$fields = array(	// (from V,F of APPNOTE.TXT)
			array( 'V',	0x02014b50		),	// central file header signature
			array( 'C',	20			),	// version made by (lower byte, version)
			array( 'C',	3			),	// version made by (upper byte, OS)
			array( 'C',	10			),	// version needed to extract (lower byte, version)
			array( 'C',	0			),	// version needed to extract (upper byte, OS)
			array( 'v',	$args['genpurp']	),	// general purpose bit flag
			array( 'v',	$args['meth']		),	// compression method (deflate or store)
			array( 'V',	$args['dts']		),	// dos timestamp
			array( 'V',	$args['crc']		),	// crc32 of data
			array( 'V',	$args['zlen']		),	// compressed data length
			array( 'V',	$args['len']		),	// uncompressed data length
			array( 'v',	$args['nlen']		),	// filename length
			array( 'v',	$args['elencdr']	),	// extra data len
			array( 'v',	0			),	// file comment length
			array( 'v',	0			),	// disk number start
			array( 'v',	1			),	// internal file attributes - 1 is binary
			array( 'V',	$attr			),	// external file attributes
			array( 'V',	$args['ofs']		),	// relative offset of local header
		);

		$fields = WPOnlineBackup_Functions::Pack_Fields( $fields ) . $args['fileloc'] . $args['extracdr'];

		// Write the fields
		if ( true !== ( $ret = $file->Write( $fields ) ) )
			return $ret;

		// Increment cdr offset
		$this->cdr_ofs += strlen( $fields );

		return true;
	}

	/*private*/ function Add_CDR_EOF()
	{
		$cdr_len = $this->cdr_ofs;
		$cdr_ofs = $this->ofs;

		// Prepare comment
		$comment = 'Backup file generated by Online Backup for WordPress ' . WPONLINEBACKUP_VERSION . PHP_EOL;

		$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',	$this->file_count	),	// number of entries in the cdr on this disk
			array( 'v',	$this->file_count	),	// 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
		);

		// Write the fields
		if ( ( $ret = $this->writer->Write( WPOnlineBackup_Functions::Pack_Fields( $fields ) . $comment ) ) !== true ) return $ret;

		return true;
	}

	/*private*/ function Add_CDR()
	{
		if ( $this->cdr !== false ) {

			// Close CDR file
			$this->cdr->Close();

			do {

				// Flush the CDR file to the end of the disk, removing CDR data as we go along
				if ( !is_bool( $flush = $this->cdr->Flush( $buffer, $this->WPOnlineBackup->Get_Setting( 'max_block_size' ) ) ) ) return $flush;

				if ( ( $ret = $this->writer->Write( $buffer ) ) !== true ) return $ret;

				if ( $flush === false ) break;

			} while ( true );

		}

		// Add the final CDR record that was not commited to the CDR file
		if ( $this->last_cdr !== false ) {
			if ( ( $ret = $this->Add_CDR_File( $this->writer, $this->last_cdr ) ) !== true ) return $ret;
		}

		// Commit the EOF CDR record
		if ( ( $ret = $this->Add_CDR_EOF() ) !== true ) return $ret;

		return true;
	}
}

?>