blob: 493a40dc25a4778c8ab5ca1c72383e2a7acf4993 [file] [log] [blame]
<?php
/**
* File contains the ezcArchiveV7Tar 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
*/
/**
* The ezcArchiveV7Tar class implements the Tar v7 archive format.
*
* ezcArchiveV7Tar is a subclass from {@link ezcArchive} that provides the common interface.
* Tar v7 algorithm specific methods are implemented in this class.
*
* ezcArchiveV7Tar reads on creation only the first {@link ezcArchiveEntry} from the archive.
* When needed next entries are read.
*
* The V7 Tar algorithm is most basic implementation of Tar. This format has the following characteristics:
* - Filenames up to 100 characters.
* - Stores the file permissions.
* - Stores the owner and group by ID.
* - Stores the last modification time.
* - Can archive: regular files and symbolic links.
* - Maximum file size: 8 Gygabyte.
*
* @package Archive
* @version //autogentag//
*/
class ezcArchiveV7Tar extends ezcArchive
{
/**
* Amount of bytes in a block.
*/
const BLOCK_SIZE = 512;
/**
* Tar archives have always $blockFactor of blocks.
*
* @var int
*/
protected $blockFactor = 20;
/**
* Stores all the headers from the archive.
*
* The first header of the archive has index zero. The ezcArchiveV7Header or a subclass from this header
* is stored in the array. {@link createTarHeader()} will create the correct header.
*
* @var array(ezcArchiveV7Header)
*/
protected $headers;
/**
* Stores the block number where the header starts.
*
* The fileNumber is the index of the array.
*
* @var array(int)
*/
protected $headerPositions;
/**
* Specifies if the archive contains null blocks.
*
* @var bool
*/
protected $hasNullBlocks;
/**
* Stores the number of added blocks.
*
* @var int
*/
protected $addedBlocks = 0;
/**
* Specifies if unreliable blocks were added.
*
* @var bool
*/
protected $addedBlocksNotReliable = false;
/**
* Initializes the Tar and tries to read the first entry from the archive.
*
* At initialization it sets the blockFactor to $blockFactor. Each tar archive
* has always $blockFactor of blocks ( 0, $blockFactor, 2 * $blockFactor, etc ).
*
* The Tar archive works with blocks, so therefore the first parameter expects
* the archive as a blockFile.
*
* @param ezcArchiveBlockFile $file
* @param int $blockFactor
*/
public function __construct( ezcArchiveBlockFile $file, $blockFactor = 20 )
{
$this->blockFactor = $blockFactor;
$this->file = $file;
$this->headers = array();
$this->headerPositions = array();
$this->entriesRead = 0;
$this->fileNumber = 0;
$this->hasNullBlocks = $this->file->isNew() ? false : true;
$this->addedBlocks = 0;
if ( $this->file->getFileAccess() !== ezcArchiveFile::WRITE_ONLY )
{
$this->readCurrentFromArchive();
}
}
/**
* Closes the archive.
*/
public function __destruct()
{
$this->close();
}
/**
* Returns the value which specifies a TAR_V7 algorithm.
*
* @return int
*/
public function getAlgorithm()
{
return self::TAR_V7;
}
/**
* Returns true because the TAR_V7 algorithm can write.
*
* @see isWritable()
*
* @return bool
*/
public function algorithmCanWrite()
{
return true;
}
/**
* Creates the a new tar header for this class.
*
* Usually this class is reimplemented by other Tar algorithms, and therefore it returns another Tar
* header.
*
* This method expects an {@link ezcArchiveBlockFile} that points to the header that should be
* read (and created). If null is given as block file, an empty header will be created.
*
* @param string|null $file
* @return ezcArchiveV7Header The ezcArchiveV7Header or a subclass is returned.
*/
protected function createTarHeader( $file = null )
{
return new ezcArchiveV7Header( $file );
}
/**
* Read the current entry from the archive.
*
* The current entry from the archive is read, if possible. This method will set the {@link $completed}
* to true, if the end of the archive is reached. The {@link $entriesRead} will be increased, if the
* entry is correctly read.
*
* @throws ezcArchiveBlockSizeException
* if the file is empty
* or if the file is not valid
* @return bool
*/
protected function readCurrentFromArchive()
{
// Not cached, read the next block.
if ( $this->entriesRead == 0 )
{
$this->file->rewind();
if ( !$this->file->isEmpty() && !$this->file->valid() )
{
throw new ezcArchiveBlockSizeException( $this->file->getFileName(), "At least one block expected in tar archive" );
}
}
else
{
// Search the new block.
$newBlock = $this->headerPositions[ $this->fileNumber - 1 ] + $this->file->getBlocksFromBytes( $this->headers[ $this->fileNumber - 1 ]->fileSize );
// Search for that block.
if ( $newBlock != $this->file->key() )
{
$this->file->seek( $newBlock );
}
// Read the new block.
$this->file->next();
}
// This might be a null block.
if ( !$this->file->valid() || $this->file->isNullBlock() )
{
$this->completed = true;
return false;
}
$this->headers[ $this->fileNumber ] = $this->createTarHeader( $this->file );
$this->headerPositions[ $this->fileNumber ] = $this->file->key();
// Set the currentEntry information.
$struct = new ezcArchiveFileStructure();
$this->headers[ $this->fileNumber ]->setArchiveFileStructure( $struct );
$this->entries[ $this->fileNumber ] = new ezcArchiveEntry( $struct );
$this->entriesRead++;
return true;
}
/**
* Writes the file data from the current entry to the given file.
*
* @param string $targetPath The absolute or relative path of the target file.
* @return bool
*/
protected function writeCurrentDataToFile( $targetPath )
{
if ( !$this->valid() )
{
return false;
}
$requestedBlock = $this->headerPositions[$this->fileNumber];
$currentBlock = $this->file->key();
if ( $currentBlock != $requestedBlock )
{
$this->file->seek( $requestedBlock );
}
$header = $this->headers[ $this->fileNumber ];
if ( $header->fileSize > 0 )
{
$completeBlocks = ( int ) ( $header->fileSize / self::BLOCK_SIZE );
$rest = ( $header->fileSize % self::BLOCK_SIZE );
// Write to file
$fp = fopen( $targetPath, "w" );
for ( $i = 0; $i < $completeBlocks; $i++ )
{
fwrite( $fp, $this->file->next() );
}
fwrite( $fp, $this->file->next(), $rest );
fclose( $fp );
}
else
{
touch( $targetPath );
}
return true;
}
/**
* Truncates the archive to $fileNumber of files.
*
* The $fileNumber parameter specifies the amount of files that should remain.
* If the default value, zero, is used then the entire archive file is cleared.
*
* @throws ezcArchiveException
* if the archive is closed
* @throws ezcBaseFilePermissionException
* if the file is read-only
* or if the current algorithm cannot write
* @param int $fileNumber
* @return bool
*/
public function truncate( $fileNumber = 0 )
{
if ( $this->file === null )
{
throw new ezcArchiveException( "The archive is closed" );
}
if ( $this->file->getFileAccess() === ezcArchiveFile::READ_ONLY || !$this->algorithmCanWrite() )
{
throw new ezcBaseFilePermissionException( $this->file->getFileName(), ezcBaseFilePermissionException::WRITE, "Archive is read-only" );
}
$originalFileNumber = $this->fileNumber;
$this->hasNullBlocks = false;
$this->addedBlocksNotReliable = true;
// Entirely empty the file.
if ( $fileNumber == 0 )
{
$this->file->truncate();
$this->entriesRead = 0;
$this->fileNumber = 0;
$this->completed = true;
}
else
{
$this->seek( $fileNumber ); // read the headers.
$endBlockNumber = $this->headerPositions[ $fileNumber - 1 ] + $this->file->getBlocksFromBytes( $this->headers[ $fileNumber - 1 ]->fileSize );
if ( $endBlockNumber === false )
{
return false;
}
if ( !$this->file->truncate ( $endBlockNumber + 1 ) )
{
throw new ezcArchiveException( "The archive cannot be truncated to " . ( $endBlockNumber + 1 ) . " block(s). " .
"This happens with write-only files or stream (e.g. compress.zlib) " );
}
$this->entriesRead = $fileNumber;
$this->completed = true;
$this->fileNumber = $originalFileNumber;
return $this->valid();
}
}
/**
* Appends a file to the archive after the current entry.
*
* One or multiple files can be added directly after the current file.
* The remaining entries after the current are removed from the archive!
*
* The $files can either be a string or an array of strings. Which, respectively, represents a
* single file or multiple files.
*
* $prefix specifies the begin part of the $files path that should not be included in the archive.
* The files in the archive are always stored relatively.
*
* Example:
* <code>
* $tar = ezcArchive( "/tmp/my_archive.tar", ezcArchive::TAR );
*
* // Append two files to the end of the archive.
* $tar->seek( 0, SEEK_END );
* $tar->appendToCurrent( array( "/home/rb/file1.txt", "/home/rb/file2.txt" ), "/home/rb/" );
* </code>
*
* When multiple files are added to the archive at the same time, thus using an array, does not
* necessarily produce the same archive as repeatively adding one file to the archive.
* For example, the Tar archive format, can detect that files hardlink to each other and will store
* it in a more efficient way.
*
* @throws ezcArchiveWriteException if one of the files cannot be written to the archive.
* @throws ezcFileReadException if one of the files cannot be read from the local filesystem.
* @throws ezcArchiveException if the archive is closed.
*
* @param string|array(string) $files Array or a single path to a file.
* @param string $prefix First part of the path used in $files.
* @return bool
*/
public function appendToCurrent( $files, $prefix )
{
if ( $this->file === null )
{
throw new ezcArchiveException( "The archive is closed" );
}
if ( $this->file->getFileAccess() !== ezcArchiveFile::READ_WRITE )
{
throw new ezcArchiveException( "Cannot appendToCurrent when writing to a read-only, write-only stream (e.g. compress.zlib)." );
}
if ( $this->file->getFileAccess() === ezcArchiveFile::READ_ONLY || !$this->algorithmCanWrite() )
{
throw new ezcBaseFilePermissionException( $this->file->getFileName(), ezcBaseFilePermissionException::WRITE );
}
$entries = $this->getEntries( $files, $prefix );
$originalFileNumber = $this->fileNumber;
for ( $i = 0; $i < sizeof( $files ); $i++ )
{
// Changes the fileNumber
$this->appendHeaderAndFileToCurrent( $entries[$i] );
}
$this->fileNumber = $originalFileNumber;
return $this->valid();
}
/**
* Append a file or directory to the end of the archive. Multiple files or directory can
* be added to the archive when an array is used as input parameter.
*
* @see appendToCurrent()
*
* @throws ezcArchiveWriteException if one of the files cannot be written to the archive.
* @throws ezcFileReadException if one of the files cannot be read from the local filesystem.
* @throws ezcArchiveException if the archive is closed.
*
* @param string|array(string) $files Add the files and or directories to the archive.
* @param string $prefix First part of the path used in $files.
* @return bool
*/
public function append( $files, $prefix )
{
if ( $this->file === null )
{
throw new ezcArchiveException( "The archive is closed" );
}
if ( $this->file->getFileAccess() === ezcArchiveFile::READ_ONLY || !$this->algorithmCanWrite() )
{
throw new ezcArchiveException( "Archive is read-only" );
}
// Appending to an existing archive with a compressed stream does not work because we have to remove the NULL-blocks.
if ( $this->hasNullBlocks && $this->file->getFileAccess() !== ezcArchiveFile::READ_WRITE )
{
throw new ezcArchiveException( "Cannot append to this archive" );
}
// Existing files need to be read, because we don't know if it contains NULL-blocks at the end of the archive.
if ( $this->file->getFileAccess() !== ezcArchiveFile::WRITE_ONLY )
{
$this->seek( 0, SEEK_END );
}
// Do the same as in appendToCurrent(). But we know that it's possible.
$entries = $this->getEntries( $files, $prefix );
$originalFileNumber = $this->fileNumber;
for ( $i = 0; $i < sizeof( $files ); $i++ )
{
// Changes the fileNumber
$this->appendHeaderAndFileToCurrent( $entries[$i] );
}
$this->fileNumber = $originalFileNumber;
return $this->valid();
}
/**
* Closes the archive correctly.
*/
public function close()
{
if ( $this->file !== null )
{
$this->writeEnd();
$this->file->close();
$this->file = null;
}
}
/**
* Writes the end of the archive.
*
* @throws ezcArchiveException
* if the archive is closed
*/
public function writeEnd()
{
if ( $this->file === null )
{
throw new ezcArchiveException( "The archive is closed" );
}
if ( $this->file->isModified() )
{
if ( !$this->hasNullBlocks )
{
if ( $this->addedBlocksNotReliable )
{
$this->appendNullBlocks();
}
else
{
// Added Blocks - Added null blocks (Block factor 20)
// 0 - 0
// 1 - 19
// 19 - 1
// 20 - 0
// 21 - 19
$nullBlocks = ( $this->blockFactor - ( $this->addedBlocks % $this->blockFactor ) ) % $this->blockFactor;
$this->file->appendNullBlock( $nullBlocks );
}
$this->hasNullBlocks = true;
$this->addedBlocksNotReliable = false;
$this->addedBlocks = 0;
}
}
}
/**
* Appends the given {@link ezcArchiveBlockFile} $file and {@link ezcArchiveEntry} $entry
* to the archive file.
*
* The $entry will be used to create the correct header, whereas the $file contains the raw data
* that should be append to the archive.
*
* @param ezcArchiveEntry $entry
* @return bool
*/
protected function appendHeaderAndFileToCurrent( $entry )
{
// Are we at a valid entry?
if ( !$this->isEmpty() && !$this->valid() )
{
return false;
}
if ( !$this->isEmpty() && $this->file->getFileAccess() !== ezcArchiveFile::WRITE_ONLY )
{
// Truncate the next file and don't add the null blocks.
$this->truncate( $this->fileNumber + 1, false );
}
if ( $this->entriesRead == 0 )
{
$this->fileNumber = 0;
}
else
{
$this->fileNumber++;
}
// Add the new header to the file map.
$this->headers[ $this->fileNumber ] = $this->createTarHeader();
$this->headers[ $this->fileNumber ]->setHeaderFromArchiveEntry( $entry );
// Search the end of the block file, append encoded header, and search for the end-again.
$this->file->seek( 0, SEEK_END );
$this->headers[$this->fileNumber]->writeEncodedHeader( $this->file );
// Add the new blocknumber to the map.
$this->headerPositions[$this->fileNumber] = $this->file->key();
// Append the file, if needed.
$this->addedBlocks += 1;
if ( $entry->getSize() > 0 )
{
$this->addedBlocks += $this->file->appendFile( $entry->getPath() );
}
if ( !( $this->file->isNew() && $this->file->getFileAccess() === ezcArchiveFile::WRITE_ONLY ) )
{
$this->addedBlocksNotReliable = true;
}
$this->hasNullBlocks = false;
$this->completed = true;
$this->entriesRead++;
$this->entries[$this->fileNumber] = $entry;
return true;
}
/**
* Appends zero or more null blocks to the end of the archive, so that it matches the $blockFactor.
*
* If the archive has already the correct size, no null blocks will be appended. Otherwise as many
* null blocks are appended (up to $blockFactor - 1) so that it matches the $blockFactor.
*
* @return void
*/
protected function appendNullBlocks()
{
$last = 0;
if ( $this->file->getLastBlockNumber() == -1 )
{
if ( !$this->file->valid() )
{
$this->file->rewind();
}
while ( $this->file->valid() )
{
$last = $this->file->key();
$this->file->next();
}
}
else
{
$last = $this->file->getLastBlockNumber();
}
$this->file->seek( $last );
// Go to the end.
/*
*/
// echo ("Last block: " . $this->file->getLastBlockNumber() );
// Need a ftell in the seek.
// $this->file->seek( 0, SEEK_END );
$blockNumber = $last;
// 0 .. 19 => first block.
// 20 .. 39 => second block.
// e.g: 20 - ( 35 % 20 ) - 1 = 19 - 15 = 4
$append = $this->blockFactor - ( $blockNumber % $this->blockFactor ) - 1;
$this->file->appendNullBlock( $append );
}
}
?>