* ---- ---- ---- ---- * |0 1 | 5 6| 9 0| 3 4| * |2 4 | 7 8| 1 2| 5 6| * ---- ---- ---- ---- * 0 1 2 3 * * ^ ^ * | \ LastBlock * * Current * * * * @package Archive * @version 1.3.1 * @access private */ class ezcArchiveBlockFile extends ezcArchiveFile { /** * The block size. * * @var int */ private $blockSize; /** * The current number of the block. * * The first block starts with zero. * * @var int */ private $blockNumber = -1; /** * The current block data. * * @var string */ private $blockData; /** * Holds the last block number. * * @var int */ private $lastBlock = -1; /** * Sets the property $name to $value. * * Because there are no properties available, this method will always * throw an {@link ezcBasePropertyNotFoundException}. * * @throws ezcBasePropertyNotFoundException if the property does not exist. * @param string $name * @param mixed $value * @return void * @ignore */ public function __set( $name, $value ) { throw new ezcBasePropertyNotFoundException( $name ); } /** * Returns the property $name. * * Available read-only property: blockSize * * @throws ezcBasePropertyNotFoundException if the property does not exist. * @param string $name * @return mixed * @ignore */ public function __get( $name ) { switch ( $name ) { case "blockSize": return $this->blockSize; } throw new ezcBasePropertyNotFoundException( $name ); } /** * Constructs a new ezcArchiveBlockFile. * * The given file name is tried to be opened in read / write mode. If that fails, the file will be opened * in read-only mode. * * If the bool $createIfNotExist is set to true, it will create the file if it doesn't exist. * * @throws ezcBaseFileNotFoundException if the file cannot be found. * @throws ezcBaseFilePermissionException if the file permissions are wrong. * * @param string $fileName * @param bool $createIfNotExist * @param int $blockSize * @param bool $readOnly */ public function __construct( $fileName, $createIfNotExist = false, $blockSize = 512, $readOnly = false ) { $this->blockSize = $blockSize; $this->openFile( $fileName, $createIfNotExist, $readOnly ); } /** * The destructor will close all open files. */ public function __destruct() { if ( $this->fp ) { fclose( $this->fp ); } } /** * Rewind the current file. * * @return void */ public function rewind() { $this->blockNumber = -1; parent::rewind(); } /** * Return the data from the current block if the block is valid. * * If the block is not valid, the value false is returned. * * @return string */ public function current() { return ( $this->isValid ? $this->blockData : false ); } /** * Iterate to the next block. * * Returns the data of the next block if it exists; otherwise returns false. * * @return string */ public function next() { if ( $this->isValid ) { // XXX move the calc to readmode? // $this->switchReadMode( ( $this->blockNumber + 1 ) * $this->blockSize ); // Read one block. $this->blockData = fread( $this->fp, $this->blockSize ); if ( strlen( $this->blockData ) < $this->blockSize ) { if ( $this->lastBlock != -1 ) { if ( $this->blockNumber != $this->lastBlock ) { throw new ezcArchiveInternalException( "Something weird happened with the blockNumber. Lastblock number registered at " . $this->lastBlock . " but changed into " . $this->blockNumber ); } } $this->lastBlock = $this->blockNumber; $this->isValid = false; return false; } $this->blockNumber++; return $this->blockData; } return false; } /** * Returns the key, the current block number of the current element. * * The first block has the number zero. * * @return int */ public function key() { return ( $this->isValid ? $this->blockNumber : false ); } /** * Returns true if the current block is valid, otherwise false. * * @return bool */ public function valid() { return $this->isValid; } /** * Returns true if the current block is a null block, otherwise false. * * A null block is a block consisting of only NUL characters. * If the current block is invalid, this method will return false. * * @return bool */ public function isNullBlock() { if ( $this->isValid ) { for ( $i = 0; $i < $this->blockSize; $i++ ) { if ( ord( $this->blockData[$i] ) != 0 ) { return false; } } return true; } return false; } /** * Appends with truncate. */ private function appendTruncate() { if ( $this->fileAccess == self::READ_ONLY ) { throw new ezcBaseFilePermissionException( $this->fileName, ezcBaseFilePermissionException::WRITE, "The archive is opened in a read-only mode." ); } if ( !$this->isEmpty && $this->isValid ) { $needToTruncate = true; $currentBlock = $this->blockNumber; // Do we need to truncate the file? // Check if we already read the entire file. This way is quicker to check. if ( $this->lastBlock != -1 ) { // Last block is known. if ( $this->lastBlock == $this->blockNumber ) { // We are at the last block. $needToTruncate = false; } } else { // The slower method. Check if we can read the next block. if ( !$this->next() ) { // We got a next block. $needToTruncate = false; } } if ( $needToTruncate ) { if ( $this->fileAccess == self::READ_APPEND ) { // Sorry, don't know how to truncate this file (except copying everything). throw new ezcArchiveException( "Cannot truncate the file" ); } if ( $this->blockNumber < $this->lastBlock ) { throw new ezcArchiveInternalException( "Expected to be at the last block." ); } $pos = $currentBlock * $this->blockSize; ftruncate( $this->fp, $pos ); $this->lastBlock = $currentBlock; $this->blockNumber = $currentBlock; } } else { if ( !$this->isEmpty && !$this->isValid ) { throw new ezcArchiveInternalException( "Not at a valid block position to append" ); } } } /** * Appends the string $data after the current block. * * The blocks after the current block are removed and the $data will be * appended. The data will always be appended after the current block. * To replace the data from the first block, the truncate() method should * be called first. * * Multiple blocks will be written when the length of the $data exceeds * the block size. If the data doesn't fill an entire block, the rest * of the block will be filled with NUL characters. * * @param string $data Data that should be appended. * @return int The total amount of blocks added. * * @throws ezcBaseFilePermissionException when the file is opened in read-only mode. */ public function append( $data ) { $this->appendTruncate(); // We are at the end of the file. Let's append the data. // Switch write mode, if needed. $this->switchWriteMode(); $dataLength = sizeof( $data ); $length = $this->writeBytes( $data ); if ( ( $mod = ( $length % $this->blockSize ) ) > 0 ) { $this->writeBytes( pack( "a". ( $this->blockSize - $mod ), "" ) ); } $addedBlocks = ( (int) (($length - 1) / $this->blockSize ) ) + 1; // Added the blocks. $this->isModified = true; $this->isEmpty = false; $this->blockNumber += $addedBlocks; $this->lastBlock += $addedBlocks; $this->blockData = $data; $this->isValid = true; $this->switchReadMode(); return $addedBlocks; } /** * Appends the data from the given file $fileName to the current block file. * * The blocks after the current block are removed and the data will be * appended. The data will always be appended after the current block. * To replace the data from the first block, the truncate() method should * be called first. * * Multiple blocks will be written when the length of the data exceeds * the block size. If the data doesn't fill an entire block, the rest * of the block will be filled with NUL characters. * * @param string $fileName The filename that contains the data. * @return int The total amount of blocks added. * * @throws ezcBaseFilePermissionException when the file is opened in read-only mode. */ public function appendFile( $fileName ) { $this->appendTruncate(); $this->switchWriteMode(); $localFile = @fopen( $fileName, "rb" ); if ( !$localFile ) { throw new ezcArchiveException( "Cannot open the file '{$fileName}' for reading." ); } $addedBlocks = 0; $length = 0; while ( !feof( $localFile ) && ( $data = fread( $localFile, $this->blockSize ) ) !== false ) { $addedBlocks++; $length = $this->writeBytes( $data ); } if ( ( $mod = ( $length % $this->blockSize ) ) > 0 ) { $this->writeBytes( pack( "a". ( $this->blockSize - $mod ), "" ) ); } fclose( $localFile ); // Added the blocks. $this->isModified = true; $this->isEmpty = false; $this->blockNumber += $addedBlocks; $this->lastBlock += $addedBlocks; $this->blockData = $data; $this->isValid = true; $this->switchReadMode(); return $addedBlocks; } /** * Write the given string $data to the current file. * * This method tries to write the $data to the file. Upon failure, this method * will retry, until no progress is made anymore. And eventually it will throw * an exception. Sometimes an (invalid) interrupt may stop the writing process. * * @throws ezcBaseFileIoException if it is not possible to write to the file. * * @param string $data * @return void */ protected function writeBytes( $data ) { $dl = strlen( $data ); if ( $dl == 0 ) { return; // No bytes to write. } $wl = fwrite( $this->fp, $data ); // Partly written? For example an interrupt can occur when writing a remote file. while ( $dl > $wl && $wl != 0 ) { // retry, until no progress is made. $data = substr( $data, $wl ); $dl = strlen( $data ); $wl = fwrite( $this->fp, $data ); } if ( $wl == 0 ) { throw new ezcBaseFileIoException ( $this->fileName, ezcBaseFileIoException::WRITE, "Retried to write, but no progress was made. Disk full?" ); } return $wl; } /** * Appends one block with only NUL characters to the file. * * @throws ezcBaseFilePermissionException if the file is opened in read-only mode. * @todo rename to appendNullBlocks * * @param int $amount * @return void */ public function appendNullBlock( $amount = 1 ) { $this->append( pack( "a". ( $amount * $this->blockSize ), "" ) ); } /** * Truncate the current block file to $block blocks. * * If $blocks is zero, the entire block file will be truncated. After the file is truncated, * make sure the current block position is valid. So, do a rewind() after * truncating the entire block file. * * @param int $blocks * @return void */ public function truncate( $blocks = 0 ) { // Empty files don't need to be truncated. if ( $this->isEmpty() ) { return true; } if ( $this->fileAccess !== self::READ_APPEND ) { // We can read-write in the file. Easy. $pos = $blocks * $this->blockSize; ftruncate( $this->fp, $pos ); $this->isModified = true; if ( $pos == 0 ) { $this->isEmpty = true; } if ( $this->blockNumber >= $blocks ) { $this->isValid = false; } $this->lastBlock = $blocks - 1; return true; } // Truncate at the end? if ( !$this->isValid ) { $this->rewind(); } // XXX check this, can be done via getLastBlockNumber() ? while ( $this->isValid && $blocks > $this->blockNumber ) { $this->next(); } if ( $this->isValid ) { throw new ezcArchiveInternalException( "Failed to truncate the file" ); } return true; } /** * Sets the current block position. * * The new position is obtained by adding the $blockOffset amount of blocks to * the position specified by $whence. * * These values are: * SEEK_SET: The first block, * SEEK_CUR: The current block position, * SEEK_END: The last block. * * The blockOffset can be negative. * * @param int $blockOffset * @param int $whence * @return void */ public function seek( $blockOffset, $whence = SEEK_SET ) { if ( $this->fileAccess == self::WRITE_ONLY && $blockOffset == 0 && $whence == SEEK_END ) { return true; } if ( ftell( $this->fp ) === false || $this->fileAccess == self::READ_APPEND ) { // Okay, cannot tell the current file position. // This happens with some compression streams. if ( !$this->isValid ) { if ( $whence == SEEK_CUR ) { throw new ezcArchiveException( "Cannot seek SEEK_CUR with an invalid block position" ); } $this->rewind(); } if ( $whence == SEEK_END && $this->lastBlock == -1 ) { // Go to the end. while ( $this->next() ); } switch ( $whence ) { case SEEK_CUR: $searchBlock = $this->blockNumber += $blockOffset; break; case SEEK_END: $searchBlock = $this->lastBlock += $blockOffset; break; case SEEK_SET: $searchBlock = $blockOffset; break; } if ( $searchBlock < $this->blockNumber ) { $this->rewind(); } while ( $this->isValid && $this->blockNumber < $searchBlock ) { $this->next(); } return ( $this->blockNumber == $searchBlock ); } else { $this->isValid = true; $pos = $this->blockSize * $blockOffset; if ( $whence == SEEK_END || $whence == SEEK_CUR ) { if ( !$this->isEmpty() ) { $pos -= $this->blockSize; } } if ( !( $whence == SEEK_SET && $pos == ftell( $this->fp ) ) ) { $this->positionSeek( $pos, $whence ); } if ( ftell( $this->fp ) === false ) { throw new ezcArchiveException( "Cannot tell the current position, but this is after the position seek. " ); } $this->blockNumber = $this->getBlocksFromBytes( ftell( $this->fp ) ) - 1; $this->next(); // Will set isValid to false, if blockfile is empty. } } /** * Calculates the blocks for the given $bytes. * * @param int $bytes * @return int */ public function getBlocksFromBytes( $bytes ) { return (int) ceil ($bytes / $this->blockSize ); } /** * Returns true if the blockfile is empty, otherwise false. * * @return bool */ public function isEmpty() { return $this->isEmpty; } /** * Returns the last block number. * * @return int */ public function getLastBlockNumber() { return $this->lastBlock; } } ?>