blob: bb75b79bad130c3ef6a9e949e56cd16c1251094e [file] [log] [blame]
<?php
/**
* File contains the ezcArchiveBlockFile class.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @package Archive
* @version //autogentag//
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
* @access private
*/
/**
* The ezcArchiveBlockFile class provides an interface for reading from and writing to a block file.
*
* A block file is a file that consist of zero or more blocks. Each block has a predefined amount
* of bytes known as the block-size. The block file implements the Iterator interface. Via the methods
*
* - key()
* - valid()
* - current()
* - rewind()
*
* can single blocks be accessed. The append(), appendToCurrent() method, appends and possibly removes
* blocks from and to the file.
*
* The block-file takes the necessary measurements to read and write to a compressed stream.
*
* The following example stores 16 bytes in 4 blocks. The current() points to the second block: 1.
* <code>
* ---- ---- ---- ----
* |0 1 | 5 6| 9 0| 3 4|
* |2 4 | 7 8| 1 2| 5 6|
* ---- ---- ---- ----
* 0 1 2 3
*
* ^ ^
* | \ LastBlock
*
* Current
* </code>
*
*
* @package Archive
* @version //autogentag//
* @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.
* @apichange 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();
}
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;
}
}
?>